Home | Send Feedback

JWT Authentication with Ionic/Angular and Spring Boot

Published: February 05, 2017  •  Updated: December 07, 2018  •  ionic, spring, java, javascript

JSON Web Token (JWT) is a standard (RFC 7519) for creating access token. A JWT consists of 3 parts: a header, the payload, and a signature. The header describes what algorithm is used to generate the signature. The payload contains information like the issuer of the token, the subject, when the token was issued, and the expiration date. There is no mandatory information an application has to write into the payload. The example application we build in this article puts the issue and expiration date and the username (subject) into the payload.
The last part of a JWT is a digital signature that prevents a malicious party from changing the contents of the token.

See a more detailed description of JWT: https://jwt.io/introduction/

In this article, we build an Ionic app that talks to a Spring Boot back end. The client app consists of a signup page where the user can create new accounts, a login page where the user enters his username and password, and a secret page that the user sees after a successful login. After a successful login, the client app receives a JWT from the server and stores it in localStorage. Next time the user opens the app, the application reads the JWT from localStorage and sends it in the request header to the server. When the token is still valid, the user is immediately logged in.

Login
Sign up
Home

Server

As usual, when I create a new Spring Boot application, I open the website https://start.spring.io, fill in the fields Group and Artifact, and select the required dependencies. For this application, we need the Web and Security dependency.


After you downloaded the generated code to your local computer, open the file pom.xml and add this dependency.

    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.9.1</version>
    </dependency>

pom.xml

jjwt is a Java implementation of the JWT standard. It provides classes and methods to create and verify tokens.


Database

We don't connect this application to a real database. The application stores the users in a hash map in memory. The User class is the entity object that represents a user in our application.

public class User {

  private String name;

  private String username;

  private String email;

  private String password;

User.java

The class UserService manages the Map of the users. The username is unique, and it serves as the key of the Map. The class provides public methods to lookup and save user objects and a check method that returns true if a username already exists.
The class is annotated with the @Service annotation so we can inject it into other Spring-managed beans.

@Service
public class UserService {

  private final Map<String, User> db;

  public UserService(PasswordEncoder passwordEncoder) {
    this.db = new ConcurrentHashMap<>();

    // add demo user
    User user = new User();
    user.setUsername("admin");
    user.setPassword(passwordEncoder.encode("admin"));
    save(user);
  }

  public User lookup(String username) {
    return this.db.get(username);
  }

  public void save(User user) {
    this.db.put(user.getUsername(), user);
  }

  public boolean usernameExists(String username) {
    return this.db.containsKey(username);
  }
}

UserService.java


Security

In this section, we are configuring Spring Security that is responsible for securing the back end.

To encrypt passwords, we configure an Argon2PasswordEncoder bean.

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new Argon2PasswordEncoder(16, 32, 8, 1 << 16, 4);
  }

Application.java

We disable the authentication part of Spring Security. Our login system is straightforward, and we will implement it without the help of Spring Security. We are still going to use Spring Security for authorization and securing our backend services. To disable the authentication system, we have to prevent the Spring Boot auto configurer from running by implementing a custom AuthenticationManager bean that does nothing

  @Bean
  @Override
  protected AuthenticationManager authenticationManager() throws Exception {
    return authentication -> {
      throw new AuthenticationServiceException("Cannot authenticate " + authentication);
    };
  }

SecurityConfig.java

Next, we look at the TokenProvider class. This class provides a method to create a JWT (createToken) and a method to parse and validate a JWT and to return an Authentication object (getAuthentication).

  public String createToken(String username) {
    Date now = new Date();
    Date validity = new Date(now.getTime() + this.tokenValidityInMilliseconds);

    return Jwts.builder().setId(UUID.randomUUID().toString()).setSubject(username)
        .setIssuedAt(now).signWith(SignatureAlgorithm.HS512, this.secretKey)
        .setExpiration(validity).compact();
  }

TokenProvider.java

