Authentication and Authorization with Spring Security

Mina
12 min readDec 19, 2023

--

I will explain various authentication and authorization mechanisms, highlighting their pros and cons. Additionally, I’ll explore JWT and Spring Security. In the end, I will guide you through a detailed example implementation of using JSON Web Tokens (JWT) in a Spring Boot 3.2 application with Spring Security 6.2.

Authentication Mechanisms:

We have two authentication mechanisms: stateful and stateless.

Stateful Authentication (Cookie/Session Based Authentication):

This is the default and traditional method for handling user authentication. In this approach, the backend is responsible for creating, storing the session ID, and verifying the user’s identity.

Here is how it works: The server creates a session ID upon a user’s login request, storing it in either a database or an in-memory cache on the server. This session ID is then stored on a cookie in the user’s browser. With each subsequent request, the server receives the cookie containing the session ID and validates the user’s identity by comparing it with the corresponding session information stored on the server.

Stateful disadvantages:

Scalability: This approach might have challenges in highly scalable systems, as it requires server-side storage for session data.

Complexity: Implementing and managing session data, can add complexity to the system.

Stateless Authentication:

Stateless authentication using tokens (e.g., JWT) that are gaining popularity, especially in modern Microservices and distributed systems.

Token-Based Authentication:
Issuing a token (e.g., JWT) upon successful authentication.
The token is sent to the server with each request for authorization.
It is Stateless and scalable.

JSON Web Token (JWT)

JWT represents claims between two parties. JWT has a compact format, making them easy to transform over the network.

It consists of three parts:

  • Header: Specifies the JWT encoding and signing algorithm, with properties { “alg”: “HS256”, “typ”: “JWT” } where alg is the algorithm that is used to encrypt the JWT.
  • Payload: Contains the data(Claims) to be sent as JSON property–value pairs within the claims.
  • Signature: This is created by encrypting, with the algorithm specified in the header: (i) the base64Url-encoded header, (ii) base64Url-encoded payload, and (iii) a secret (or a private key):
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret|privateKey)

Using HMAC SHA256 algorithm, the signature will be created in the above way and separated by dots. For more details, refer to the jwt.io website.

Where and why should we use JWT?

Authentication: JWTs can be used for user authentication. Once a user logs in, a JWT is generated on the server and sent to the client. The client includes this token in the header of subsequent requests to authenticate the user.

Authorization: JWTs can also be used to convey information about the user’s permissions and roles. The server can include this information in the payload of the JWT, and the client can use it for authorization purposes.

Stateless: JWTs are often used in stateless authentication mechanisms. The server does not need to store the user’s session data, making it scalable, efficient and less complex.

Expiration: JWTs can have an expiration time, after which they are no longer considered valid. This helps enhance security by limiting the window of opportunity for an attacker to use a stolen token.

Spring Security

Spring Security is a powerful framework that focuses on providing both authentication and authorization to Java applications, also addressing common security vulnerabilities like CSRF (cross-site request forgery) and CORS (Cross-origin resource sharing).

Whenever we expose REST endpoints in Spring Boot, all the incoming requests are first received by the DispatcherServlet. The DispatcherServlet is the front and responsible to dispatch incoming HttpRequests to the correct handlers.

Adding spring security, enables us with the security filter chain to process requests and perform security-related tasks. We can customize this filter chain by adding or modifying filters based on our requirements.

This ensures that any incoming request will go through these filters.

Here is a list of filters in the filter chain:

The order is important, because they are dependent of each other.

We can also add a custom filter between two other filters in the chain.

Here is the diagram of the flow with step-by-step explanation:

  • Authentication : Filters like UsernamePasswordAuthenticationFilter handle the extraction of username/password from the HTTP request and create an authentication token.
  • AuthenticationManager : Receives requests from filters and delegates the validation of user details to the available authentication providers. It manages one or multiple authentication providers.
  • AuthenticationProvider : Is responsible for validating user details.
  • UserDetailsManager/UserDetailsService : loads user details and match the user with the provided input.

I will now create a Spring Boot application to handle user registration and authentication via REST APIs using JWT and Spring Security, providing a detailed step-by-step explanation.

I will expose Restful APIs for user registration and login. Client can make a request with their credentials (email + password) and when they are successfully authenticated the service will return a JWT.

Here you can find the source code: https://github.com/minarashidi/authentication

Project setup

Start by creating a new Spring Boot project using Spring Initializr, selecting the following dependencies. Import the project into your preferred IDE.

Here is the pom.xml file:

    <!-- Webmvc/Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- DB -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>

<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>

<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
  • Spring web(spring-boot-starter-web) for building Restful APIs
  • Spring Security for handling user authentication and authorization
  • JWT to use token-based authentication
  • Spring JDBC to interact with database
  • PostgreSQL Driver, we are going to be using the Postgres database.

Signup API

We need to expose a signup endpoint to register user, first I create a signup request class:


// Dto object with basic validation
public record SignupRequest(
@NotBlank(message = "Name cannot be blank")
String name,

@Email(message = "Invalid email format")
@NotBlank(message = "Email cannot be blank")
String email,

@NotBlank(message = "Password cannot be blank")
@Size(min = 6, max = 20, message = "Password must be between 6 and 20 characters")
String password) {

}

