Categories: Angular

Securing your Spring Boot and Angular app with JWT #3 – Frontend

There are several issues that need to be covered when you want to secure a frontend module of a Spring Boot and Angular app. Learn how to handle security based on JWT and enhance user experience with the AuthGuard functionality.

What we are going to build

In the Securing your Spring Boot and Angular app with JWT #1 – Introduction post you can find the description of the secured multi-module application which we are going to create.

In the Securing your Spring Boot and Angular app with JWT #2 – Backend post you can find the details of safeguarding the backend module.

The finished project is available in the GitHub repository – little-pinecone/jwt-spring-boot-angular-scaffolding.

In this post we are focusing on the frontend module.

To keep the code snippets reasonably short in this post, I don’t include the imports and full tests here. You can find them in the source code so don’t hesitate to inspect the repo.

Architecture overview

You can see the final directory tree for the frontend module on the image below:

Start the Angular app

Let’s start the frontend module with the following command:

$ ng serve

The landing page displays only the link to the Cookie dispenser page, where the cookies received from the API should be listed:

Clicking on the Cookie dispenser link triggers the 401 error:

The request for cookies was send to the API, but the backend module expects a valid JWT token to grant access to the secured resources. So far, everything works as expected – the API security blocks unauthorized calls to protected endpoints.

We need to apply login functionality, obtain a valid token and attach it to every API call. Furthermore, we have to guard routes that shouldn’t be even reached without the token.

Secure the frontend

Handle the login form submission

At this moment, the application serves a simple login page with a dummy form. Whatever we insert into the username and password inputs won’t be processed:

<!-- src/app/auth/components/login/login.component.html -->
…
<form #loginForm="ngForm">
    <div class="form-group">
        <label for="username">Username:</label>
        <input id="username" class="form-control" type="text"
        placeholder="Username" name="username" required>
    </div>
…

We need to bind the credentials provided by users to the data model as in any ordinary Angular form.

Add data model

Create the credentials.ts file with the following class:

// src/app/auth/credentials.ts
export class Credentials {
  public constructor(public username: string, public password: string){};
}

In the login.component.ts file import the Credentials class and instantiate the object with empty username and password:

// src/app/auth/components/login/login.component.ts
…
export class LoginComponent implements OnInit {

  credentials: Credentials = new Credentials('', '');
…

Update the form template

Bind both inputs with [(ngModel)] and provide validation messages. Below is the code for the username input:

<!-- src/app/auth/components/login/login.component.html -->
…
<form #loginForm="ngForm">
    <div class="form-group">
        <label for="username">Username:</label>
        <input id="username" class="form-control" type="text"
        placeholder="Username" name="username" required
        [(ngModel)]="credentials.username" #username="ngModel">
        <div [hidden]="username.valid || username.pristine"
        class="text-danger">
        Username is required
        </div>
    </div>
…

Then copy the following code for the password input:

<!-- src/app/auth/components/login/login.component.html -->
…
    <div class="form-group">
        <label for="password">Password:</label>
        <input id="password" class="form-control" type="text"
        placeholder="Password" name="password" required
        [(ngModel)]="credentials.password" #password="ngModel">
        <div [hidden]="password.valid || password.pristine"
        class="text-danger">
        Password is required
        </div>
    </div>
…

Disable the Login button until the form is completed correctly. You also want to call the login() function when the form is submitted, therefore add the following code to the view:

<!-- src/app/auth/components/login/login.component.html -->
…
<button class="btn btn-warning"
    (click)="login()"
    [disabled]="!loginForm.form.valid">
    <span class="ml-1">Log in</span>
</button>
…

We have to add the login() function to the login.component.ts file:

// src/app/auth/components/login/login.component.ts
public login(): void {
}

At this moment the method is empty, we will implement the login functionality later.

As a result, the form submission is turned off until the credentials are provided. In the screenshot below you can see that removing the username value produces the validation message for that field:

The work done in this section is contained in the commit c9be87c3acae01148703bbcd449026eb90bccd79.

Send a login request

We need to call the API to authenticate a user who tries to log in. In case of a successful authentication, the backend sends the Authorization header containing the JWT token that we need to extract, store and append to every subsequent API call.

Create the token service with the following command:

$ ng generate service auth/services/token

We will use it to call the API login endpoint. Copy the following code to the service:

// src/app/auth/services/token.service.ts
…
const httpOptions = {
  headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
  observe: 'response' as 'response'
};
const API_URL = environment.apiUrl;

@Injectable({
  providedIn: 'root'
})
export class TokenService {