The JWT is created with the Jwts builder from the jjwt library. We set a random id, the subject (username in our application), the issue date and the expiration date. The builder also needs to know the signature algorithm and a secret key. The application reads this key from the application.properties file. The secret key is a per-application secret and should be different for each application. The method compact() builds and returns the JWT as a String.

  public Authentication getAuthentication(String token) {
    String username = Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(token)
        .getBody().getSubject();

    User user = this.userService.lookup(username);
    if (user == null) {
      throw new UsernameNotFoundException("User '" + username + "' not found");
    }

    UserDetails userDetails = org.springframework.security.core.userdetails.User
        .withUsername(username).password("").authorities(Set.of()).accountExpired(false)
        .accountLocked(false).credentialsExpired(false).disabled(false).build();

    return new UsernamePasswordAuthenticationToken(userDetails, null,
        userDetails.getAuthorities());
  }

TokenProvider.java

This method receives a JWT and parses it. The code needs to use the same secret key as in the generate method in order to check the validity of the token. It then extracts the subject, which is the username in our application, and reads the user from the "database". Instead of doing a database query, you could store all information (for example, role information) into the JWT and extract them here. Doing a database query here has the advantage that we can immediately block a user or change his roles, but it also poses a bottleneck if you have thousands of users.

With no autoconfigured login system in place, we now implement the login endpoint

  @PostMapping("/login")
  public ResponseEntity<String> authorize(@Valid @RequestBody User loginUser) {

    User user = this.userService.lookup(loginUser.getUsername());
    if (user != null) {
      boolean pwMatches = this.passwordEncoder.matches(loginUser.getPassword(),
          user.getPassword());
      if (pwMatches) {
        String token = this.tokenProvider.createToken(loginUser.getUsername());
        return ResponseEntity.ok(token);
      }
    }
    else {
      this.passwordEncoder.matches(loginUser.getPassword(),
          this.userNotFoundEncodedPassword);
    }

    return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
  }

AuthController.java

This is a plain HTTP POST endpoint that receives the username and password as request parameters. The method fetches the user from the database and compares the passwords. If the login information is valid, it creates the JWT and sends it back in the body of the response. Otherwise, it returns an HTTP status code 401.
Note that the password hash algorithm should be configured in a way that the check runs for about 1 second. When the endpoint receives a request for a user that is not stored in the database we need to make sure that the runtime of the method stays the same by doing an artificial password check.

Next, we implement an empty secured HTTP GET endpoint.

  @GetMapping("/authenticate")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  public void authenticate() {
    // we don't have to do anything here
    // this is just a secure endpoint and the JWTFilter
    // validates the token
    // this service is called at startup of the app to check
    // if the jwt token is still valid
  }

AuthController.java

The client application at startup sends a request to this endpoint to decide if it should present the login page or route directly to the secret page. A request to this endpoint with a valid JWT returns an HTTP response code of 200, otherwise 403.

The next part of our security configuration is the JWTFilter class. This class is injected into the Spring Security filter chain, and every HTTP request that needs to be authenticated flows through this filter. The filter is responsible for extracting the JWT, creating the Authentication object and putting it into the security context. The Spring Security authorization system needs this information to check if somebody has access to a resource or not.

  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
      FilterChain filterChain) throws IOException, ServletException {
    try {
      HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
      String jwt = resolveToken(httpServletRequest);
      if (jwt != null) {
        Authentication authentication = this.tokenProvider.getAuthentication(jwt);
        if (authentication != null) {
          SecurityContextHolder.getContext().setAuthentication(authentication);
        }
      }
      filterChain.doFilter(servletRequest, servletResponse);
    }
    catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException
        | SignatureException | UsernameNotFoundException e) {
      Application.logger.info("Security exception {}", e.getMessage());
      ((HttpServletResponse) servletResponse)
          .setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    }
  }

JWTFilter.java

The client sends the JWT in the Authorization HTTP request header

Authorization:Bearer eyJhbGciOiJIUzUxMiJ9......

