JWT Authentication with Ionic 3 and Spring Boot

Published: February 05, 2017  •  ionic3, 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 are no mandatory information an application has to write into the payload. The example application we build in this article will put the issue and expiration date and the username (subject) into the payload.
The last part of an 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. After a successful signup and login the client receives a JWT from the server and stores it locally on the client. Next time the user opens the app he will 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 additional dependency.

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

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;
  // get and set methods
}

src/main/java/ch/rasc/jwt/db/User.java

The class UserService manages the map of the users. Because the username must be 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);
  }
}

src/main/java/ch/rasc/jwt/db/UserService.java


Security

In this section we will configure Spring Security that is responsible for securing the back end. I took inspiration for the configuration from the JHipster project and copied the three classes JWTConfigurer, JWTFilter and TokenProvider from this project.

To encrypt the password we configure a PasswordEncoder bean.

@Bean
public PasswordEncoder passwordEncoder() {
  return new BCryptPasswordEncoder(12);
}

src/main/java/ch/rasc/jwt/Application.java

Then we need to create an implementation of the UserDetailsService interface. The interface dictates that the method loadUserByUsername must be implemented.

  @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();
  }

src/main/java/ch/rasc/jwt/security/AppUserDetailService.java

The code 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();
  }

src/main/java/ch/rasc/jwt/security/jwt/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 is 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());
  }

src/main/java/ch/rasc/jwt/security/jwt/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 the parse succeeds the username is extracted and the code loads the UserDetails from the "database". It's debatable if the application should do an database query here, because we could 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 will be injected into the Spring Security filter chain and every http request, that needs to be authenticated, will flow 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);
  }
}

src/main/java/ch/rasc/jwt/security/jwt/JWTFilter.java

The client sends the token in the Authorization header

Authorization:Bearer eyJhbGciOiJIUzUxMiJ9......

and the first thing the code 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 exists in the database or it throws one of several exceptions when the validation of the JWT failed. In that case the filter returns a http status code of 401.
When getAuthentication returns an Authentication object the filter puts it into a thread local variable

SecurityContextHolder.getContext().setAuthentication(authentication)

so other parts of the application have access to the authentication object.

Next class is the JWTConfigurer. This is a helper class that adds the JWTFilter into the chain of security filters. It inserts the JWTFilter before the UsernamePasswordAuthenticationFilter filter, to check if the request contains a valid JWT before presenting a login dialog.

public class JWTConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
  private final TokenProvider tokenProvider;
  public JWTConfigurer(TokenProvider tokenProvider) {
    this.tokenProvider = tokenProvider;
  }

  @Override
  public void configure(HttpSecurity http) throws Exception {
    JWTFilter customFilter = new JWTFilter(this.tokenProvider);
    http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
  }
}

src/main/java/ch/rasc/jwt/security/jwt/JWTConfigurer.java

Finally we need to configure what endpoints are secured and which ones are public. We do that with a 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 {
    // @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").permitAll()
        .antMatchers("/login").permitAll()
        .antMatchers("/public").permitAll()
        .anyRequest().authenticated()
        .and()
      .apply(new JWTConfigurer(this.tokenProvider));
    // @formatter:on
  }
}

src/main/java/ch/rasc/jwt/security/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. The http endpoints /signup, /login and /public are accessible without an authentication. All the other endpoints will be secured and require a valid JWT token. At the end the JWTConfigurer helper class injects the JWTFilter into the Spring Security filter chain.


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());
}

src/main/java/ch/rasc/jwt/AuthController.java

The method checks if the username already exists and returns the string "EXISTS" if it does. The client will present an error message when it receives this response.

If the username does not already exist the password is encoded with bcrypt before it's stored in the "database". Then it 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 tokenProvider.createToken(loginUser.getUsername());
  }
  catch (AuthenticationException e) {
    Application.logger.info("Security exception {}", e.getMessage());
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    return null;
  }
}

src/main/java/ch/rasc/jwt/AuthController.java

The 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.

And finally we create an endpoint that is only accessible with a valid JWT.

