Member-only story
JWT Authentication in Angular
Let’s build an Angular app with JWT authentication.
In my last article, JWT Auth in ASP.NET Core, we talked about the implementation of JWT in the back-end. To follow up, this article will focus on the front-end part of the JWT story. You can find the front-end source code from the same GitHub repository as the back-end part.
To make JWT authentication work, the front-end application at least operates in the following scenes:
- Displays a login form, and sends user credentials to the back-end service to get user’s claims, a JWT access token, and a refresh token.
- Stores the JWT access token and refresh token in a browser’s localStorage, so that the application in different browser tabs can use the same tokens.
- Adds an authorization header when sending HTTP requests.
- Tracks the expiration time of the access token and sends a request to refresh tokens when the access token is about to expire.
- Removes the tokens from localStorage when the user logs out.
Today, we will build a simple app using Angular. We will implement an AuthService
class to handle login, logout, and refresh token processes, as well as operations for localStorage key-value pairs. We will create a JwtInterceptor
class to add a JWT Bearer token to the HTTP request headers, and an UnauthorizedInterceptor
class to redirect the user to the login page if an HTTP status code 401 is received. We will use an AuthGuard
to prevent unauthenticated users from visiting the application pages.
If you pull the code from my GitHub repository, then you can run the demo application on Linux Docker containers. Or, if you want, you can run the Angular app and the ASP.NET Core app separately. The following screen recording shows a demo of this app.
Now let’s dive into the code.
auth.service.ts
Let’s use Angular CLI to generate an auth.service.ts
file. I find that this AuthService
class is a little bit lengthy, so I have decided to first paste the skeleton of the AuthService
class, then I will explain its methods.
In the code above, the first three lines are the default decorator for an Angular service, which means that the service will be available globally for dependency injection.
Line 5 sets the back-end API URL, which is configured in the environment.ts
file and it will talk to the Account
controller in the ASP.NET Core web API project.
Line 6 refers to a private field timer
, which is used in two private methods startTokenTimer()
and stopTokenTimer()
. The startTokenTimer()
method is called inside the login()
method and the refreshToken()
method. We use rxjs observables to track the access token’s lifetime, so that when the token is about to expire, the timer
will trigger the refreshToken()
method to exchange a new set of tokens. On the other hand, when the logout()
method is called, the stopTokenTimer()
method will halt the timer
.
Lines 7 and 8 show a common pattern in Angular for sharing the user’s state. The user$
observable can be broadcast to all observers in services, components, guards, interceptors, and so on. Note that I don’t save the user’s state in the browser localStorage, so that end-users are not able to modify its value. Using the user$
observable can guarantee an immutable user state, which may contain information about user permissions and other claims.
Lines 10 to 20 are optional touch-ups. We define a storageEventListener
which will watch the value change events in the browser’s localStorage when the application starts, and the event listener will be removed when the application terminates. We need this global event listener because we want all browser tabs to sync with the user’s information upon login and logout events. As a result, if a user logs in the app from a browser tab, then the other tabs will also reflect the login status. Similarly, if a user logs out of the app from a tab, then all other tabs will be logged out as well. Most of online articles or tutorials miss this feature.
The following code snippet shows some more implementation details.
The storageEventListener
(lines 1 to 10) monitors the value changes for the login-event
and the logout-event
which are dispatched in the login()
and logout()
methods, respectively. When a user logs out, then other tabs will have a null
user, which could invalidate those sessions. When a user logs in, then other tabs will reload their current pages which are bonded with new session parameters. Line 19 is an example for dispatching the login-event
value change events.
The login()
method takes the username and password, and passes them to the login API endpoint. Upon a successful login, line 17 emits a new value to the _user
subject, so that all observers will get its latest value. Line 18 saves the access token and the refresh token in a browser’s localStorage, so that the tokens can be shared across browser tabs or windows.
Line 20 executes the startTokenTimer()
method, which starts a timer to count down. The getTokenRemainingTime()
method computes the access token’s expiration time by decoding the access token. The timer runs until the JWT access token is about to expire, then the timer calls the refreshToken()
method to refresh the tokens. If your app is a highly sensitive website, you may want to stop refresh tokens after certain amount of times, then you can create another key-value pair in localStorage to track it.
I will omit the code for logout()
and refreshToken()
methods for simplicity. Let’s move on to the AuthGuard
.
auth.guard.ts
We use Angular CLI to generate a guard which controls the access of desired routes. In this demo app, we implement the canActivate
method which listens to the user$
observable in the AuthService
class, so that if the user$
observable emits a null
value, then route navigation will end up at the login page. The following code snippet shows an example implementation of the AuthGuard
class.
Then in the app-routing.module.ts
file, we can protect some routes using the canActivate
lifecycle hook like below.
jwt.interceptor.ts
and unauthorized.interceptor.ts
We need an HTTP interceptor to add an authorization header, so that all requests sent to the back-end API endpoints will have the access token for identity purposes. Angular CLI can easily generate the interceptor’s skeleton for us. We simply need to clone the original HTTP request, and attach the Bearer token to the Authorization header. The following code snippet shows an example implementation of the JwtInterceptor
class.
If a request returns an HTTP status code 401, then it means the current user’s identity is no longer permitted to the resource, so we should redirect the user to the login page. We can use another HttpInterceptor
to deal with the 401 responses. An example UnauthorizedInterceptor
class is shown below.
In a production-ready app, we may need to implement another service to gracefully handle all errors. We will not discuss that in this article.
app-initializer.ts
and core.module.ts
It is a good practice to refresh tokens when the app is first loaded in a browser tab, in order to improve user experience. To do that, we write an appInitializer
function like below.
The appIntializer
, JwtInterceptor
, and UnauthorizedInterceptor
are registered in an Angular module as follows.
Finally, we can import the CoreModule
into the AppModule
, so that the three providers above can work globally.
I think I have touched all the bases for implementing our Angular app. We can try out the app using the ng serve
command in Angular CLI, after we start the ASP.NET Core web API app. Please also try the app in two or more browser tabs and play with the login/logout functionalities.
Serve the Angular App with NGINX on Docker
I also include a Dockerfile
for the Angular app, so that it can be served by an NGINX server in a Docker container. I have written another article, Get Started with NGINX on Docker, which talks about the configurations of Docker and NGINX, so I won’t repeat them here.
To echo the beginning of this article, we can also run the app using Docker Compose, so that both the back-end app and the front-end app can run simultaneously.
Conclusion
That’s all for today. We have implemented an Angular app with JWT authentication, and you can play with it on multiple browser tabs/windows. Again, the complete solution is in my GitHub repository. Hope it helps. Thanks for reading.