The first thing the filter does is extracting the JWT from this header (resolveToken). Then it calls the getAuthentication method from the TokenProvider. This method returns an Authentication object when the JWT is valid and puts the object into the security context. In case of an invalid JWT, it does not put an Authentication object into the context, and Spring Security will prevent the request from accessing secure endpoints.

Finally, we need to configure Spring Security and put all the pieces together. We need to tell Spring Security what endpoints are secured and which ones are public. We do that with a configuration class that extends WebSecurityConfigurerAdapter subclass.

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.csrf(cust -> cust.disable())
        .cors(Customizer.withDefaults())
        .sessionManagement(
            customizer -> customizer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        // optional, if you want to access the
        // services from a browser
        // .httpBasic(Customizer.withDefaults())
        .authorizeRequests(customizer -> {
          customizer.antMatchers("/signup", "/login", "/public").permitAll();
          customizer.anyRequest().authenticated();
        })
        .addFilterAfter(new JWTFilter(this.tokenProvider),
            SecurityContextPersistenceFilter.class);
  }

SecurityConfig.java

The configuration disables CSRF protection and enables CORS handling (server and client apps are served from different ports). Then it sets the session creation policy to STATELESS that prevents Spring Security from creating HttpSession objects and cookies. Each request already contains the Authorization header with the JWT, and we don't need an additional session cookie. The HTTP endpoints /signup, /login, and /public are accessible without authentication. All the other endpoints are secured and require a valid JWT token. Lastly, the JWTFilter is put into the security filter chain right after the SecurityContextPersistenceFilter, which is one of the first filters that gets called.


Endpoints

The configuration for the security is now finalized, and we can move on to the implementation of the other HTTP endpoints. We start with the method that handles the signup request.

  @PostMapping("/signup")
  public String signup(@RequestBody User signupUser) {
    if (this.userService.usernameExists(signupUser.getUsername())) {
      return "EXISTS";
    }

    signupUser.encodePassword(this.passwordEncoder);
    this.userService.save(signupUser);
    return this.tokenProvider.createToken(signupUser.getUsername());
  }

AuthController.java

This method checks if the username already exists and returns the string "EXISTS" in that case. The client then presents an error message to the user when it receives this response.

If the username does not already exist, the password is encoded with the password encoder before it's stored in the "database". Then the method creates the JWT and returns it, and the user is logged in.


Finally, we create a test endpoint that is only accessible with a valid JWT.

  @GetMapping("/secret")
  public String secretService(@AuthenticationPrincipal UserDetails details) {
    System.out.println(details.getUsername());
    return "A secret message";
  }

TestController.java

The client shows the response from this method on the secret page. This concludes the server part of our application.

Client

The client is built with the Ionic framework and based on the blank starter template.

ionic start jwt blank

I installed the @auth0/angular-jwt library that simplifies the JWT handling and ng2-validation for additional form validation functions.

npm install @auth0/angular-jwt
npm install ng2-validation

The application needs to add the JWT to every HTTP request it sends to the server. @auth0/angular-jwt handles this for us and automatically attaches the JWT as an Authorization HTTP request header. The library does this with the help of an HttpInterceptor.

@auth0/angular-jwt also provides a few helper methods for checking the expiration date of a token and for decoding the token's payload.


Pages

The start command already created a page called HomePage. The app presents this page to the user if he has a valid JWT. We also need to create two additional pages, a service, and a route guard.

ng generate page Signup
ng generate page Login
ng generate service Auth
ng generate guard Auth

App Configuration

Next, we need to edit app.module.ts. We have to add our components and configure the @auth0/angular-jwt library.

import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {RouteReuseStrategy, RouterModule, Routes} from '@angular/router';
import {IonicModule, IonicRouteStrategy} from '@ionic/angular';
import {AppComponent} from './app.component';
import {HomePage} from './home/home.page';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {HttpClientModule} from '@angular/common/http';
import {LoginPage} from './login/login.page';
import {SignupPage} from './signup/signup.page';
import {JwtModule} from '@auth0/angular-jwt';
import {environment} from '../environments/environment';
import {AuthGuard} from './auth.guard';

