In this post, we will learn how JWT(JSON Web Token) based authentication works, and how to build a Spring Boot application in Java to implement it using the Spring Security library library.

banner

If you already know how JWT works, and just want to see the implementation, you can skip ahead, or see the source code on Github

The JSON web token (JWT) allows you to authenticate your users in a stateless manner, without actually storing any information about them on the system itself (as opposed to session based authentication).

The JWT Format

Consider a user called user1, trying to login to an application or website: Once they’re successful they would receive a token that looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIxIiwiZXhwIjoxNTQ3OTc0MDgyfQ.2Ye5_w1z3zpD4dSGdRp3s98ZipCNQqmsHRB9vioOx54

This is a JWT, which is made up of three parts (separated by .):

  1. The first part is the header (eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9). The header specifies information like the algorithm used to generate the signature (the third part). This part is pretty standard and is the same for any JWT using the same algorithm.
  2. The second part is the payload (eyJ1c2VybmFtZSI6InVzZXIxIiwiZXhwIjoxNTQ3OTc0MDgyfQ), which contains application specific information (in our case, this is the username), along with information about the expiry and validity of the token.
  3. The third part is the signature (2Ye5_w1z3zpD4dSGdRp3s98ZipCNQqmsHRB9vioOx54). It is generated by combining and hashing the first two parts along with a secret key.

Note that the header and payload are not encrypted – They are just base64 encoded. This means that anyone can decode them using a base64 decoder

For example, if we decode the header to plain text, we will see the below content:

{ "alg": "HS256", "typ": "JWT" }

 

If you are using linux or Mac OS, you can also execute the following statement on the terminal:

echo eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 | base64 -d

Similarly, the contents of the payload are:

{ "username": "user1", "exp": 1547974082 }

How the JWT Signature Works

So if the header and signature of a JWT can be accessed by anyone, what actually makes a JWT secure? The answer lies in how the third portion (the signature) is generated.

Consider an application that wants to issue a JWT to a user (for example, user1) that has successfully signed in.

Making the header and payload are pretty straightforward: The header is fixed for our use case, and the payload JSON object is formed by setting the user ID and the expiry time in unix milliseconds.

The application issuing the token will also have a key, which is a secret value, and known only to the application itself.

The base64 representations of the header and payload are then combined with the secret key and then passed through a hashing algorithm (in this case its HS256, as mentioned in the header)

jwt algorithm

The details of how the algorithm is implemented is out of scope for this post, but the important thing to note is that it is one way, which means that we cannot reverse the algorithm and obtain the components that went into making the signature - so our secret key remains secret.

Verifying a JWT

To verify a JWT, the server generates the signature once again using the header and payload from the incoming JWT, and its secret key. If the newly generated signature matches the one on the JWT, then the JWT is considered valid.

Now, if you are someone trying to issue a fake token, you can easily generate the header and payload, but without knowing the key, there is no way to generate a valid signature. If you try to tamper with the existing payload of a valid JWT, the signatures will no longer match.

jwt verification

In this way, the JWT acts as a way to authorize users in a secure manner, without actually storing any information (besides the key) on the issuing server.

Implementation in Spring Boot

Now that we know how JWT based authentication works, let’s implement it in a new Spring Boot server.

Creating the HTTP Server

Let’s start by initializing the HTTP server with Spring boot:

// Declare the main application class. We will define our HTTP routes here
@SpringBootApplication
@RestController
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

	// We will implement this later
	@RequestMapping("/welcome")
	public String simpleRequest(HttpServletResponse response) {
		return "" 
	}

}

Currently, we have a single /welcome route - ideally, we want to send a personalized message to the logged in user, like "welcome <username>".

Let’s see what we need to do to achieve this:

  1. We need to let the user log in
  2. We have to issue a new JWT token every time a user logs in - this token should contain information about the logged in user that we can use in our welcome page.
  3. We should extract the users information from the JWT for every request made to the /welcome route.

