Username-password and JWT-based authentication is a common way of securing an API. The authorization server creates a token after the first authentication and allows the client to access the endpoint with the generated token for subsequent requests. Today, we’ll learn how to implement Username-password and JWT-based authentication in a Spring Boot 3 application using JPA, MySQL, and Spring Security.
Table Of Contents:
Example Application
We’ll create an example application with login and an authentication-protected dummy endpoint. Let’s begin with defining dependencies.
Maven Configuration:
Create a pom.xml file like the one below in the project root:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>spring-security-jwt-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.4</version>
</parent>
<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-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.2.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.12.5</version>
</dependency>
</dependencies>
<build>
<finalName>${project.name}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
Database Preparations and Configuration
As we’ll access the users from the database, let’s create the following user entity and repository.
package org.example.persistence;
import jakarta.persistence.*;
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String fullName;
@Column(nullable = false, length = 30, unique = true)
private String username;
@Column(nullable = false, length = 120)
private String password;
@Column(nullable = false, length = 20)
private String role;
private boolean enabled;
public UserEntity() {
}
public UserEntity(Long id, String fullName, String username, String password, String role, boolean enabled) {
this.id = id;
this.fullName = fullName;
this.username = username;
this.password = password;
this.role = role;
this.enabled = enabled;
}
public Long getId() {
return id;
}
public String getFullName() {
return fullName;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public String getRole() {
return role;
}
public boolean isEnabled() {
return enabled;
}
}
package org.example.persistence;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
Optional<UserEntity> findByUsername(String username);
}
We’ll store the users in a MySQL database in the example application. So, create a configuration file named application.yml under the resources folder to tell Hibernate what database we use and how to access it. Since we created the application for demo purposes, we will upload the first user from a script file to the database.
spring:
sql:
init:
# Just to load initial data for the demo. DO NOT USE IT IN PRODUCTION
mode: always
datasource:
# Put your
url: jdbc:mysql://localhost:3306/demo-dev
username: devuser
password: devpassword
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
database: mysql
database-platform: org.hibernate.dialect.MySQLDialect
# Just to load initial data for the demo. DO NOT USE IT IN PRODUCTION
defer-datasource-initialization: true
hibernate:
ddl-auto: update
jackson:
serialization:
indent-output: true
server:
port: 3000
To create an initial user, create a file named data.sql under the resources folder with the following content. The file will be loaded when the application starts.
truncate table `users`;
insert into `users` (`id`, `username`, `password`, `full_name`, `enabled`, `role`) values(1, 'user', '{bcrypt}$2a$10$IeofhAYT3lUfrF0bi1aflOat.IU3xOkZWaAWAuVc9jO2.QxTtH4RO', 'User', 1, 'USER');
-- for h2 database
-- alter table `users` alter column `id` restart with 2;
-- for mysql database
alter table `users` AUTO_INCREMENT = 2;
Run the following command to create a MySQL container to be used in the development environment.
$ docker run --name mysql \
-e MYSQL_ROOT_PASSWORD=root \
-e MYSQL_DATABASE=demo-dev \
-e MYSQL_USER=devuser \
-e MYSQL_PASSWORD=devpassword \
-p 3306:3306 \
-d mysql:latest
We’ve finished the database preparations. Let’s run the application and, see the created initial user on the database by running the following command.
$ docker exec -t mysql \
mysql \
-u devuser \
--password=devpassword \
-A demo-dev \
-e 'select * from users;'
You should see an output like the one below:
mysql: [Warning] Using a password on the command line interface can be insecure.
+----+------------------+-----------+----------------------------------------------------------------------+------+----------+
| id | enabled | full_name | password | role | username |
+----+------------------+-----------+----------------------------------------------------------------------+------+----------+
| 1 | 0x01 | User | {bcrypt}$2a$10$IeofhAYT3lUfrF0bi1aflOat.IU3xOkZWaAWAuVc9jO2.QxTtH4RO | USER | user |
+----+------------------+-----------+----------------------------------------------------------------------+------+----------+
Configure Spring Security To Authenticate Users From The Database
Create an implementation of UserDetailsService
interface like the following one to tell Spring Security how to load the user from the database.
package org.example.security;
import org.example.persistence.UserEntity;
import org.example.persistence.UserRepository;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
@Component
public class UserDetailsServiceImp implements UserDetailsService {
private final UserRepository userRepository;
public UserDetailsServiceImp(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity user = userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User not found"));
return User.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRole())
.disabled(!user.isEnabled())
.build();
}
}
Create a configuration class like the following one to declare the access rules and required Java beans.
package org.example.config;
import io.jsonwebtoken.security.Keys;
import org.example.api.filter.JwtAuthFilter;
import org.example.security.JwtUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.crypto.SecretKey;
@Configuration
public class SecurityConfig {
private final UserDetailsService userDetailsService;
public SecurityConfig(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
public JwtUtil jwtUtil() {
final SecretKey secretKey = Keys.hmacShaKeyFor(configProperties.getSecretKey().getBytes());
return new JwtUtil(secretKey);
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(configurer -> configurer
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(
configurer -> configurer
.requestMatchers("/login").permitAll()
.anyRequest().authenticated()
)
.authenticationProvider(authenticationProvider());
return http.build();
}
private AuthenticationProvider authenticationProvider() {
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
final DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder);
provider.setUserDetailsService(userDetailsService);
return provider;
}
}
JWT Utility
Since we need to create and validate A JWT at some points, we’ll create a utility class to perform these actions more easily.
package org.example.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.function.Function;
public class JwtUtil {
private final SecretKey secretKey;
public JwtUtil(SecretKey secretKey) {
this.secretKey = secretKey;
}
public String generate(String username, Integer ttlInMs) {
return Jwts.builder()
.subject(username)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + ttlInMs))
.signWith(secretKey)
.compact();
}
public String extractUsername(String token) throws JwtException {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpirationDate(String token) throws JwtException {
return extractClaim(token, Claims::getExpiration);
}
public Date extractCreatedAt(String token) throws JwtException {
return extractClaim(token, Claims::getIssuedAt);
}
public boolean isTokenValid(String token, String username) {
return !isTokenExpired(token) && extractUsername(token).equals(username);
}
private boolean isTokenExpired(String token) throws JwtException {
return extractExpirationDate(token).before(new Date());
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolvers) throws JwtException {
final Claims claims = extractClaims(token);
return claimsResolvers.apply(claims);
}
private Claims extractClaims(String token) throws JwtException {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
}
}
Define the following configurations in the application.yml file.
#...
application:
security:
secret-key: 3MP8Xi8ExjXcPHbOO3wWLRHJDGqwK6XV
jwt-ttl: 300000 # 5 minutes
Create a class named SecurityConfigProperties
like the following one to inject the custom configurations specified above wherever needed easily.
package org.example.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "application.security")
public class SecurityConfigProperties {
private String secretKey;
private Long jwtTtl;
public String getSecretKey() {
return secretKey;
}
public Long getJwtTtl() {
return jwtTtl;
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
public void setJwtTtl(Long jwtTtl) {
this.jwtTtl = jwtTtl;
}
}
Create an instance of JwtUtil with the secret key specified in the configuration and register as a bean.
package org.example.config;
import io.jsonwebtoken.security.Keys;
import org.example.security.JwtUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.crypto.SecretKey;
//...
@Configuration
public class SecurityConfig {
private final SecurityConfigProperties configProperties;
private final UserDetailsService userDetailsService;
public SecurityConfig(SecurityConfigProperties configProperties, UserDetailsService userDetailsService) {
this.configProperties = configProperties;
this.userDetailsService = userDetailsService;
}
//...
@Bean
public JwtUtil jwtUtil() {
final SecretKey secretKey = Keys.hmacShaKeyFor(configProperties.getSecretKey().getBytes());
return new JwtUtil(secretKey);
}
//...
}
Create A Login Endpoint
Create the following request and response models.
package org.example.api.dto;
import jakarta.validation.constraints.NotBlank;
public class LoginRequest {
@NotBlank
private final String username;
@NotBlank
private final String password;
public LoginRequest(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}
package org.example.api.dto;
import java.time.LocalDate;
import java.util.Date;
public class LoginResponse {
private final String token;
private final Date createdAt;
private final Date expiresAt;
public LoginResponse(String token, Date createdAt, Date expiresAt) {
this.token = token;
this.createdAt = createdAt;
this.expiresAt = expiresAt;
}
public String getToken() {
return token;
}
public Date getCreatedAt() {
return createdAt;
}
public Date getExpiresAt() {
return expiresAt;
}
}
Create a login controller to handle the login requests.
package org.example.api.controller;
import jakarta.validation.Valid;
import org.example.JwtUtil;
import org.example.api.dto.LoginRequest;
import org.example.api.dto.LoginResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.*;
@RestController
public class LoginController {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
private final Integer jwtTtl;
public LoginController(
AuthenticationManager authenticationManager,
JwtUtil jwtUtil,
@Value("{application.security.jwt-ttl}") Integer jwtTtl
) {
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
this.jwtTtl = jwtTtl;
}
@ResponseStatus(HttpStatus.OK)
@PostMapping("/login")
public LoginResponse login(@RequestBody @Valid LoginRequest request) {
UsernamePasswordAuthenticationToken authToken = UsernamePasswordAuthenticationToken.unauthenticated(request.getUsername(), request.getPassword());
Authentication authentication = authenticationManager.authenticate(authToken);
if (!authentication.isAuthenticated()) {
throw new UsernameNotFoundException("Failed to authenticate");
}
String token = jwtUtil.generate(request.getUsername(), jwtTtl);
return new LoginResponse(
token,
jwtUtil.extractCreatedAt(token),
jwtUtil.extractExpirationDate(token)
);
}
}
Now we have completed the part up to the authentication. Let’s run the application and perform the following authentication request.
$ curl -s -XPOST \
-H 'Content-Type: application/json' \
-d '{"username": "user", "password": "password"}' \
http://localhost:3000/login
You should see an output like this:
{
"token" : "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNzE2NzU0MDY5LCJleHAiOjE3MTY3NTQzNjl9.g3clo5I801f1CHp5tIBWULH82HOSqDjyxFfymQqKAVY",
"createdAt" : "2024-05-26T20:07:49.000+00:00",
"expiresAt" : "2024-05-26T20:12:49.000+00:00"
}
Authenticate Subsequent Requests With JWT
Create a filter named JwtAuthFilter
to authenticate the requests with JWT in the Authorization
request header.
package org.example.api.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.security.JwtUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService;
private final JwtUtil jwtUtil;
private final String AUTH_HEADER = "Authorization";
private final String AUTH_TYPE = "Bearer";
public JwtAuthFilter(UserDetailsService userDetailsService, JwtUtil jwtUtil) {
this.userDetailsService = userDetailsService;
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String token = extractAuthorizationHeader(request);
if (token == null) {
filterChain.doFilter(request, response);
return;
}
final String tokenUser = jwtUtil.extractUsername(token);
if (tokenUser != null && SecurityContextHolder.getContext().getAuthentication() == null) {
final UserDetails userDetails = userDetailsService.loadUserByUsername(tokenUser);
if (!jwtUtil.isTokenValid(token, tokenUser)) {
throw new UsernameNotFoundException("Failed to authenticate with access token");
}
final SecurityContext context = SecurityContextHolder.createEmptyContext();
final UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
context.setAuthentication(authToken);
SecurityContextHolder.setContext(context);
}
filterChain.doFilter(request, response);
}
private String extractAuthorizationHeader(HttpServletRequest request) {
final String headerValue = request.getHeader(AUTH_HEADER);
if (headerValue == null || !headerValue.startsWith(AUTH_TYPE)) {
return null;
}
return headerValue.substring(AUTH_TYPE.length()).trim();
}
}
As you could see from the code above, the filter allows the client to access the endpoint when the JWT from the Authorization header is valid and the user from the token exists in the user database.
Since we want requests targeting the authentication-protected endpoints to be handled by JwtAutFilter, we configure JwtAuthFilter to be called before UsernamePasswordAuthenticationFilter by the following configuration.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthFilter jwtAuthFilter) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(configurer -> configurer
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(
configurer -> configurer
.requestMatchers("/login").permitAll()
.anyRequest().authenticated()
)
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
Testing
Finally, we completed the implementation. As we reach the point that we‘re going to test the application, let’s create a dummy controller to simulate an authentication-protected endpoint.
package org.example.api.controller;
import org.example.api.dto.GreetingModel;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
@Controller
public class GreetingController {
@GetMapping("/greeting")
@ResponseStatus(HttpStatus.OK)
public GreetingModel sayHello() {
return new GreetingModel("Hello World!");
}
}
package org.example.api.dto;
public class GreetingModel {
private final String message;
public GreetingModel(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
Run the application and execute the following call.
$ curl -I -H 'Content-Type: application/json' http://localhost:3000/greeting
Because we called the endpoint without JWT, we should see a result like the following one.
HTTP/1.1 403
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 0
Date: Mon, 27 May 2024 05:37:57 GMT
Let’s authenticate and call the endpoint again with the JWT we received from the login response.
Execute the following call to authenticate the application
$ curl -s -XPOST \
-H 'Content-Type: application/json' \
-d '{"username": "user", "password": "password"}' \
http://localhost:3000/login
And after the execution probably you would see something like this
{
"token" : "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNzE2Nzg4NjA2LCJleHAiOjE3MTY3ODg5MDZ9.FQG-wmli8vrBrJ7RLLlxlkXGFtA4RT3rRdjjjOrD3EQ",
"createdAt" : "2024-05-27T05:43:26.000+00:00",
"expiresAt" : "2024-05-27T05:48:26.000+00:00"
}
Let’s try to call the dummy endpoint with our new token
$ curl \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNzE2Nzg4NjA2LCJleHAiOjE3MTY3ODg5MDZ9.FQG-wmli8vrBrJ7RLLlxlkXGFtA4RT3rRdjjjOrD3EQ' \
http://localhost:3000/greeting
You should see the same output as below
{
"message" : "Hello World!"
}
Just to not take the article longer, we haven’t touched the point regarding exception handler part, but I hope you enjoyed reading.
You can find the full source code of the example application on GitHub.
Thanks for reading!
Credits
- Cover photo by Micah Williams on Unsplash
Top comments (0)