const routes: Routes = [
  {path: '', redirectTo: 'home', pathMatch: 'full'},
  {path: 'home', component: HomePage, canActivate: [AuthGuard]},
  {path: 'login', component: LoginPage},
  {path: 'signup', component: SignupPage},
  {path: '**', redirectTo: '/home'}
];

export function tokenGetter(): string | null {
  return localStorage.getItem('jwt_token');
}

@NgModule({
  declarations: [AppComponent, HomePage, LoginPage, SignupPage],
  imports: [BrowserModule,
    CommonModule,
    HttpClientModule,
    JwtModule.forRoot({
      config: {
        tokenGetter,
        allowedDomains: environment.allowedDomains
      }
    }),
    FormsModule,
    IonicModule.forRoot(),
    RouterModule.forRoot(routes, { useHash: true, relativeLinkResolution: 'legacy' })],
  providers: [
    {provide: RouteReuseStrategy, useClass: IonicRouteStrategy}
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}

app.module.ts

To import the angular-jwt library, we add JwtModule to the imports section. By default, the @auth0/angular-jwt library reads the token from localStorage with the key id_token. In this example, we use a custom token getter method that reads the token from localStorage with the name jwt_token.

Because @auth0/angular-jwt attaches an HttpInterceptor to Angular's HTTP client service, every request initiated from our application goes through that interceptor. By default, the library does not add the Authorization header to any request. You have to whitelist URLs where the library is allowed to add the Authorization header with the whitelistedDomains property.

In this application, only requests sent to localhost:8080 should have an Authorization HTTP request header.


AuthService

The AuthService class manages the authentication of our app. It validates the JWT, sends login and signup requests to the server, and handles the responses.

Our app utilizes the ReplaySubject provided by the RxJs library to notify other parts of the app when the authorization state changes. This object represents an observable sequence as well as an observer. Every time the app calls next on this object, all subscribers are notified.
We only expose the observable part of the subject to others, so they can only subscribe but not call next.

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

  private readonly jwtTokenName = 'jwt_token';

  private authUser = new ReplaySubject<string | null>(1);
  public authUserObservable = this.authUser.asObservable();

auth.service.ts

Every time the application starts up, it calls the hasAccess() method.

  hasAccess(): Promise<boolean> {
    const jwt = localStorage.getItem(this.jwtTokenName);

    if (jwt && !this.jwtHelper.isTokenExpired(jwt)) {

      return new Promise((resolve, _) => {

        this.httpClient.get(`${environment.serverURL}/authenticate`)
          .subscribe(() => {
              this.authUser.next(jwt);
              resolve(true);
            },
            err => {
              this.logout();
              resolve(false);
            });
      });

      // OR
      // this.authUser.next(jwt);
      // Promise.resolve(true);
    } else {
      this.logout();
      return Promise.resolve(false);
    }
  }

auth.service.ts

The method fetches the JWT from localStorage, checks the validity, and then sends a request to the secure endpoint /authenticate. If that call succeeds, it calls authUser.next with the JWT as a parameter and returns true.

If the /authenticate call fails, or there is no JWT locally stored, or the token is expired, the app deletes the token, calls authUser.next(null) and navigates to the login page

  logout(): void {
    localStorage.removeItem(this.jwtTokenName);
    this.authUser.next(null);
    this.navCtrl.navigateRoot('login', {replaceUrl: true});
  }

auth.service.ts

The logout method is also called when the user clicks on the logout icon.

The call to /authenticate is debatable, as the query to fetch the user from the database. It depends on the use case. If the client and the server only depend on the information stored in the JWT without ever check the server and the user database, the application would not be able to immediately block a user or change the roles of a user. On the other hand, we are sending an additional request to the server, which could be a bottleneck if you have thousands of users.

The next method we implement in the AuthService class is login. This method is called from the login page after the user enters his username and password and taps the login button. The method posts the data to the /login endpoint and receives back a JWT when the login information is valid. The code stores the token in localStorage and then calls authUser.next.

  login(values: { username: string, password: string }): Observable<string> {
    return this.httpClient.post(`${environment.serverURL}/login`, values, {responseType: 'text'})
      .pipe(tap(jwt => this.handleJwtResponse(jwt)));
  }

auth.service.ts

  private handleJwtResponse(jwt: string): string {
    localStorage.setItem(this.jwtTokenName, jwt);
    this.authUser.next(jwt);

    return jwt;
  }

auth.service.ts

And finally, we implement the signup method. The function posts the signup information to the /signup endpoint and receives back either a JWT or the string EXISTS when the username already exists. When the response is a JWT, it calls handleJwtResponse, which stores the token.

  signup(values: { name: string, email: string, username: string, password: string }): Observable<string> {
    return this.httpClient.post(`${environment.serverURL}/signup`, values, {responseType: 'text'})
      .pipe(tap(jwt => {
        if (jwt !== 'EXISTS') {
          return this.handleJwtResponse(jwt);
        }
        return jwt;
      }));
  }

auth.service.ts


AuthGuard

In the Angular route configuration, you see that the application protects the route to the home page with a canActivate guard

const routes: Routes = [
  {path: '', redirectTo: 'home', pathMatch: 'full'},
  {path: 'home', component: HomePage, canActivate: [AuthGuard]},
  {path: 'login', component: LoginPage},
  {path: 'signup', component: SignupPage},
  {path: '**', redirectTo: '/home'}
];

app.module.ts

This guard calls the hasAccess method from the AuthService class

import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot, UrlTree} from '@angular/router';
import {Observable} from 'rxjs';
import {AuthService} from './auth.service';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {

  constructor(private readonly authService: AuthService) {
  }

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return this.authService.hasAccess();
  }

}