jwt login flow

We can configure our application to perform all these steps using the Spring security library.

Spring Security Configuration

To add the security configuration, let’s create a new SecurityConfiguration class:

/**
 * Defines the spring security configuration for our application via the `getSecurityFilterChain`
 * method
 */
@Configuration
@EnableWebSecurity
class SecurityConfiguration {

    /**
     * @param httpSecurity is injected by spring security
     * @param jwtSecurityContextRepository is the injected instance of the
     *        JwtSecurityContextRepository class that we defined earlier
     */
    @Bean
    @Autowired
    SecurityFilterChain getSecurityFilterChain(HttpSecurity httpSecurity,
        JwtSecurityContextRepository jwtSecurityContextRepository) throws Exception {

        httpSecurity
            // Disable CSRF (not required for this demo)
            .csrf().disable()
            // Configure stateless session management. For JWT based auth, all the user
            // authentication info is self contained in the token itself, so we don't
            // need to store any additional session information
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            // Configure the custom authentication strategy we defined earlier
            .sessionAuthenticationStrategy(new JwtSessionAuthenticationStrategy())
            .and()
            .securityContext()
            // Configure the context repository that was injected
            .securityContextRepository(jwtSecurityContextRepository)
            .and()
            .authorizeHttpRequests().anyRequest().authenticated()
            .and()
            // Now when the user navigates to /login (default) they will
            // be taken to a form login page, and be redirected to the "/welcome"
            // route when successfully logged in
            .formLogin()
            .successForwardUrl("/welcome");

        return httpSecurity.build();
    }

    @Bean
    public AuthenticationEventPublisher authenticationEventPublisher(
        ApplicationEventPublisher applicationEventPublisher) {
        return new DefaultAuthenticationEventPublisher(applicationEventPublisher);
    }

    // We can store some sample user names and passwords in an in memory user details manager.
    // Since this method is exposed as a bean it will be auto injected into other spring components
    // Like the JwtSecurityContextRepository class
    @Bean
    public UserDetailsService userDetailsService() {
        // we Are going with to default password encoder or for this example
        // however, in production you should use something more robust, like a BCryptPasswordEncoder
        UserDetails user1 = User.withDefaultPasswordEncoder()
            .username("user1")
            .password("password1")
            .roles("USER")
            .build();

        UserDetails user2 = User.withDefaultPasswordEncoder()
            .username("user2")
            .password("password2")
            .roles("USER")
            .build();

        return new InMemoryUserDetailsManager(user1, user2);
    }
}

Now, the SecurityFilterChain configured by the getSecurityFilterChain method will define the mechanism used to authenticate our users.

We are defining custom implementations for our session authentication strategy and security context repository:

  1. The session authentication strategy tells the framework what to do once a user successfully authenticates. In our case we will be issuing a new JWT when this happens.
  2. The security context repository is used to load user information for every authenticated request that comes into our system. This would involve verifying the JWT signature and decoding user information from the token.

Before we get to these custom implementations let’s define a utility class that provides functions to create, verify, and decode JWT tokens:

/**
 * Contains all the methods needed to encode, decode and verify the JWT
 */
public class JwtUtils {

    // Private key used to sign and verify the JWT
    private static final String SECRET_KEY = "my-secret-key";
    // Name of the cookie to set and retrieve
    private static final String COOKIE_NAME = "token";
    // Settings for the JWT configuration, like algorithm, verification method and expiry age
    // In most cases, we can use these settings as is
    private static final Algorithm JWT_ALGORITHM = Algorithm.HMAC256(SECRET_KEY);
    private static final JWTVerifier JWT_VERIFIER = JWT.require(JWT_ALGORITHM).build();
    private static final int MAX_AGE_SECONDS = 120;
    private static final int MAX_REFRESH_WINDOW_SECONDS = 30;