  constructor(private http: HttpClient) { }

  public getResponseHeaders(credentials: Credentials) {
    let loginUrl = API_URL + '/login';
    return this.http.post(loginUrl, credentials, httpOptions);
  }
}

The apiUrl value is already defined in the environment.ts and environment.prod.ts files:

// src/environments/environment.ts
export const environment = {
…
  apiUrl: 'http://localhost:8080/api'
};
Adjust the apiUrl value to your development and production configuration.

Obtain the token

We created the service that sends the login requests. In the next step we are going to provide the logic for extracting the right header from the response.

Create the authorization service:

$ ng generate service auth/services/auth

We are going to define the key that will be used for storing the JWT token in the fronted module. What’s more, after users are authenticated, we want to redirect them from the '/login' path. The redirectToUrl variable is set to '/cookies' by default, but will carry any path that user wanted to reach before logging in to the application. Of course, we need to inject the TokenService to call the API and Router to handle redirections.

Copy the following code to the service:

// src/app/auth/services/auth.service.ts
…
@Injectable({
  providedIn: 'root'
})
export class AuthService {

  static readonly TOKEN_STORAGE_KEY = 'token';
  redirectToUrl: string = '/cookies';

  constructor(private router: Router, private tokenService: TokenService) { }
…
}

Furthermore, we are going to add the core responsibility – the login() function. It calls the getResponseHeaders from the TokenService and saves the token from the Authorization header in the LocalStorage. Add the following code to the auth.service.ts file

// src/app/auth/services/auth.service.ts
…
export class AuthService {
…
  public login(credentials: Credentials): void {
    this.tokenService.getResponseHeaders(credentials)
    .subscribe((res: HttpResponse<any>) => {
      this.saveToken(res.headers.get('authorization'));
      this.router.navigate([this.redirectToUrl]);
    });
  }

  private saveToken(token: string){
    localStorage.setItem(AuthService.TOKEN_STORAGE_KEY, token);
  }
}

We also need a method to reach for the currently stored token, so add this last function to the file:

// src/app/auth/services/auth.service.ts
…
export class AuthService {
…
  public getToken(): string {
    return localStorage.getItem(AuthService.TOKEN_STORAGE_KEY);
  }
}

The authorization service is ready to be used in the app.

Implement the login function

Let’s get back to our empty login() function in the login.component.ts file. We are going to inject the AuthService to the constructor so we can use it inside the login() method :

// src/app/auth/components/login/login.component.ts
…
export class LoginComponent implements OnInit {
  …
  constructor(private authService: AuthService) { }

  public login(): void {
    this.authService.login(this.credentials);
  }
}

After those changes, submitting the login form with legitimate credentials will result in receiving a valid token from the API that will be stored in the LocalStorage. A user is authenticated in the app and is automatically redirected to the "/cookies" page. However, the token is not added to this request – instead of the page with tasty treats we are getting the 401 error. We are going to deal with this issue in the next section.

The work done in this section is contained in the commit 71b13383b4b77819234b854e97fec419c60bc450.

Append the token to every API request

We need to intercept every request to the API and add the token from the LocalStorage. Create the jwt.token.interceptor.ts file and copy the following code to it:

// src/app/auth/interceptors/jwt.token.interceptor.ts
…
@Injectable()
export class JwtTokenInterceptor implements HttpInterceptor {

  constructor(public auth: AuthService) {}

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    let interceptedRequest = request.clone({
      setHeaders: {
        Authorization: `Bearer ${this.auth.getToken()}`
      }
    });

    return next.handle(interceptedRequest);
  }
}

We are injecting the AuthService to access the getToken() function. Then, we are appending the Bearer token to any request send to the API.

We have to declare usage of this interceptor in app.module:

// src/app/app.module.ts
…
@NgModule({
  …
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: JwtTokenInterceptor,
      multi: true
    }
  ],
  …
})
export class AppModule { }

Refresh the page and you will see the cookies served by the API.