I added basic validation on login request properties using spring-boot-starter-validation dependency.

By adding spring-boot-starter-validation dependency, we do not need to do any extra configuration and it will automatically add the Hibernate Validator dependency to your pom.xml file and configure the validation for you.

A quick note: hibernate-validator is entirely separate from the persistence aspects of Hibernate. So, by adding it as a dependency, we’re not adding these persistence aspects into the project.

Here is the controller:

@PostMapping("/signup")
public ResponseEntity<Void> signup(@Valid @RequestBody SignupRequest requestDto) {
userService.signup(requestDto);
return ResponseEntity.status(HttpStatus.CREATED).build();
}

In the service, we first need to check if the user does not exist, then encrypt the password before saving it to the database.

 package com.mina.authentication.service;

import com.mina.authentication.controller.dto.SignupRequest;
import com.mina.authentication.domain.User;
import com.mina.authentication.exceptions.DuplicateException;
import com.mina.authentication.repository.UserRepository;
import java.util.Optional;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
public class UserService {

private final UserRepository repository;
private final PasswordEncoder passwordEncoder;

public UserService(UserRepository repository, PasswordEncoder passwordEncoder) {
this.repository = repository;
this.passwordEncoder = passwordEncoder;
}

@Transactional
public void signup(SignupRequest request) {
String email = request.email();
Optional<User> existingUser = repository.findByEmail(email);
if (existingUser.isPresent()) {
throw new DuplicateException(String.format("User with the email address '%s' already exists.", email));
}

String hashedPassword = passwordEncoder.encode(request.password());
User user = new User(request.name(), email, hashedPassword);
repository.add(user);
}

}

And in the repository, I use JDBC Client in Spring Boot 3.2 which is Fluent API to store user in DB.

@Repository
public class UserRepository {

private static final String INSERT = "INSERT INTO authentication.user (name, email, password) VALUES(:name, :email, :password)";
private static final String FIND_BY_EMAIL = "SELECT * FROM authentication.user WHERE email = :email";

private final JdbcClient jdbcClient;

public UserRepository(JdbcClient jdbcClient) {
this.jdbcClient = jdbcClient;
}

public void add(User user) {
long affected = jdbcClient.sql(INSERT)
.param("name", user.name())
.param("email", user.email())
.param("password", user.password())
.update();

Assert.isTrue(affected == 1, "Could not add user.");
}

public Optional<User> findByEmail(String email) {
return jdbcClient.sql(FIND_BY_EMAIL)
.param("email", email)
.query(User.class)
.optional();
}
}

Login API

Now the login API which takes the email and password as login request and returns JWT token as a response.

In the controller:

@PostMapping(value = "/login")
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.email(), request.password()));
String token = JwtHelper.generateToken(request.email());
return ResponseEntity.ok(new LoginResponse(request.email(), token));
}

We also need to have a helper class to generate and resolve JWT token.

package com.mina.authentication.helper;

import com.mina.authentication.exceptions.AccessDeniedException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import java.security.Key;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import org.springframework.security.core.userdetails.UserDetails;

public class JwtHelper {

private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
private static final int MINUTES = 60;

public static String generateToken(String email) {
var now = Instant.now();
return Jwts.builder()
.subject(email)
.issuedAt(Date.from(now))
.expiration(Date.from(now.plus(MINUTES, ChronoUnit.MINUTES)))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}

public static String extractUsername(String token) {
return getTokenBody(token).getSubject();
}

public static Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}

private static Claims getTokenBody(String token) {
try {
return Jwts
.parser()
.setSigningKey(SECRET_KEY)
.build()
.parseSignedClaims(token)
.getPayload();
} catch (SignatureException | ExpiredJwtException e) { // Invalid signature or expired token
throw new AccessDeniedException("Access denied: " + e.getMessage());
}
}

private static boolean isTokenExpired(String token) {
Claims claims = getTokenBody(token);
return claims.getExpiration().before(new Date());
}
}

generateToken(String email) : we need to create a Claims object from user data and build a JWT token. The Claims object is used as the jwt payload.

UserDetailsService & UserRepository

Spring security uses an interface called UserDetailsService to load user details and match the user with the input.

We create a custom user details service which implements UserDetailsService and override the loadUserByUsername() method, which returns an UserDetails object.

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

private final UserRepository repository;

public UserDetailsServiceImpl(UserRepository repository) {
this.repository = repository;
}

@Override
public UserDetails loadUserByUsername(String email) {

User user = repository.findByEmail(email).orElseThrow(() ->
new NotFoundException(String.format("User does not exist, email: %s", email)));

return org.springframework.security.core.userdetails.User.builder()
.username(user.email())
.password(user.password())
.build();
}
}

Spring security will internally call this method with user provided email, and then match the password from UserDetails object with user provided password.

Adding Spring Security Config

We need to create a security config class to define permissions and annotate it with @EnableWebSecurity @Configuration These annotations tells spring security to use our custom security configuration instead of default.

