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.
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 .
):
- 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. - 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. - 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)
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.
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:
- We need to let the user log in
- 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.
- We should extract the users information from the JWT for every request made to the
/welcome
route.
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:
- 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.
- 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:
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.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.getValidatedToken
verifies a JWT token payload and returns the validated token.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.