    static Cookie generateCookie(String username) {
        // Create a new JWT token string, with the username embedded in the payload
        Instant now = Instant.now();
        String token = JWT.create()
            .withIssuedAt(now)
            .withExpiresAt(now.plusSeconds(MAX_AGE_SECONDS))
            // A "claim" is a single payload value which we can set
            .withClaim("username", username)
            .sign(JWT_ALGORITHM);

        // Create a cookie with the value set as the token string
        Cookie jwtCookie = new Cookie(COOKIE_NAME, token);
        jwtCookie.setMaxAge(MAX_AGE_SECONDS);
        return jwtCookie;
    }

    static Optional<String> getToken(HttpServletRequest request) {
        // Get the cookies from the request
        Cookie[] cookies = request.getCookies();
        if (cookies == null) {
            return Optional.empty();
        }

        // Find the cookie with the cookie name for the JWT token
        for (int i = 0; i < cookies.length; i++) {
            Cookie cookie = cookies[i];
            if (!cookie.getName().equals(COOKIE_NAME)) {
                continue;
            }
            // If we find the JWT cookie, return its value
            return Optional.of(cookie.getValue());
        }
        // Return empty if no cookie is found
        return Optional.empty();
    }

    static Optional<DecodedJWT> getValidatedToken(String token) {
        try {
            // If the token is successfully verified, return its value
            return Optional.of(JWT_VERIFIER.verify(token));
        } catch (JWTVerificationException e) {
            // If the token can't be verified, return an empty value
            return Optional.empty();
        }
    }

    // Gets the expiry timestamp from the request and returns true if it falls
    // within the allowed window, which starts at a given time before expiry
    // in this case, 30s
    static boolean isRefreshable(HttpServletRequest request) {
        Optional<String> token = getToken(request);
        if (token.isEmpty()) {
            return false;
        }
        Instant expiryTime = JWT.decode(token.get()).getExpiresAtAsInstant();
        Instant canBeRefreshedAfter = expiryTime.minusSeconds(MAX_REFRESH_WINDOW_SECONDS);
        return Instant.now().isAfter(canBeRefreshedAfter);
    }

}

We are using the auth0 java JWT library for the JWT implementation

The JwtUtils class can now help us deal with JWT tokens in our HTTP server:

  1. generateCookie creates a cookie with a new JWT token embedded within it. The token itself has the username encoded within its payload along with an expiry date.
  2. getToken lets us obtain the username from the token contained in the cookie. By default, cookies are sent with every HTTP request from the same client.
  3. getValidatedToken verifies a JWT token payload and returns the validated token.
  4. isRefreshable checks if the JWT can be refreshed based on its expiry date.

Handling User Sign In

Let’s see how to implement the JwtSessionAuthenticationStrategy class that we used in our security configuration. This defines what will happen once the user logs in.

/**
 * This is our custom session authentication strategy, where we issue a JWT token when the user
 * successfully logs in
 */
public class JwtSessionAuthenticationStrategy implements SessionAuthenticationStrategy {

    public JwtSessionAuthenticationStrategy() {}

    @Override
    public void onAuthentication(Authentication authentication, HttpServletRequest request,
            HttpServletResponse response) throws SessionAuthenticationException {
        // Get the authenticated user information
        User user = (User) authentication.getPrincipal();
        // Create a cookie containing the JWT that has the username encoded within its payload
        Cookie jwtCookie = JwtUtils.generateCookie(user.getUsername());
        // Set the cookie via the HTTP response
        response.addCookie(jwtCookie);
    }
}

When a user successfully logs in, we create a JWT, and set a cookie containing the JWT via the response.addCookie method.

Now this cookie will be sent in every subsequent request.

The actual username and password verification is handled under the hood by the spring security framework. The onAuthentication is only called once this verification is complete

Handling Post Authentication Routes

Now that all logged in clients have session information stored on their end as cookies, we can use it to:

  • Authenticate subsequent user requests
  • Get information about the user making the request

We can implement a new class (JwtSecurityContextRepository) to implement this:

/**
 * Responsible for validating the JWT and extracting and populating user details
 */
@Component
public class JwtSecurityContextRepository implements SecurityContextRepository {

    private UserDetailsService userDetailsService;

    // Use Spring dependency injection to assign the UserDetailsService instance
    // that we defined in the SecurityConfiguration class
    @Autowired
    public JwtSecurityContextRepository(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
        // Extract the JWT token from the request
        HttpServletRequest request = requestResponseHolder.getRequest();
        Optional<String> maybeToken = JwtUtils.getToken(request);

        // Get the security context for the request thread, which should be empty at this point
        SecurityContext context = SecurityContextHolder.getContext();

        // If token doesn't exist, return the empty context
        if (maybeToken.isEmpty()) {
            return context;
        }

        // decode and validate the JWT
        Optional<DecodedJWT> decodedJWT = JwtUtils.getValidatedToken(maybeToken.get());
        // if the token cannot be decoded or validated, return the empty context
        if (decodedJWT.isEmpty()) {
            return context;
        }

        // Create the UserDetails instance from the decoded JWT
        UserDetails userDetails = userDetailsService
            .loadUserByUsername(decodedJWT.get().getClaim("username").asString());

        // Create the authentication token from userDetails and add it to the security context
        // This will now be available in all downstream spring filters (including the request
        // handlers)
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
            new UsernamePasswordAuthenticationToken(userDetails, null,
                userDetails.getAuthorities());
        context.setAuthentication(usernamePasswordAuthenticationToken);
        return context;
    }

    // Since the user information is stored in the JWT itself, we can leave this method as a no-op
    @Override
    public void saveContext(SecurityContext context, HttpServletRequest request,
        HttpServletResponse response) {}

    // to override this method, we need to return a boolean value denoting
    // if the token exists in our request
    @Override
    public boolean containsContext(HttpServletRequest request) {
        return JwtUtils.getToken(request).isPresent();
    }

}

The @Component annotation adds the JwtSecurityContextRepository class to springs dependency injection framework. This means that it can be injected into other included classes. In this case, we’re injecting it into the getSecurityFilterChain method in our security configuration.

The @AutoWired annotation in the constructor injects the UserDetailsService object that we defined in our security configuration as a Bean.

Finally, let’s implement the /welcome route in our Application class:

@SpringBootApplication
@RestController
public class Application {

    // ...

	// All requests to '/welcome' will be handled by this method
	@RequestMapping("/welcome")
	public String simpleRequest(HttpServletResponse response) {
		// Get the username of the currently logged in user
		Optional<String> username = getUsernameFromSecurityContext();
		if (username.isEmpty()) {
			// if user information cannot be obtained, return
			// a 403 status
			response.setStatus(HttpStatus.FORBIDDEN.value());
			return "error";
		}

		return "welcome " + username.get();
	}

    private Optional<String> getUsernameFromSecurityContext() {
		// Get the security context for this request thread
		// and get the principal object from the context
		SecurityContext context = SecurityContextHolder.getContext();
		Object principal = context.getAuthentication().getPrincipal();

		// If the user is authenticated, the principal should be an
		// instance of UserDetails
		if (!(principal instanceof UserDetails)) {
			// if not, return an empty value
			return Optional.empty();
		}

		// Get the username from the userdetails and return
		// the welcome message
		String username =
			((UserDetails) principal).getUsername();
		return Optional.of(username);
	}

    // ...
}

Now that we’ve implemented the security context repository, we can use getUsernameFromSecurityContext - which gets details of the logged in user for the current request thread.

Since the security context belongs to a thread, we don’t need to provide any arguments to the SecurityContextHolder.getContext() method.

Refreshing The JWT Token

In this example, we have set a short expiry time of two minutes. We should not expect the user to login every two minutes if their token expires.

To solve this, we will create another /refresh route that takes the previous token (which is still valid), and returns a new token with a renewed expiry time.