We need to create a Bean of SecurityFilterChain which implements our custom filter logic.

package com.mojang.authentication.config;

import com.mojang.authentication.service.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

private final UserDetailsServiceImpl userDetailsService;

public SecurityConfig(UserDetailsServiceImpl userDetailsService) {
this.userDetailsService = userDetailsService;
}

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

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
return http
.cors(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// Set permissions on endpoints
.authorizeHttpRequests(auth -> auth
// our public endpoints
.requestMatchers(HttpMethod.POST, "/api/auth/signup/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/auth/login/**").permitAll()
.requestMatchers(HttpMethod.GET, "/authentication-docs/**").permitAll()
// our private endpoints
.anyRequest().authenticated())
.authenticationManager(authenticationManager)
.build();
}

@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
return authenticationManagerBuilder.build();
}
}

// CORS(Cross-origin resource sharing) is just to avoid if you run javascript across different domains like if you execute JS on http://testpage.com and access http://anotherpage.com
// CSRF(Cross-Site Request Forgery)

SessionCreationPolicy.STATELESS is used to specify that Spring Security does not create or access a session.

/api/auth/signup/** This line tells spring security to permit all request without any authentication that starts with this url.

I use Bcrypt to hash passwords. In practice, we don’t store plain text passwords into database.

@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
return authenticationManagerBuilder.build();
}

We need to create a Bean of AuthenticationManager which will be used to authenticate the user. We passed our CustomUserDetailsService object & password encoding object to AuthenticationManager.

If you need to ckeck if the user is authenticated:

authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.email(), request.password()));

This authenticates the user with email & password. The authenticationManager.authenticate() method will internally call loadUserByUsername() method from our CustomUserDetailsService class. Then it will match the password from userDetailsService with the password found from LoginRequest. This method will throw exception if the authentication is not successful.

Now build & run the application. Send a POST request from Postman to /api/auth/signup api.

Then login:

Now we have successfully created a JWT token and sent back to user. Now if we want to access any other resource, we will get a 403 Forbidden error which means the request is not authenticated.

Adding JwtAuthorizationFilter

We need to add a JWT authorization filter for each request. This filter will block all requests that don’t have JWT token in the request header.

private final JwtAuthFilter jwtAuthFilter;

public SecurityConfig(UserDetailsServiceImpl userDetailsService, JwtAuthFilter jwtAuthFilter) {
this.userDetailsService = userDetailsService;
this.jwtAuthFilter = jwtAuthFilter;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
return http
.cors(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.POST, "/api/auth/signup/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/auth/login/**").permitAll()
.requestMatchers(HttpMethod.GET, "/authentication-docs/**").permitAll()
.anyRequest().authenticated())
.authenticationManager(authenticationManager)

// Add JWT token filter
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}

Please note that we added the JwtTokenFilter before the Spring Security internal UsernamePasswordAuthenticationFilter. We’re doing this because we need access to the user identity at this point to perform authentication/authorization.

UsernamePasswordAuthenticationFilter: Tries to find a username/password request parameter from POST login API and if found, create a UsernamePasswordAuthenticationToken from a username and password then tries to authenticate the user.

To implement JwtAuthFilter we need to extend OncePerRequestFilter since we need every request to be authenticated before going through spring security filter.

package com.mina.authentication.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mojang.authentication.controller.dto.ApiErrorResponse;
import com.mojang.authentication.exceptions.AccessDeniedException;
import com.mojang.authentication.helper.JwtHelper;
import com.mojang.authentication.service.UserDetailsServiceImpl;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

private final UserDetailsServiceImpl userDetailsService;
private final ObjectMapper objectMapper;

public JwtAuthFilter(UserDetailsServiceImpl userDetailsService, ObjectMapper objectMapper) {
this.userDetailsService = userDetailsService;
this.objectMapper = objectMapper;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String authHeader = request.getHeader("Authorization");
String token = null;
String username = null;
if (authHeader != null && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
username = JwtHelper.extractUsername(token);
}
// If the accessToken is null. It will pass the request to next filter in the chain.
// Any login and signup requests will not have jwt token in their header, therefore they will be passed to next filter chain.
if (token == null) {
filterChain.doFilter(request, response);
return;
}
// If any accessToken is present, then it will validate the token and then authenticate the request in security context
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (JwtHelper.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, null);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}

filterChain.doFilter(request, response);
} catch (AccessDeniedException e) {
ApiErrorResponse errorResponse = new ApiErrorResponse(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write(toJson(errorResponse));
}
}

private String toJson(ApiErrorResponse response) {
try {
return objectMapper.writeValueAsString(response);
} catch (Exception e) {
return ""; // Return an empty string if serialization fails
}
}
}

Now to access resources, include an Authorization header

You can find the source code here: https://github.com/minarashidi/authentication

I hope this post helped you to gain better understanding of the different authentication mechanisms and how to work with spring security.

I’m eager to hear your thoughts and comments. Feel free to share your insights or ask any questions you may have.

Thank you for reading!

--

--

Mina

Software Engineer with a passion for technology | Distributed Systems | Microservices | AWS https://www.linkedin.com/in/minarashidi/