The work done in this section is contained in the commit e561ff512771a06404cda39e293432429a691c5e.

Add logout functionality

The API security configuration allows us to call the logout endpoint:

// backend/src/main/java/in/keepgrowing/jwtspringbootangularscaffolding/config/SecurityConfig.java
…
@Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        …
                .and()
                .logout()
…

Let’s use it and add the logout() function to the token.service file:

// src/app/auth/services/token.service.ts
…
public logout() {
    let logoutUrl = API_URL + '/logout';
    return this.http.get(logoutUrl, {responseType: 'text'});
  }
…

After a successful API call we can remove the token from the LocalStorage. We will also need a function to check the current state of a user. Copy both methods to your auth.service file:

// src/app/auth/services/auth.service.ts
…
public logout(): void {
    this.tokenService.logout()
    .subscribe(() =>{
      localStorage.removeItem(AuthService.TOKEN_STORAGE_KEY);
    });
}

public isLoggedIn(): boolean {
    return !!this.getToken();
}
…

To display the Login or Logout option in the application header we need to inject the AuthService into the HeaderComponent and implement the logout() function:

// src/app/layout/header/header.component.ts
…
export class HeaderComponent implements OnInit {
…
  constructor(public authService: AuthService) { }

  public logout():void {
    this.authService.logout();
  }
…

We are going to display the Login or Logout option depending of the current state of a user returned by the authService.isLoggedIn() method:

<!-- src/app/layout/header/header.component.html -->
…
  <a routerLink="" class="text-white" [ngClass]="{'hidden': !authService.isLoggedIn() }"
    (click)="logout()">
    <span class="ml-1">Log out</span>
  </a>
  <a routerLink="/login" class="text-white" [ngClass]="{'hidden': authService.isLoggedIn() }"
  routerLinkActive="active">
    <span class="ml-1">Login</span>
  </a>
…

We need to declare the hidden class in the file for the header styles:

/* src/app/layout/header/header.component.scss */
.hidden {
  display: none;
}

The header on your login page should look like the one on the screenshot below:

The work done in this section is contained in the commit bc5a317d7606e8ae56b4df4580f8988e1c3ef1d4.

Implement AuthGuard

Our API will return 401 error to the web console when unauthorized users try to access any secured route. That’s not a user friendly solution. We should redirect those users to the login page instead of sending requests without tokens. We are going to add AuthGuard to enhance the usability of our application.

Generate the guard with the following command:

$ ng generate guard auth/guards/auth

The guard will use AuthService to verify if users are logged in and Router to redirect them to the login form if they are not. Let’s inject them to the constructor:

// src/app/auth/guards/auth.guard.ts
…
export class AuthGuard implements CanActivate {

  constructor(private authService: AuthService, private router: Router) {}
 …
}

We will replace the default code in the canActivate() function with the following lines:

// src/app/auth/guards/auth.guard.ts
…
  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {

    let url: string = state.url;
    return this.checkLogin(url);
  }
…
}

We are declaring the url variable which holds the path that the user tried to access. Depending on the boolean returned by the checkLogin() method the access to that path will be granted or not. Copy the function to the file:

// src/app/auth/guards/auth.guard.ts
…
export class AuthGuard implements CanActivate {
…
  private checkLogin(url: string): boolean {
    if(this.authService.isLoggedIn()) {
      return true;
    }
    this.authService.redirectToUrl = url;
    this.router.navigate(['/login']);
    return false;
  }
}

The last thing that needs to be done is to declare paths that are guarded. Add the canActivate: [AuthGuard]  part to your routing module:

// src/app/app-routing.module.ts
…
const routes: Routes = [
  …
  { canActivate: [AuthGuard], path: 'cookies', component: CookieListComponent }
];
…

The page available for the authenticated and authorized users should look like the one on the screenshot below:

The work done in this section is contained in the commit f9d82ce0c3c4ee26b058c374f999df3f360a0d65.

Handle 401 errors returned from the backend

Right now, when an unauthorised user tries to reach the protected /cookies route, the Angular AuthGuard blocks sending a request to the backend and the user is redirected to the /login page. However, when we have a route that is not guarded explicitly by Angular, the request will be sent to the backend and the user will receive a 401 error.