@RestController
public class TestController {


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

src/main/java/ch/rasc/jwt/TestController.java

The client will show the response from this method on the HomePage. This concludes the server part of our application.


Client

The client is built with the Ionic framework and we start as usual with the ionic start command.

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 that reads the token from the 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 doing a 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 will present this page to the user if he has a valid JWT. We also need to create two additional pages and an Auth provider that handles the login and signup requests.

ionic g page Signup
ionic g page Login
ionic g provider Auth

App Configuration

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

export function jwtOptionsFactory(storage: Storage) {
  return {
    tokenGetter: () => storage.get('jwt_token'),
    whitelistedDomains: ['localhost:8080']
  }
}

@NgModule({
  declarations: [
    MyApp,
    HomePage,
    LoginPage,
    SignupPage
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    JwtModule.forRoot({
      jwtOptionsProvider: {
        provide: JWT_OPTIONS,
        useFactory: jwtOptionsFactory,
        deps: [Storage]
      }
    }),
    IonicModule.forRoot(MyApp),
    IonicStorageModule.forRoot(),
    CustomFormsModule
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage,
    LoginPage,
    SignupPage
  ],
  providers: [
    StatusBar,
    SplashScreen,
    {provide: ErrorHandler, useClass: IonicErrorHandler},
    AuthProvider
  ]
})
export class AppModule {
}

src/app/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 the localStorage with the key id_token. Because we want to use Ionic's Storage service in this example, we have to tell the library where to look for the token. We do that by creating a jwtOptionsFactory method and configure the tokenGetter property. Then we configure the JwtModule with a config object and specify the method as the factory method for the module:

JwtModule.forRoot({
      jwtOptionsProvider: {
        provide: JWT_OPTIONS,
        useFactory: jwtOptionsFactory,
        deps: [Storage]
      }
    })

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.

export function jwtOptionsFactory(storage: Storage) {
  return {
    tokenGetter: () => storage.get('jwt_token'),
    whitelistedDomains: ['localhost:8080']
  }
}

In our application, only request sent to localhost:8080 will contain the Authorization header.

CustomFormsModule imports the ng2-validation library into our app.

And we have to add the SignupPage and LoginPage to the declarations and entryComponents sections.


AuthProvider

The src/providers/auth/auth.ts file implements the AuthProvider class that 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 will be notified. We use that mechanism for navigation from the LoginPage to the secure HomePage and back.

authUser = new ReplaySubject<any>(1);

If you look in the src/app/app.components.ts file you see how other parts of the code can subscribe to this object. When the subscribe function receives a JWT is will change the rootPage to HomePage and if the input is null it will present the LoginPage to the user.

    this.authProvider.authUser.subscribe(jwt => {
      if (jwt) {
        this.rootPage = HomePage;
      }
      else {
        this.rootPage = LoginPage;
      }
    });

    this.authProvider.checkLogin();

src/app/app.component.ts

Every time the application starts up it calls the checkLogin() function of the authProvider. This function checks if a JWT is stored locally.

  checkLogin() {
    this.storage.get(this.jwtTokenName).then(jwt => {
      if (jwt && !this.jwtHelper.isTokenExpired(jwt)) {
        this.httpClient.get(`${SERVER_URL}/authenticate`)
          .subscribe(() => this.authUser.next(jwt),
            (err) => this.storage.remove(this.jwtTokenName).then(() => this.authUser.next(null)));
        // OR
        // this.authUser.next(jwt);
      }
      else {
        this.storage.remove(this.jwtTokenName).then(() => this.authUser.next(null));
      }
    });
  }

src/providers/auth/auth.ts

The function fetches the JWT from the storage, if it exists it checks the validity and then it calls a secure endpoint /authenticate. If that call succeeds it calls authUser.next with the JWT as parameter which triggers the navigation to the HomePage in app.components.ts.

If the /authenticate call fails or there is no JWT locally stored or the token is expired, the app deletes the token from the storage, calls authUser.next(null) which then triggers a navigation to the LoginPage.

This call to /authenticate is again debatable like 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 have a way 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 function we implement in the AuthProvider class is login. This function is called from the LoginPage after the user enters his username and password and taps the login button. The function posts the data to the /login endpoint and receives back a JWT when the login information is correct. The code stores the token locally with the storage.set function and then calls authUser.next which triggers the HomePage to load.

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

  private handleJwtResponse(jwt: string) {
    return this.storage.set(this.jwtTokenName, jwt)
      .then(() => this.authUser.next(jwt))
      .then(() => jwt);
  }

src/providers/auth/auth.ts

The logout function, called when the user taps on the logout icon from the HomePage, deletes the token in the Storage and calls authUser.next(null) to trigger a navigation to the LoginPage.

  logout() {
    this.storage.remove(this.jwtTokenName).then(() => this.authUser.next(null));
  }

src/providers/auth/auth.ts

And finally we implement the signup function which is called from the SignupPage. 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 will call handleJwtResponse which stores the token and navigates to the HomePage.

  signup(values: any): Observable<any> {
    return this.httpClient.post(`${SERVER_URL}/signup`, values, {responseType: 'text'})
      .pipe(tap(jwt => {
        if (jwt !== 'EXISTS') {
          return this.handleJwtResponse(jwt);
        }
        return jwt;
      }));
  }

src/providers/auth/auth.ts


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 function login from the authProvider. If an error occurs a toast message is presented to tell the user that something went wrong. When the login succeeds the user will see the HomePage.

Template: src/pages/login/login.html
TypeScript class: src/pages/login/login.ts


Sign up Page

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

To handle the special "EXISTS" case the app injects the NgModel of the username field into the SignupPage class.

  @ViewChild('username')
  usernameModel: NgModel;

And later when the server returns the string "EXISTS" the app can access the FormControl and set an error

 this.usernameModel.control.setErrors({'usernameTaken':true});

Exists error

Template: src/pages/signup/signup.html
TypeScript class: src/pages/signup/signup.ts


Home Page

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

<ion-content padding>
  Welcome <strong>{{user}}</strong>

  <p>
    Message from the secret service:
    <strong>{{message}}</strong>
  </p>
</ion-content>

src/pages/home/home.html

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

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

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

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

src/pages/home/home.ts

The HomePage also displays a logout icon in the top right corner. A tap on this icon will call the authProvider.logout() function.

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