To minimize misuse of a JWT, the expiry time is usually kept in the order of a few minutes. Typically the client application would refresh the token in the background.

@SpringBootApplication
@RestController
public class Application {

    // ...

	@GetMapping("/refresh")
	public void refreshToken(HttpServletRequest request, HttpServletResponse response) {
		// Get the username of the currently logged in user
		Optional<String> username = getUsernameFromSecurityContext();
		if (username.isEmpty() || !JwtUtils.isRefreshable(request)) {
			// if user information cannot be obtained,
			// or if the token isn't supposed to be refreshed
			// return a 403 status
			response.setStatus(HttpStatus.FORBIDDEN.value());
			return;
		}

		Cookie jwtCookie = JwtUtils.generateCookie(username.get());
		// Set the cookie via the HTTP response
		response.addCookie(jwtCookie);
	}

    private Optional<String> getUsernameFromSecurityContext() {
		// Get the security context for this request thread
		// and get the principal object from the context
		SecurityContext context = SecurityContextHolder.getContext();
		Object principal = context.getAuthentication().getPrincipal();

		// If the user is authenticated, the principal should be an
		// instance of UserDetails
		if (!(principal instanceof UserDetails)) {
			// if not, return an empty value
			return Optional.empty();
		}

		// Get the username from the userdetails and return
		// the welcome message
		String username =
			((UserDetails) principal).getUsername();
		return Optional.of(username);
	}

    // ...
}

Handling Logout

Logging out can be tricky when it comes to JWT-based authentication, since our application is meant to be stateless - meaning we don’t store any information about issued JWT tokens on our server.

The only information we do have is our secret key and algorithm used to encode and decode the JWT. If a token satisfies these requirements, it is considered valid by our application.

This is why the recommended way to handle logout is to provide tokens with a short expiry time, and require the client to keep refreshing the token. This way, we can ensure that for an expiry period T, the maximum time a user can stay logged in without the applications explicit permission is T seconds.

Another option we have is to create a /logout route that clears the users token cookie, so that subsequent requests would be unauthenticated.

We can do this by configuring logout behaviour in our SecurityConfiguration class:

@Configuration
@EnableWebSecurity
class SecurityConfiguration {

    // ...

    // We need to add more configuration to the `getSecurityFilterChain` method
    // the previous configuration remains the same
    @Bean
    @Autowired
    SecurityFilterChain getSecurityFilterChain(HttpSecurity httpSecurity,
        JwtSecurityContextRepository jwtSecurityContextRepository) throws Exception {

        httpSecurity
            .csrf().disable()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .sessionAuthenticationStrategy(new JwtSessionAuthenticationStrategy())
            .and()
            .securityContext()
            .securityContextRepository(jwtSecurityContextRepository)
            .and()
            .authorizeHttpRequests().anyRequest().authenticated()
            .and()
            .formLogin()
            .successForwardUrl("/welcome")
            .and()

            // We can use the `logout()` method to configure its behaviour
            // When the user logs out, remove the cookie that stored their
            // JWT payload
            .logout()
            .deleteCookies(JwtUtils.COOKIE_NAME);

        return httpSecurity.build();
    }

    // ...
}

However, this is a client side implementation, and can be circumvented if the client decides not to follow instructions and delete the cookie.

We can also store JWTs that we want to invalidate on the server, but this would make our application stateful.

Running Our Application

We can run the application with maven:

mvn clean compile package && java -jar ./target/spring-security-examples-0.0.1-SNAPSHOT.jar

Now, using any HTTP client with support for cookies (like Postman, or your web browser) make a sign-in request with the appropriate credentials:

POST http://localhost:8081/login 
Content-Type: application/x-www-form-urlencoded

username=user1&password=password1

You can now try hitting the welcome route from the same client to get the welcome message:

GET http://localhost:8081/welcome

Result:

welcome user1

Hit the refresh route, and then inspect the clients cookies to see the new value of the token cookie:

POST http://localhost:8081/refresh

You can find the working source code for this example here.