In our example we don’t deal with expired tokens. Therefore, if the token is no longer valid and the user wants to log out, the unauthorised request will be sent to the backend. Right now the frontend does not handle properly the API response shown in the screenshot below:

We can manage this situation by adding error handling in the interceptor:

// frontend/src/main/angular/src/app/auth/interceptors/jwt.token.interceptor.ts
…
constructor(public auth: AuthService, private router: Router) {}

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    …
    return next.handle(interceptedRequest).pipe(catchError(x => this.handleErrors(x)));
  }

  private handleErrors(err: HttpErrorResponse): Observable<any> {
    if (err.status === 401) {
      this.auth.redirectToUrl = this.router.url;
      this.router.navigate(['/login']);
      return of(err.message);
    }
  }

We created the handleErrors function that redirects users to the /login page when the API returns 401 error and we use it when the intercepted request is returned. To achieve this we had to inject the Router instance in the constructor.

The work done in this section is contained in the commit e26dbc7649a0bb48bbf0f2f66ae32179490d120a.

Photo by Yingchou Han on StockSnap

little_pinecone

View Comments

  • WOW! All 3 of your tutorials are THE BEST tutorials I encountered on the internet! Thanks for all the effort you put into this!

    • Thank you :) Recently I realised that I missed handling 401 errors from the backend so I added it to the tutorial ("Handle 401 errors returned from the backend" section) and to the repository (this commit).

  • It's not clear to me how your logout is working. When I hit /logout I'm getting a 404. I don't see anywhere in your config where you setup the logout URL as api/logout or how you're handling the logout functionality.

  • Hi,

    We are going to explore files in the following order:

    - header.component.html

    - header.component.ts

    - auth.service.ts

    - token.service.ts

    - SecurityConfig.java

    When I click 'Logout' link in my header.component.html, I call the logout() function from the header.component.ts that calls the logout() function from the authService.

    Two things happen there (auth.service.ts):

    1. logout() function from the tokenService is called.

    2. Token is removed from the localStorage.

    So we go to the token.service.ts file. The logout() function sets the logoutUrl variable. It's API_URL + 'logout'. I set API_URL in the environment.ts file to 'http://localhost:8080/api'. Therefore, the whole url is 'http://localhost:8080/api/logout'.

    After setting the url, the authService uses it to send a get request to the backend:

    return this.http.get(logoutUrl, {responseType: 'text'});

    There is a test in the token.service.spec.ts file: 'should call logout endpoint'.

    In SecurityConfig I used the logout() functionality in the configuration (line 46). And Spring Boot deals with logging out the user for me.

    However, as I'm reading the docs for HttpSecurity.java, I see that adding the logout to the configuration seems to be redundant as the

    LogoutConfigurer<HttpSecurity> logout()

    "(…) is automatically applied when using WebSecurityConfigurerAdapter." and my SecurityConfig extends WebSecurityConfigurerAdapter.

    All in all, if info that the logout link was clicked is transferred through header.component.ts, auth.service.ts to token.service.ts (where the backend is finally called) logging out works.

    The process is documented in the 'Add logout functionality section' of this post. You can also find the relevant code in the bc5a317d7606e8ae56b4df4580f8988e1c3ef1d4 commit.

    I hope it helps :)

Share
Published by
little_pinecone

Recent Posts

Simplify the management of user roles in Spring Boot

Spring Security allows us to use role-based control to restrict access to API resources. However,…

3 years ago

Create a custom annotation to configure Spring Boot tests

A custom annotation in Spring Boot tests is an easy and flexible way to provide…

3 years ago

Keycloak with Spring Boot #4 – Simple guide for roles and authorities

Delegating user management to Keycloak allows us to better focus on meeting the business needs…

3 years ago

Keycloak with Spring Boot #3 – How to authorize requests in Swagger UI

Swagger offers various methods to authorize requests to our Keycloak secured API. I'll show you…

3 years ago

Keycloak with Spring Boot #2 – Spring Security instead of Keycloak in tests

Configuring our Spring Boot API to use Keycloak as an authentication and authorization server can…

3 years ago

Keycloak with Spring Boot #1 – Configure Spring Security with Keycloak

Keycloak provides simple integration with Spring applications. As a result, we can easily configure our…

3 years ago