auth.guard.ts

This method either returns true or false. False blocks the route request and displays the login page. True allows the request to go through and shows the home page to the user.


Login Page

The login page is a simple form implemented with the template-driven approach and presents the user a username and password field and two buttons one to log in and the other to switch to the signup page.

When the user taps on the login button, the app opens a loading dialog and calls the method login from AuthService. If an error occurs, a toast message is presented to tell the user that something went wrong. When the login succeeds, the user sees the home page.

Template: login.page.html
TypeScript class: login.page.ts


Sign up Page

The signup page display the input fields name, email, username and password. A tap on the signup button calls the authService.signup function.

To handle the special "EXISTS" case, the app injects the NgModel of the username field into the SignupPage class. And later, when the server returns the string "EXISTS", the app can access the FormControl and set an error.

Exists error

Template: signup.page.html
TypeScript class: signup.page.ts


Home Page

The HomePage is the page the users see when login or sign up was successful or when the app finds a valid JWT in the storage. The home page displays the username of the logged-in user and the response of the /secret HTTP endpoint.

The HomePage subscribes to the authService.authUserObservable in the constructor, and when it receives a JWT it decodes it with the JwtHelper.decodeToken function extracts the username from the subject field (sub), and assigns it to the instance variable user.

    this.authService.authUserObservable.subscribe(jwt => {
      if (jwt) {
        const decoded = jwtHelper.decodeToken(jwt);
        this.user = decoded.sub;
      } else {
        this.user = null;
      }
    });

home.page.ts

The ngOnInit lifecycle method calls the /secret endpoint and assigns the response to the message instance variable.

  ngOnInit(): void {
    this.httpClient.get(`${environment.serverURL}/secret`, {responseType: 'text'}).subscribe(
      text => this.message = text,
      err => console.log(err)
    );
  }

home.page.ts

The HomePage also displays a logout icon in the top right corner. Tap on this icon calls the authService.logout() method.


This concludes our JWT implementation with Ionic/Angular and Spring Boot.

You find the complete source code for this project on GitHub.