JSON Web Tokens (JWTs) are stateless, compact, and self-contained standard to transmit the information as a JSON object. This object is usually encoded and encrypted to ensure the authenticity of the message. JWTs are small enough to be sent through URLs. Since they are self-contained, applications can glean sufficient authentication information from them, saving trips to the database. Being stateless, JWTs are particularly suitable to work with REST and HTTP (which are also stateless).
So, how does this work?
- When an application is secured using a JWT-based authentication, it requires a user to login with their credentials. These credentials can be backed by a database, a dedicated Identity and Access Management (IAM) system, etc.
- Once the login is successful, the application returns a JWT token. This token can be saved on the client-side (using localStorage, cookie, etc.).
- When a subsequent request is made to the application, the token should be sent with it in an
Authorization
header, often using a Bearer schema.
In this post, we’ll create a Spring Boot API and secure it using Spring Security and JWT-based authentication.
httpie is a user-friendly HTTP client with first-class JSON support and many other features. We’ll use it to send requests to our APIs.
Configure the project
Generate a Spring Boot project with Spring Initializr, and add the spring-boot-starter-web
as a dependency.
xml version="1.0" encoding="UTF-8"?>
<?project xmlns="http://maven.apache.org/POM/4.0.0"
<xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
modelVersion>4.0.0</modelVersion>
<
parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
<parent>
</
groupId>dev.mflash.guides</groupId>
<artifactId>spring-security-jwt-auth</artifactId>
<version>0.0.1-SNAPSHOT</version>
<
properties>
<java.version>15</java.version>
<properties>
</
dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<dependency>
</
dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<exclusion>
</exclusions>
</dependency>
</dependencies>
</
build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<plugin>
</plugins>
</build>
</
project> </
Create some endpoints
Let’s create some endpoints to begin with. Here’s a GenericController
that exposes a GET endpoint which returns a message. This is the functional way of implementing a controller.
// src/main/java/dev/mflash/guides/jwtauth/controller/GenericController.java
public @Controller class GenericController {
public static final String PUBLIC_ENDPOINT_URL = "/jwt/public";
private ServerResponse publicEndpoint(ServerRequest request) {
return ServerResponse.ok().contentType(APPLICATION_JSON).body(messageMap("public"));
}
private Map<String, String> messageMap(String type) {
return Map.of("message", String.format("Hello, world! This is a %s endpoint", type));
}
public @Bean RouterFunction<ServerResponse> genericRoutes() {
return route()
.GET(PUBLIC_ENDPOINT_URL, this::publicEndpoint)
.build();
} }
Launch the application and send the request to the endpoint.
$ http :8080/jwt/public
HTTP/1.1 200 # other headers
{"message": "Hello, world! This is a public endpoint"
}
Enable User registration with Spring Security
Spring Security’s AuthenticationManager
works with a UserDetails
object to handle the authentication. For a custom user, say CustomUser
, we’ll have to provide a corresponding UserDetails
object. This can be done by
- defining a
CustomUser
andCustomUserRepository
- extending
UserDetailsService
interface and overriding itsloadUserByUsername
method to return the details for aCustomUser
, and - adding this service to the
AuthenticationManager
through a configuration.
In our case, we’re going to save the
CustomUser
in an in-memory H2 database. You can use other databases (such as Postgres or MongoDB) to do the same. You can even integrateUserDetailsService
with solutions like LDAP, OIDC, etc., if needed.
Add spring-boot-starter-data-jdbc
, h2
and spring-boot-starter-security
in the pom.xml
.
xml version="1.0" encoding="UTF-8"?>
<?project xmlns="http://maven.apache.org/POM/4.0.0"
<xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
modelVersion>4.0.0</modelVersion>
<
parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
<parent>
</
groupId>dev.mflash.guides</groupId>
<artifactId>spring-security-jwt-auth</artifactId>
<version>0.0.1-SNAPSHOT</version>
<
properties>
<java.version>15</java.version>
<properties>
</
dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<dependency>
</
dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
<dependency>
</dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
<dependency>
</
dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<dependency>
</
dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<exclusion>
</exclusions>
</dependency>
</dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
<dependency>
</dependencies>
</
build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<plugin>
</plugins>
</build>
</
project> </
Create an API to save CustomUser
Since we would need a table to save the CustomUser
objects, let’s create a schema.sql
file to initialize it.
-- src/main/resources/schema.sql
DROP TABLE IF EXISTS custom_user;
CREATE TABLE custom_user (
INT AUTO_INCREMENT PRIMARY KEY,
id VARCHAR(250) NOT NULL,
email VARCHAR(250) NOT NULL,
name VARCHAR(250) NOT NULL
password );
Spring provides multiple ways to initialize a database schema through scripts available on the classpath. Check out the docs for more details.
In this case, Spring will read the schema.sql
file and execute the statements specified in it whenever the application is launched.
Define an entity corresponding to this table.
// src/main/java/dev/mflash/guides/jwtauth/security/CustomUser.java
public class CustomUser {
private @Id int id;
private String email;
private String name;
private String password;
// getters, setters, etc.
}
Define a repository to save and fetch the user. For this application, we’ll treat the email
as the username of a CustomUser
.
// src/main/java/dev/mflash/guides/jwtauth/security/CustomUserRepository.java
public interface CustomUserRepository extends CrudRepository<CustomUser, Long> {
Optional<CustomUser> findByEmail(String email);
}
To let a user register on the application, expose an endpoint to create a new CustomUser
.
// src/main/java/dev/mflash/guides/jwtauth/controller/UserRegistrationController.java
public @Controller class UserRegistrationController {
public static final String REGISTRATION_URL = "/user/register";
private final CustomUserRepository repository;
private final PasswordEncoder passwordEncoder;
public UserRegistrationController(CustomUserRepository repository, PasswordEncoder passwordEncoder) {
this.repository = repository;
this.passwordEncoder = passwordEncoder;
}
private ServerResponse register(ServerRequest request) throws ServletException, IOException {
final CustomUser newUser = request.body(CustomUser.class);
.setPassword(passwordEncoder.encode(newUser.getPassword()));
newUser.save(newUser);
repositoryreturn ServerResponse.ok().contentType(APPLICATION_JSON)
Map.of("message", String.format("Registration successful for %s", newUser.getName())));
.body(
}
public @Bean RouterFunction<ServerResponse> registrationRoutes() {
return route()
.POST(REGISTRATION_URL, this::register)
.build();
} }
Note that we’re encoding the plaintext password sent by the user before saving it into the database. PasswordEncoder
is not provided by default; we’ll have to inject it through a configuration.
Integrate the user management with Spring Security
Implement the UserDetailsService
which provides loadUserByUsername
method to convert CustomUser
for Spring Security by specifying that the email
is the username field.
// src/main/java/dev/mflash/guides/jwtauth/security/CustomUserDetailsService.java
public @Service class CustomUserDetailsService implements UserDetailsService {
private static final String PLACEHOLDER = UUID.randomUUID().toString();
private static final User DEFAULT_USER = new User(PLACEHOLDER, PLACEHOLDER, List.of());
private final CustomUserRepository repository;
public CustomUserDetailsService(CustomUserRepository repository) {
this.repository = repository;
}
public @Override UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return repository.findByEmail(email)
CustomUserConverter::toUser)
.map(DEFAULT_USER);
.orElse(
} }
CustomUserConverter
is a utility class that provides methods to convert CustomUser
into other objects.
// src/main/java/dev/mflash/guides/jwtauth/security/CustomUserConverter.java
public class CustomUserConverter {
public static User toUser(CustomUser user) {
return new User(user.getEmail(), user.getPassword(), List.of());
}
public static UsernamePasswordAuthenticationToken toAuthenticationToken(CustomUser user) {
return new UsernamePasswordAuthenticationToken(user.getEmail(), user.getPassword(), List.of());
} }
Now, we can inject CustomUserDetailsService
into Spring Security’s AuthenticationManager
to complete the integration of CustomUser
.
// src/main/java/dev/mflash/guides/jwtauth/security/SecurityConfiguration.java
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final CustomUserDetailsService userDetailsService;
public SecurityConfiguration(CustomUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public @Bean PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
protected @Override void configure(AuthenticationManagerBuilder auth) throws Exception {
.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
auth
} }
Note that the same PasswordEncoder
is used by UserRegistrationController
to encode the password of a new user.
Add Authentication and Authorization filters
To work with JWT, add the following dependencies in pom.xml
.
<!-- Rest of the POM file -->
dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
<dependency>
</dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
<dependency>
</dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
<dependency>
</dependencies> </
Create a TokenManager
class that’ll generate and parse the JWT tokens.
// src/main/java/dev/mflash/guides/jwtauth/security/TokenManager.java
public class TokenManager {
private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS512);
public static final String TOKEN_PREFIX = "Bearer ";
private static final int TOKEN_EXPIRY_DURATION = 10; // in days
public static String generateToken(String subject) {
return Jwts.builder()
.setSubject(subject)Date.from(ZonedDateTime.now().plusDays(TOKEN_EXPIRY_DURATION).toInstant()))
.setExpiration(SECRET_KEY)
.signWith(
.compact();
}
public static String parseToken(String token) {
return Jwts.parserBuilder()
SECRET_KEY)
.setSigningKey(
.build().replace(TOKEN_PREFIX, ""))
.parseClaimsJws(token
.getBody()
.getSubject();
} }
SECRET_KEY
is a randomly-generated key using the HS512
algorithm (there are other algorithms, as well). This key is used for signing the tokens by generateToken
method and subsequently to read them by parseToken
method. We’ve also set the tokens to expire after 10 days (through TOKEN_EXPIRY_DURATION
constant).
Now, define an AuthenticationFilter
to verify the correct user.
// src/main/java/dev/mflash/guides/jwtauth/security/CustomAuthenticationFilter.java
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public static final String AUTH_HEADER_KEY = "Authorization";
private final AuthenticationManager authenticationManager;
public CustomAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
public @Override Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
try {
var user = new ObjectMapper().readValue(request.getInputStream(), CustomUser.class);
return authenticationManager.authenticate(CustomUserConverter.toAuthenticationToken(user));
catch (IOException e) {
} throw new AuthenticationCredentialsNotFoundException("Failed to resolve authentication credentials", e);
}
}
protected @Override void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authResult) throws IOException, ServletException {
.addHeader(AUTH_HEADER_KEY,
responseTOKEN_PREFIX + generateToken(((User) authResult.getPrincipal()).getUsername()));
} }
Here,
- the
attemptAuthentication
method extracts the user from the request and tries to authenticate them with the help ofAuthenticationManager
. - On successful authentication, a token is generated by
TokenManager
and attached to the header of the response (seesuccessfulAuthentication
method). This token will be used for subsequent requests and will be checked every time a request arrives.
On successful verification of the token, access to the application will be enabled with the help of the doFilterInternal
method of the CustomAuthorizationFilter
.
// src/main/java/dev/mflash/guides/jwtauth/security/CustomAuthorizationFilter.java
public class CustomAuthorizationFilter extends BasicAuthenticationFilter {
public CustomAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
protected @Override void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String header = request.getHeader(AUTH_HEADER_KEY);
if (Objects.isNull(header) || !header.startsWith(TOKEN_PREFIX)) {
.doFilter(request, response);
chainreturn;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authentication);
.doFilter(request, response);
chain
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String header = request.getHeader(AUTH_HEADER_KEY);
if (Objects.nonNull(header) && header.startsWith(TOKEN_PREFIX)) {
try {
String username = parseToken(header);
return new UsernamePasswordAuthenticationToken(username, null, List.of());
catch (ExpiredJwtException e) {
} throw new AccessDeniedException("Expired token");
catch (UnsupportedJwtException | MalformedJwtException e) {
} throw new AccessDeniedException("Unsupported token");
catch (Exception e) {
} throw new AccessDeniedException("User authorization not resolved");
}else {
} throw new AccessDeniedException("Authorization token not found");
}
} }
Here, the doFilterInternal
method extracts the Authorization
header, fetches the authentication status, and updates the SecurityContext
. If the authentication fails, the request to the application is denied by the filter.
We need to register these filters and specify which endpoints are protected and which are accessible publicly in the SecurityConfiguration
.
// src/main/java/dev/mflash/guides/jwtauth/security/SecurityConfiguration.java
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final CustomUserDetailsService userDetailsService;
public SecurityConfiguration(CustomUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected @Override void configure(HttpSecurity http) throws Exception {
.cors().and()
http.disable()
.csrf()
.authorizeRequests()PUBLIC_ENDPOINT_URL).permitAll()
.antMatchers(POST, REGISTRATION_URL).permitAll()
.antMatchers(.authenticated().and()
.anyRequest()new CustomAuthenticationFilter(authenticationManager()))
.addFilter(new CustomAuthorizationFilter(authenticationManager()))
.addFilter(.sessionCreationPolicy(STATELESS);
.sessionManagement()
}
public @Bean PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
protected @Override void configure(AuthenticationManagerBuilder auth) throws Exception {
.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
auth
}
public @Bean CorsConfigurationSource corsConfigurationSource() {
final var source = new UrlBasedCorsConfigurationSource();
.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
sourcereturn source;
} }
Here, we’ve
- allowed the user registration endpoint and an endpoint from
GenericController
to be accessible publicly, and restricted all other URLs to be accessible only after authentication. - disabled the session management of Spring Security by setting the
SessionCreationPolicy
to beSTATELESS
since JWT authentication is stateless.
Let’s create a sample endpoint in GenericController
that is secured with this implementation.
// src/main/java/dev/mflash/guides/jwtauth/controller/GenericController.java
public @Controller class GenericController {
public static final String PUBLIC_ENDPOINT_URL = "/jwt/public";
public static final String PRIVATE_ENDPOINT_URL = "/jwt/private";
private ServerResponse publicEndpoint(ServerRequest request) {
return ServerResponse.ok().contentType(APPLICATION_JSON).body(messageMap("public"));
}
private ServerResponse privateEndpoint(ServerRequest request) {
return ServerResponse.ok().contentType(APPLICATION_JSON).body(messageMap("private"));
}
private Map<String, String> messageMap(String type) {
return Map.of("message", String.format("Hello, world! This is a %s endpoint", type));
}
public @Bean RouterFunction<ServerResponse> genericRoutes() {
return route()
.GET(PUBLIC_ENDPOINT_URL, this::publicEndpoint)
.GET(PRIVATE_ENDPOINT_URL, this::privateEndpoint)
.build();
} }
Testing the application
Launch the application, and try to hit the http://localhost:8080/jwt/private endpoint.
$ http :8080/jwt/private
HTTP/1.1 403# other headers
{"error": "Forbidden",
"message": "",
"path": "/jwt/private",
"status": 403,
"timestamp": "2020-11-05T11:32:30.771+00:00"
}
The response 403 Forbidden
is expected, since this endpoint is no longer accessible publicly. Now, register as a new user.
'Arya Antrix' email=arya.antrix@example.com password=pa55word
$ http POST :8080/user/register name=
HTTP/1.1 200# other headers
{"message": "Registration successful for Arya Antrix"
}
and login with this user.
$ http POST :8080/login email=arya.antrix@example.com password=pa55word
HTTP/1.1 200
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhcnlhLmFudHJpeEBleGFtcGxlLmNvbSIsImV4cCI6MTYwNTQ0NjUyNn0.lxeHhzdaDxa_PEF3zzhIsft6M3qexjJA2CyrPzAFrAZOP7zgP1slec5w41v08R_9LC7Bnbb7loIwNGn5GlVohg# other headers
You’ll receive a response 200 OK
with an Authorization
header that contains a Bearer
token. Use this token and hit the http://localhost:8080/jwt/private endpoint, again. This time, you’ll get a successful response.
'Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhcnlhLmFudHJpeEBleGFtcGxlLmNvbSIsImV4cCI6MTYwNTQ0NjUyNn0.lxeHhzdaDxa_PEF3zzhIsft6M3qexjJA2CyrPzAFrAZOP7zgP1slec5w41v08R_9LC7Bnbb7loIwNGn5GlVohg'
$ http :8080/jwt/private
HTTP/1.1 200# other headers
{"message": "Hello, world! This is a private endpoint"
}
You can use the above scenarios to write some unit tests (using Spring’s MockMvc
and AssertJ assertions).
// src/test/java/dev/mflash/guides/jwtauth/controller/GenericControllerTest.java
@SpringBootTest
@AutoConfigureMockMvc
@ExtendWith(SpringExtension.class)
class GenericControllerTest {
private @Autowired MockMvc mvc;
@Test
@DisplayName("Should be able to access public endpoint without auth")
void shouldBeAbleToAccessPublicEndpointWithoutAuth() throws Exception {
MockHttpServletResponse response = mvc.perform(get(PUBLIC_ENDPOINT_URL))
.isOk())
.andExpect(status().getResponse();
.andReturn()
.getContentAsString()).isNotEmpty();
assertThat(response
}
@Test
@DisplayName("Should get forbidden on private endpoint without auth")
void shouldGetForbiddenOnPrivateEndpointWithoutAuth() throws Exception {
.perform(get(PRIVATE_ENDPOINT_URL))
mvc.isForbidden())
.andExpect(status()
.andReturn();
}
@Test
@DisplayName("Should be able to access private endpoint with auth")
@WithMockUser(username = "jwtUser")
void shouldBeAbleToAccessPrivateEndpointWithAuth() throws Exception {
MockHttpServletResponse response = mvc.perform(get(PRIVATE_ENDPOINT_URL))
.isOk())
.andExpect(status().getResponse();
.andReturn()
.getContentAsString()).isNotEmpty();
assertThat(response
} }
Here,
- the first test verifies that the public endpoint is accessible without any authentication
- the second test verifies that the application returns a proper error status (403 Forbidden) when the private endpoint receives a request without any authentication, and
- the final test verifies that once a user has been authenticated successfully, they’re able to access the private endpoint.