Protect the Routes
Overview
Now that we are authenticating with a provider we need to look at protecting our routes. This protection takes two major forms:
- Guarding our routes so a user cannot navigate to various places within our application unless they are logged in.
- Protecting our backend API such that users cannot access data without a valid access token. Our role is to pass the access token to our API.
We will also see how to handle the possibility that our APIs may now issue 401 errors in cases where our access token has expired or is otherwise invalid.
We will build upon the application we created in the getting started tutorial in order to implement route guards for our application's routes as well as to add HTTP interceptors to attach access tokens to outgoing requests and to handle potential 401 errors in responses.
Let's Code
As mentioned previously, this tutorial builds upon the application created when doing the getting started tutorial. If you have the code from when you performed that tutorial, then you are good to go. If you need the code you can make a copy from our GitHub repository.
Route Guards
We are using the Tab1Page
to manage our authentication status. Let's assume that the Tab2Page
and Tab3Page
should only be accessible if the user is authenticated. We already have a method that determines if the user is authenticated or not. In its most basic form, we use the existence of an AuthResult
with an access token to determine whether or not we are authenticated. Other tutorials show how this can be expanded.
We will use an Angular CanActivate guard to protect those routes. Generate the guard via npx ng generate guard core/guards/auth
:
Now that the guard has been generated, we need to build it out.
For this application, we want to guard the tab2
and tab3
routes. We cannot guard tab1
because we are using that one to log in.
Import the authGuard
and apply it to those two routes.
We can still navigate to the tab2
and tab3
routes because our guard always returns true
.
We need to make our guard async
and inject the AuthenticationService
.
We can then check the user's authentication status. We want to return true
if the user is authenticated. Otherwise we want to return false
.
For this application, we want to guard the tab2
and tab3
routes. We cannot guard tab1
because we are using that one to log in.
Import the authGuard
and apply it to those two routes.
We can still navigate to the tab2
and tab3
routes because our guard always returns true
.
We need to make our guard async
and inject the AuthenticationService
.
We can then check the user's authentication status. We want to return true
if the user is authenticated. Otherwise we want to return false
.
Test this in your app. You should see that you cannot navigate to Tab2Page
or Tab3Page
unless you are authenticated. This is exactly what we want and it works well.
Run the application in a web browser and navigate directly to http://localhost:8100/tabs/tab2
while not authenticated. The result will be a white screen. We cannot navigate to the path, but we also don't have an existing route upon which to remain. We need to navigate somewhere. Let's add some code to navigate to the Tab1Page
in cases such as this.
Now when navigating directly to http://localhost:8100/tabs/tab2
while not authenticated, the user will be redirected to Tab1Page
to authenticate. Note that this seems like something that only applies when running in the web and not a problem for our native application. This could be an issue for our native app, however, if it is using deep links or if our default route is protected.
Provide the Access Token
When a user logs in using Auth Connect the application receives an AuthResult
that represents the authentication session. The AuthResult
object provides access to several types of tokens:
- ID Token: The ID token contains information pertaining to the identity of the authenticated user. The information within this token is typically consumed by the client application.
- Access Token: The access token signifies that the user has properly authenticated. This token is typically sent to the application's backend APIs as a bearer token. The token is verified by the API to grant access to the protected resources exposed by the API. Since this token is used in communications with the backend API, a common security practice is to give it a very limited lifetime.
- Refresh Token: Since access tokens typically have a short lifetime, longer lived refresh tokens are used to extend the length of a user's authentication session by allowing the access tokens to be refreshed.
The most common way for the backend API to protect its routes is to require that requests include a valid access token. As such, we are going to have to send the access token with each request.
Add a method to the AuthenticationService
that gets the access token:
Modify the Tab1Page
to grab the access token.
Display the results in the page.
Add a method to the AuthenticationService
that gets the access token:
Modify the Tab1Page
to grab the access token.
Display the results in the page.
We would not normally grab the access token and display it like that. This is just being done to make sure everything is working. Log in and out a few times. You should see a token while logged in but not while logged out.
We will use an HTTP interceptor to attach the access token to outgoing HTTP requests. Use npx ng generate interceptor
to generate the code.
We now need to build up the interceptor and hook it up so it executes with each request.
The Angular CLI generated the start of the interceptor for us. This interceptor currently does nothing.
Inject the authentication service.
Not all requests require a token. For our made up use-case, paths ending in public
do not need a token.
Before passing the request to the next handler in the pipeline, get the access token if it is required for this request.
If the token exists add it to the request as a bearer token.
The main.ts
file needs to be updated to provide the interceptor.
This interceptors are typically provided with the HTTP client.
The Angular CLI generated the start of the interceptor for us. This interceptor currently does nothing.
Inject the authentication service.
Not all requests require a token. For our made up use-case, paths ending in public
do not need a token.
Before passing the request to the next handler in the pipeline, get the access token if it is required for this request.
If the token exists add it to the request as a bearer token.
The main.ts
file needs to be updated to provide the interceptor.
This interceptors are typically provided with the HTTP client.
Handle 401 Errors
Now that the access token is sent to the backend, we need to also handle the case where the backend rejects the access token resulting in a 401 - Unauthorized response status. This can be done through another HTTP interceptor.
The interceptor needs to be built out to clear the session data and navigate to the login page when a 401 error occurs.
We are starting with the generated interceptor.
Tap into the observable pipeline for the request. Notice that we only need to handle the error
case.
Inject the NavController
and SessionService
.
If the error is an HttpErrorResponse
and the status is 401
, clear the session and redirect so the user can log in again.
We are starting with the generated interceptor.
Tap into the observable pipeline for the request. Notice that we only need to handle the error
case.
Inject the NavController
and SessionService
.
If the error is an HttpErrorResponse
and the status is 401
, clear the session and redirect so the user can log in again.
Next Steps
Currently, if we have an AuthResult
with an access token we assume the user is properly authenticated. If you would like to expand this logic to first make sure the access token has not expired, and try to refresh it if it has, then please have a look at the tutorial on refreshing the session.
Happy coding!! 🤓