Securing a Single Page Application is a very important part of its implementation, yet sometimes it brings a lot of confusion, especially when there are many ways to achieve it. In this article, I will focus on the approach utilizing JSON Web Tokens (JWT) as a mechanism to convey user rights. Moreover, I will present the benefits and potential pitfalls of JWT-based security.
In this article, you will learn:
- how to restrict access to the given parts of Angular application, using Router Guards
- how to intercept HTTP calls, adding an Access Token as it is required by the server
- why we need a Refresh Token and how to use it transparently for the user
Table of Contents
Application setup
Let’s think of the common use case where there are some pages (routes) in the application that the access to is restricted only for authorized users. After successful authentication, for example via a login form, the user is granted with an access to some restricted parts of the system (for example an admin page).
Authentication is the process of proving one’s identity. If we talk about login form, we assume that if a person is in the possession of the password associated with the given username, then that must be the person that this username belongs to.
Authorization happens after successful authentication and determines if the given user is authorized to access given resources (for example subpages in SPA).
For the sake of simplicity let’s assume that we have an application with a login page, available under /login
route, and a page displaying a random number generated by the server, available under /secret-random-number
. The random number page should be available only for the authorized users. If we manually try to access /secret-random-number
we should be redirected back to the login page.
Router Guards
To achieve the goal of restricting access to /secret-random-number
and redirecting back to the login page, in case the user is not logged in, we can make use of Angular’s built-in mechanism called Router Guards
. These guards allow us to implement policies governing possible route transitions in an Angular application. Imagine a situation when a user tries to open a page that he has no access rights to. In such a case application should not allow this route transition. To achieve this goal we can make use of CanActivate
guard. As Router Guards
are just simple class providers, we need to implement a proper interface. Let’s take a look at below code snippet presenting AuthGuard
.
1 | ({ |
AuthGuard
implements canActivate()
which tells Angular router whether it can or cannot activate a particular route. To attach given guard to the route that it should protect, we just need to place its reference in canActivate
property of that route as presented below. In our case, we want to protect the /login
route. We want to allow users to open this route, only if they are not logged in. Otherwise, we redirect to /secret-random-number
. The same approach applies to protecting other routes, with different policies implemented for given routes. Also, we can notice the canLoad
property in below routes configuration. This kind of protection allows us to prevent a lazy-loaded route from being fetched from the server. Usually, canLoad
guards implement the same policy as canActivate
guards.
1 | const routes: Routes = [ |
JSON Web Token
We came to the point where we have secured the routes in our application. The next step is to think about HTTP requests that the application sends to the server. If we only prevent the user from performing forbidden actions in our application, we will still be prone to the unauthorized HTTP calls that could be executed by the user, for example with any other HTTP client. Because of that, what is more important in securing a web application is making sure that the unauthorized server requests are not allowed. To make it possible for the server to recognize if the request is coming from an authorized user, we can attach an additional HTTP header indicating that fact. Here is the place where JSON Web Tokens (JWT) come into play.
The general idea standing behind JWT is to securely transmit information between parties. In our case, it is the user’s identity along with his rights, transmitted between the client (browser) and the server. When the user logs in, sending login query to the server, he receives back a JWT (aka access token) signed by the server with a private key. This private key should be known only to the server as it allows the server later to verify that the token is legitimate. When JWT is transmitted between the browser and the server, it is encoded with Base64 algorithm, that makes it look like a string of random characters (nothing could be further from the truth!). If you take a JWT and decode it with Base64 you will find a JSON object. Below you can find a decoded content of a JWT from our example application. On jwt.io you can play with JWT online.
Every JWT is composed of 3 blocks: header, payload, and signature. The header defines the type of the token and the used algorithm. The payload is the place where we put the data we want to securely transmit. In this case, we have a username, role, issuing timestamp (iat) and expiration timestamp (exp). The last block (HMACSHA256 function) is a signature generated with HMAC and SHA-256 algorithms. The signature guarantees not only that the token was created by a known party, but also the token’s integrity.
Integrity is the assurance of the accuracy and consistency of the data over its lifetime. In the case of JWT token, it means that it has not been altered during the transmission.
1 | { |
When the user successfully logs into the application and receives an access token, it has to be persisted somehow by the application. We can use for example local storage of the browser to save that token. It is fairly convenient and easy to implement, but it’s prone to XSS attacks. Another approach could be to use HttpOnly Cookie which is considered safer than local storage. Once we have JWT persisted, we will be attaching it to outgoing requests in HTTP Header. Before we dive into that aspect, let’s take a look at another important characteristic of JWT.
At this point, it is worth taking a closer look at the self-contained nature of JWT. When the server receives HTTP requests with JWT Access Token, it does not have to ask any persistence layer (for example database) for the verification of users rights. Those rights are inside the token. And since we guarantee authenticity and integrity of Access Token we can trust the information inside it. This is a really interesting feature of JWT because it opens the door for higher scalability of the system. Alternative scenarios would require saving some session id on the backend side and asking for it each and every time there is a need to authorize the request. Having self-contained Access Token, we don’t have to replicate token among server clusters or implement sticky sessions.
If you are interested in learning more about building secure Web applications consider joining our flagship program Web Security Academy. It will teach you everything you need to know in that area. Some of the actionable guidelines are also covered in our secure programming training blog post.
Http interceptor
Once we have our Access Token (JWT) persisted after user logs into the application, we want to use it to authorize outgoing requests. One approach could be to simply update every service that communicates with API to enrich requests with additional HTTP Header. This will result in a lot of duplicated code comparing to approach with HTTP Interceptor. The goal of HTTP Interceptor is to apply some processing logic to every outgoing request in the application.
Creating an HTTP interceptor is quite similar to creating a Router Guard. We need to have a class that implements a specific interface with the required method. In this case, it is HttpInterceptor
with intercept
method. Take a look at following code snippet with the interceptor from our example application. First, we want to check if the token is available with this.authService.getJwtToken()
. If we have a token, we set an appropriate HTTP header. This code also contains error handling logic, which will be described later in this article.
1 | () |
Having implemented our interceptor, it is necessary to register it as a provider with HTTP_INTERCEPTORS
token in Angular module.
1 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; |
Refresh Token
Taking into account that JWT is self-contained we need to think about one more thing - there is no way to invalidate it! If someone other than us gets into possession of the token there is very little we can do about it. That’s why it is a good idea to always give the token short time of validity. There are no strict rules of how long a token should live and it depends on the system requirements. A good starting point could be to have a token that is only valid for 15 minutes. After that time server would not consider this token valid and would not authorize requests with it.
So here comes another challenge - we don’t want to force the user to login into the application, let’s say, every 15 minutes. The solution to this problem is a Refresh Token
. This kind of token lives somewhere on the server side (database, in-memory cache, etc) and is associated with the particular user’s session. It is important to notice that this token differs from JWT in many ways. First, it is not self-contained - it can be as simple as a unique random string. Second, we need to have it stored to be able to verify if user’s session is still alive. This gives us an ability to invalidate the session by simply removing the associated pair of [user, refresh_token]
. When there is an incoming request with Access Token that has become invalid, the application can send a Refresh Token to obtain a new Access Token. If the user’s session is still alive, the server would respond with a new valid JWT. In our example, we will be sending Refresh Token transparently for the user, so that he is not aware of the refreshing process.
Let’s get back to our interceptor. If you remember from the previous code snippet, in case of HTTP 401 Error (Unauthorized) we have a special method handle401Error
for handling this situation. Here comes a tricky part - we want to queue all HTTP requests in case of refreshing. This means that if the server responds with 401 Error, we want to start refreshing, block all requests that may happen during refreshing, and release them once refreshing is done. To be able to block and release requests during the refreshing, we will use BehaviorSubject
as a semaphore.
First, we check if refreshing has not already started and set isRefreshing
variable to true and populate null into refreshTokenSubject
behavior subject. Later, the actual refreshing request starts. In case of success, isRefreshing
is set to false and received JWT token is placed into the refreshTokenSubject
. Finally, we call next.handle
with the addToken
method to tell interceptor that we are done with processing this request. In case the refreshing is already happening (the else part of the if statement), we want to wait until refreshTokenSubject
contains value other than null. Using filter(token => token != null)
will make this trick! Once there is some value other than null (we expect new JWT inside) we call take(1)
to complete the stream. Finally, we can tell the interceptor to finish processing this request with next.handle
.
1 | private isRefreshing = false; |
As you see, the combination of Access Token and Refresh Token is a tradeoff between scalability and security. Restricting the validity time of Access Token decreases the risk of an unwanted person using it, but using Refresh Token requires statefulness on the server.
AuthService
The last missing part of our solution is AuthService
. This will be the place where we implement all the logic to handle logging in and out. Below you can find the source of that service and we will analyze it step by step.
Let’s start with the login
method. Here we use HttpClient
to execute post call to the server and apply some operators with pipe()
method. By using tap()
operator we are able to execute the desired side effect. On successful post method execution, we should receive Access Token and Refresh Token. The side effect we want to execute is to store these tokens calling doLoginUser
. In this example, we make use of localstorage. Once stored, the value in the stream is mapped to true in order for the consumer of that stream to know that the operation succeeded. Finally, in case of error, we show the alert and return observable of false.
Side effect is a term used in Functional Programming. This concept is opposite to functional purity which means that there are no state changes in the system and the function always returns the result based on its inputs (regardless of the system state). If there is a state change (for example variable change) we call it side effect.
Implementation of the logout
method is basically the same, apart from the fact, that inside of the request’s body we send refreshToken
. This will be used by the server to identify who is attempting to log out. Then, the server will remove the pair of [user, refresh_token]
and refreshing will not be possible anymore. Yet, Access Token will still be valid until it expires, but we remove it from the localstorage.
1 | ({ |
Summary
We have covered the most important pieces of designing an authorization mechanism on the frontend side in Angular. You can find full sources of frontend and backend side under GitHub repositories:
Using JWT as an Access Token has a lot of benefits and it’s fairly simple to implement. However, you should be aware of the limitations and possible XSS Attacks. The way to minimize the risk is to use HttpOnly Cookies to store the tokens.
If you liked this article, please share it on social media or leave a comment, so I know that it was helpful. You are also more than welcome to Join Angular Academy Slack!
If you are interested in more Angular-related material don’t forget to follow me on Twitter and subscribe to the email newsletter and to Angular Academy YouTube channel.
Comments