Home | Send Feedback

JWT Authentication with Ionic 4 and Spring Boot

Published: February 05, 2017  •  Updated: December 07, 2018  •  ionic4, 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 or 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 the signature that is calculated over the whole message and prevents a malicious party to change 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 a username and a password and a secret page that the user sees after a successful authentication request. The client receives a JWT from the server and stores it in localStorage. Next time the user opens the app, he is going to be automatically logged in if the JWT is still valid.

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.

Spring Initializr


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. It just 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. Because the username is unique, 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() {
    this.db = new ConcurrentHashMap<>();
  }

  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. I took inspiration for the configuration from the JHipster project and copied the two classes JWTFilter and TokenProvider from this project.

To encrypt the password, we configure a PasswordEncoder bean.

  @Bean
  public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
  }

Application.java

Then we need to create an implementation of the UserDetailsService interface. Because of the interface, we have to implement the method loadUserByUsername.

@Component
public class AppUserDetailService implements UserDetailsService {

  private final UserService userService;

  public AppUserDetailService(UserService userService) {
    this.userService = userService;
  }

  @Override
  public final UserDetails loadUserByUsername(String username)
      throws UsernameNotFoundException {
    final User user = this.userService.lookup(username);
    if (user == null) {
      throw new UsernameNotFoundException("User '" + username + "' not found");
    }

    return org.springframework.security.core.userdetails.User.withUsername(username)
        .password(user.getPassword()).authorities(Collections.emptyList())
        .accountExpired(false).accountLocked(false).credentialsExpired(false)
        .disabled(false).build();
  }

}

AppUserDetailService.java

This implementation fetches the User object with the UserService from the "database" and instantiates an UserDetails. The code utilizes the builder from the User class to create the UserDetails instance.

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. For the signature, the builder needs to know the algorithm (HMAC with SHA-512), and it needs a secret key. The application reads this key from the application.yml file (the key has to be a base64 encoded string). The method setExpiration sets the date until when the token is valid. The current date and time are set with the setIssuedAt method, and the username is put into the subject with setSubject. The final call to 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();
    UserDetails userDetails = this.userService.loadUserByUsername(username);

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

TokenProvider.java

This method receives the JWT and parses it. The code needs to set the same secret key as in the token creation method to check the validity of the signature. After parse succeeds the username is extracted, and the code loads the UserDetails from the "database". It's debatable if the application should do a database query here because we can store all the authorities (roles) of a user in the JWT and then extract it here. Depending on the use case and how long the JWT is valid, checking the user in the database could make sense. The advantage is that we can block users and change their roles immediately.

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.

  @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 token in the Authorization 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 either an Authentication object or null when the token is valid, but the user does not exist, or it throws one of several exceptions when the validation of the JWT failed. In all error cases, the filter returns an HTTP status code of 401.

Finally, we need to configure what endpoints are secured and which ones are public. We do that with a configuration class that extends WebSecurityConfigurerAdapter subclass.

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  private final TokenProvider tokenProvider;

  public SecurityConfig(TokenProvider tokenProvider) {
    this.tokenProvider = tokenProvider;
  }

  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    JWTFilter jwtFilter = new JWTFilter(this.tokenProvider);

    // @formatter:off
    http
      .csrf().disable()
      .cors()
        .and()
      .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
    //.httpBasic() // optional, if you want to access
    //  .and()     // the services from a browser
      .authorizeRequests()
        .antMatchers("/signup", "/login", "/public").permitAll()
        .anyRequest().authenticated()
        .and()
      .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
    // @formatter:on
  }

}

SecurityConfig.java

The configuration disables CSRF protection and enables CORS handling. 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 additional session cookies. 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 added to the security filter chain before the UsernamePasswordAuthenticationFilter.


Endpoints

The configuration for the security is now finalized, and we can move on to the implementation of the 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 when it receives this response.

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


Next method is the handler for the /login request. This for the case when the user already has an account in our application, but the JWT has expired, or the user logs in from a new device where the JWT is not stored locally.

  @PostMapping("/login")
  public String authorize(@Valid @RequestBody User loginUser,
      HttpServletResponse response) {
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
        loginUser.getUsername(), loginUser.getPassword());

    try {
      this.authenticationManager.authenticate(authenticationToken);
      return this.tokenProvider.createToken(loginUser.getUsername());
    }
    catch (AuthenticationException e) {
      Application.logger.info("Security exception {}", e.getMessage());
      response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
      return null;
    }
  }

AuthController.java

This method creates an instance of UsernamePasswordAuthenticationToken and calls authenticate of the authenticationManager. When username and/or password are not valid, this call throws an exception, and the method returns the status code 401. When the authentication is successful, the method creates a JWT and returns it to the client.


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

package ch.rasc.jwt;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@CrossOrigin
public class TestController {

  @GetMapping("/public")
  public String publicService() {
    return "This message is public";
  }

  @GetMapping("/secret")
  public String secretService() {
    return "A secret message";
  }

}

TestController.java

The client shows the response from this method on the home 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

Next we install 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

JSON Web Tokens are usually sent in the HTTP header, and we could write code ourselves that reads the token from storage and puts it into the HTTP header, but @auth0/angular-jwt handles this for us and automatically attaches the JWT as an Authorization header every time the app is making an HTTP request. The library does that with a HttpInterceptor that it attaches to Angular's HTTP client service.

@auth0/angular-jwt also introduces 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.

export function tokenGetter() {
  return localStorage.getItem('jwt_token');
}

@NgModule({
  declarations: [AppComponent, HomePage, LoginPage, SignupPage],
  entryComponents: [],
  imports: [BrowserModule,
    CommonModule,
    HttpClientModule,
    JwtModule.forRoot({
      config: {
        tokenGetter: tokenGetter,
        whitelistedDomains: environment.whitelistedDomains
      }
    }),
    FormsModule,
    IonicModule.forRoot(),
    RouterModule.forRoot(routes, {useHash: true})],
  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. You have to export this getter function; otherwise, Angular's ngc compiler throws an error.

Because @auth0/angular-jwt attaches a 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 contain the Authorization header.


AuthService

The AuthService class manages the authentication of our app. It checks 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<any>(1);
  public authUserObservable = this.authUser.asObservable();

auth.service.ts

Every time the application starts up, it calls the hasAccess() method. This function checks if a JWT is stored locally.

  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 function fetches the JWT from localStorage, if it exists it checks the validity and then it calls the secure endpoint /authenticate. If that call succeeds, it calls authUser.next with the JWT as parameter and returns true. This method is called from the AuthGuard, which we discuss in the next section, and guards expect a true or false return value.

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() {
    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 again 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. This is not a problem when the JWT has a very short validity, but in this example, the token is valid for 30 days.

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 was correct. The code stores the token in localStorage and then calls authUser.next.

  login(values: any): 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 which is called from the signup page. The function posts the sign-up 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: any): 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 route configuration, you see that the application protects the route to the home page with a 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 and in this case 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 login 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 the login and 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() {
    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. A tap on this icon calls the authService.logout() method.


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