DEV Community

Cover image for Implementing Token-Based Authentication in Spring Boot Using Spring Security, JWT, and JDBC Template
Sumukha B S
Sumukha B S

Posted on

Implementing Token-Based Authentication in Spring Boot Using Spring Security, JWT, and JDBC Template

Github Link for the Code Reference

Introduction

In modern web applications, secure user authentication is crucial. Traditionally, session-based authentication has been widely used, but as applications become more distributed and scalable, token-based authentication offers several advantages.

Token-based authentication allows applications to be stateless, meaning that the server doesn’t need to store any session data, making it well-suited for scalable, RESTful APIs.

This tutorial will guide you through implementing JWT (JSON Web Token) authentication in a Spring Boot application using Spring Security with JDBC Template.

JWT(JSON Web Token) is a compact, URL-safe way of representing claims transferred between two parties. It's commonly used for stateless authentication where each request is authenticated using a signed token.

Why JWT and Token-Based Authentication?

Stateless Authentication
JWT tokens are self-contained, carrying the user’s authentication information directly within the token payload, which reduces server memory usage and increases scalability.

Cross-Platform Support
Tokens are easy to use in mobile and web applications since they can be stored securely in the client (e.g., in local storage or cookies).

Security
Each token is digitally signed, ensuring its integrity and allowing the server to verify it without querying a database on every request.

What You Will Learn

In this tutorial, you will learn how to:

  1. Set up a Spring Boot application with Spring Security.
  2. Implement JWT token-based authentication using the JDBC Template to manage users and store refresh tokens securely.
  3. Set up endpoints for login, access token generation, and refresh token handling.

By the end of this tutorial, you’ll have a secure, stateless authentication system that leverages Spring Boot and JWT to provide seamless and scalable access control for your applications.


API Flow:

API Floe


Technology requirements:

  • Java 17 / 11 / 8
  • Spring Boot 3 / 2 (with Spring Security, Spring Web)
  • jjwt-api 0.11.5
  • PostgreSQL/MySQL
  • Maven

1. Set Up Your Spring Boot Project

Use Spring web tool or your development tool (STS, Intellij or any IDE) to create a Spring Boot project.

open pom.xml and add dependencies for Spring Security, JWT, and JDBC Template:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

2. Configure Database, App properties

Under src/main/resources folder, open application.properties and add below configurations. I will be using postgres database for this tutorial.

spring.application.name= authmanager

server.port= 1001
servlet.context-path= /authmanager

database.username= postgres 
database.password= admin@123
database.driverClassName= org.postgresql.Driver
database.jdbcUrl= jdbc:postgresql://localhost:5432/postgres
database.maxActive= 5
database.minIdle= 5
database.poolName= Authmanager Postgres Datasource

app.jwtSecret= ###############ib-Spring###############
app.jwtExpirationMs= 3600000
app.jwtRefreshExpirationMs= 86400000
Enter fullscreen mode Exit fullscreen mode

3. Set Up Database Tables

Define a simple table structure for user information, roles, user-role mapping and refresh tokens:

CREATE SCHEMA IB;
-------------------------------------------------------------------------

create sequence users_uniqueid_seq START 1;

create table ib.users(
        uniqueid bigint not null default nextval('users_uniqueid_seq') PRIMARY KEY,
        email varchar(75),
        password varchar(200),
        username varchar(20)
);

insert into ib.users(email,password,username) values ('admin@ib.com','$2a$10$VcdzH8Q.o4KEo6df.XesdOmXdXQwT5ugNQvu1Pl0390rmfOeA1bhS','admin');

#(password = 12345678)
-------------------------------------------------------------------------

create sequence roles_id_seq START 1;

create table ib.roles(
        id int not null default nextval('roles_id_seq') PRIMARY KEY,
        name varchar(20)
);

INSERT INTO ib.roles(name) VALUES('ROLE_USER');
INSERT INTO ib.roles(name) VALUES('ROLE_MODERATOR');
INSERT INTO ib.roles(name) VALUES('ROLE_ADMIN');

-------------------------------------------------------------------------

create table ib.user_roles(
        user_uniqueid bigint not null,
        role_id int not null,
        primary key(user_uniqueid,role_id)
);
insert into ib.user_roles (user_uniqueid,role_id) values (1,3);
-------------------------------------------------------------------------

create sequence refresh_tokens_id_seq START 1;

create table ib.refresh_tokens(
        id bigint not null default nextval('refresh_tokens_id_seq') PRIMARY KEY,
        uniqueid bigint,
        token varchar(500) not null,
        expiryDate TIMESTAMP WITH TIME ZONE not null
);

-------------------------------------------------------------------------
Enter fullscreen mode Exit fullscreen mode

4. Create the models

Let’s define the following models.
In models package, create below 4 files:

model/ERole.java

package com.security.authmanager.model;

public enum ERole {
    ROLE_USER,
    ROLE_MODERATOR,
    ROLE_ADMIN
}
Enter fullscreen mode Exit fullscreen mode

model/Role.java

package com.security.authmanager.model;

public class Role {
    private Integer id;
    private ERole name;

    public Role() {
    }

    public Role(ERole name) {
        this.name = name;
    }

    //generate getters and setters
}
Enter fullscreen mode Exit fullscreen mode

model/User.java

package com.security.authmanager.model;

import java.util.HashSet;
import java.util.Set;

public class User {
    private Long id;
    private String username;
    private String email;
    private String password;

    private Set<Role> roles = new HashSet<>();

    public User() {
    }

    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }

    //generate getters and setters
}

Enter fullscreen mode Exit fullscreen mode

model/RefreshToken.java

package com.security.authmanager.model;

import java.util.HashSet;
import java.util.Set;

public class User {
    private Long id;
    private String username;
    private String email;
    private String password;

    private Set<Role> roles = new HashSet<>();

    public User() {
    }

    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }

    //generate getters and setters
}

Enter fullscreen mode Exit fullscreen mode

5. Implement CustomUserDetailsRepository class

The CustomUserDetailsRepository class is a Spring @Repository that handles custom database operations related to User and Role entities. It uses JdbcTemplate to execute SQL queries for tasks like fetching users, checking if a user exists by username or email, creating new users, and fetching roles.

package com.security.authmanager.repository;

import com.security.authmanager.common.QueryConstants;
import com.security.authmanager.model.ERole;
import com.security.authmanager.model.Role;
import com.security.authmanager.model.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

@Repository
public class CustomUserDetailsRepository {

    private static final Logger logger = LoggerFactory.getLogger(CustomUserDetailsRepository.class);
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public User fetchUserByUserName(String userName){
        try{
            return jdbcTemplate.query((conn) ->{
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.FETCH_USER);
                ps.setString(1, userName.toUpperCase());
                return ps;
            },rs->{
                User user = null;
                Set<Role> roles = new HashSet<>();
                while (rs.next()) {
                    if (user == null) {
                        user = new User();
                        user.setEmail(rs.getString("email"));
                        user.setId(rs.getLong("uniqueid"));
                        user.setPassword(rs.getString("password"));
                        user.setUsername(rs.getString("username"));
                    }
                    Role role = new Role();
                    role.setId(rs.getInt("id"));
                    role.setName(ERole.valueOf(rs.getString("name")));
                    roles.add(role);
                }
                if (user != null) {
                    user.setRoles(roles);
                }
                return user;
            });
        }catch(Exception e){
            logger.error("Exception in fetchUserByUserName()",e);
            throw new RuntimeException(e);
        }
    }

    public boolean existsByUsername(String userName) {
        try{
              return jdbcTemplate.query((conn) -> {
                 final PreparedStatement ps = conn.prepareStatement(QueryConstants.CHECK_USER_BY_USERNAME);
                 ps.setString(1, userName.toUpperCase());
                 return ps;
             }, (rs,rownum) -> rs.getInt("count")).get(0)>0;
        }catch(Exception e){
            logger.error("Exception in existsByUsername()",e);
            throw new RuntimeException(e);
        }
    }

    public boolean existsByEmail(String email) {
        try{
            return jdbcTemplate.query((conn) -> {
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.CHECK_USER_BY_EMAIL);
                ps.setString(1, email.toUpperCase());
                return ps;
            }, (rs,rownum) -> rs.getInt("count")).get(0)>0;
        }catch(Exception e){
            logger.error("Exception in existsByEmail()",e);
            throw new RuntimeException(e);
        }
    }

    public Role findRoleByName(ERole eRole) {
        try{
            return jdbcTemplate.query((conn) -> {
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.FETCH_ROLE_BY_NAME);
                ps.setString(1, String.valueOf(eRole));
                return ps;
            }, rs -> {
                Role role=null;
                while(rs.next()){
                    role = new Role();
                    role.setName(ERole.valueOf(rs.getString("name")));
                    role.setId(rs.getInt("id"));
                }
                return role;
            });
        }catch(Exception e){
            logger.error("Exception in findRoleByName()",e);
            throw new RuntimeException(e);
        }
    }

    public void createUser(User user) {
        try(Connection conn = Objects.requireNonNull(jdbcTemplate.getDataSource()).getConnection()){
            try (PreparedStatement userStatement = conn.prepareStatement(QueryConstants.INSERT_TO_USERS,Statement.RETURN_GENERATED_KEYS)) {
                userStatement.setString(1, user.getEmail().toUpperCase());
                userStatement.setString(2, user.getPassword());
                userStatement.setString(3, user.getUsername().toUpperCase());
                userStatement.executeUpdate();
                // Retrieve generated userId
                try (ResultSet generatedKeys = userStatement.getGeneratedKeys()) {
                    if (generatedKeys.next()) {
                        Long userId = generatedKeys.getLong(1); // Assuming userId is of type VARCHAR
                        logger.info("gen userid {}",userId.toString());
                        user.setId(userId);
                    }
                }
            }
            if (user.getRoles() != null && !user.getRoles().isEmpty()) {
                try (PreparedStatement userRoleStatement = conn.prepareStatement(QueryConstants.INSERT_TO_USER_ROLES)) {
                    for (Role role : user.getRoles()) {
                        userRoleStatement.setLong(1, user.getId());
                        userRoleStatement.setLong(2, role.getId());
                        userRoleStatement.executeUpdate();
                    }
                }
            }
        }catch(Exception e){
            logger.error("Exception in existsByEmail()",e);
            throw new RuntimeException(e);
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

This repository performs custom SQL-based CRUD operations for managing User and Role data in the database.

Key Functions:

  • Fetch users by username and retrieve associated roles.
  • Check if a user exists by username or email.
  • Insert new users and their roles into the database.
  • Retrieve role details based on role names.

6. Implement RefreshTokenRepository class

The RefreshTokenRepository class is a Spring @Repository that handles database operations related to the RefreshToken entity. It uses Spring's JdbcTemplate for interacting with the database through raw SQL queries, encapsulating the logic for saving, deleting, and retrieving refresh tokens.

package com.security.authmanager.repository;

import com.security.authmanager.common.QueryConstants;
import com.security.authmanager.model.ERole;
import com.security.authmanager.model.RefreshToken;
import com.security.authmanager.model.Role;
import com.security.authmanager.model.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.sql.PreparedStatement;
import java.sql.Timestamp;
import java.util.Optional;

@Repository
public class RefreshTokenRepository {
    private static final Logger logger = LoggerFactory.getLogger(RefreshTokenRepository.class);
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void deleteRefreshToken(RefreshToken refreshToken) {
        try{
            jdbcTemplate.update(QueryConstants.DELETE_REFRESH_TOKEN,(final PreparedStatement ps) ->{
                ps.setString(1,refreshToken.getToken());
            });
        }catch (Exception e){
            logger.error("Exception in deleteRefreshToken()",e);
            throw new RuntimeException(e);
        }
    }

    public int deleteRefreshTokenByUser(User user) {
        return 0;
    }

    public RefreshToken saveRefreshToken(RefreshToken refreshToken) {
        try{
            jdbcTemplate.update(QueryConstants.SAVE_REFRESH_TOKEN,(final PreparedStatement ps) ->{
                ps.setLong(1,refreshToken.getUser().getId());
                ps.setString(2,refreshToken.getToken());
                ps.setTimestamp(3, Timestamp.from(refreshToken.getExpiryDate()));
            });
        }catch (Exception e){
            logger.error("Exception in saveRefreshToken()",e);
            throw new RuntimeException(e);
        }
        return refreshToken;
    }

    public Optional<RefreshToken> findByToken(String token) {
        RefreshToken refreshToken = new RefreshToken();
        try{
            return Optional.ofNullable(jdbcTemplate.query((conn) -> {
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.FIND_BY_TOKEN);
                ps.setString(1, token);
                return ps;
            }, rs -> {
                User user = new User();
                while (rs.next()) {
                    refreshToken.setId(rs.getLong("id"));
                    refreshToken.setToken(rs.getString("token"));
                    refreshToken.setExpiryDate(rs.getTimestamp("expiryDate").toInstant());
                    user.setId(rs.getLong("uniqueid"));
                    user.setEmail(rs.getString("email"));
                    user.setUsername(rs.getString("username"));
                    refreshToken.setUser(user);
                }
                return refreshToken;
            }));
        }catch(Exception e){
            logger.error("Exception in findByToken()",e);
            throw new RuntimeException(e);
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

This repository interacts directly with the database for CRUD operations on RefreshToken entities.

Key Functions:

  • Save refresh tokens.
  • Retrieve refresh tokens by token value.
  • Delete refresh tokens either by token or user.

7. Configure Spring Security

  • WebSecurityConfig configures Spring Security to use JWT-based token authentication.

  • It defines beans for various components needed for authentication like AuthTokenFilter (for handling JWT tokens), DaoAuthenticationProvider (for retrieving user details and validating passwords), and BCryptPasswordEncoder (for hashing and comparing passwords).

The SecurityFilterChain configures how incoming HTTP requests are secured:
- Permits access to certain public routes (/auth/**, /test/**).
- Protects all other routes, requiring authentication.
- Disables session management (making the system stateless).
- Configures a filter to intercept and process JWT tokens for
user authentication.

package com.security.authmanager.security;

import com.security.authmanager.security.jwt.AuthEntryPointJwt;
import com.security.authmanager.security.jwt.AuthTokenFilter;
import com.security.authmanager.security.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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
@EnableMethodSecurity
public class WebSecurityConfig {
    @Autowired
    UserDetailsServiceImpl userDetailsService;

    @Autowired
    private AuthEntryPointJwt unauthorizedHandler;

    @Bean
    public AuthTokenFilter authenticationJwtTokenFilter() {
        return new AuthTokenFilter();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();

        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());

        return authProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }

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

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable())
                .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth ->
                        auth.requestMatchers("/api/v1.0/auth/**").permitAll()
                                .requestMatchers("/api/v1.0/test/**").permitAll()
                                .anyRequest().authenticated()
                );

        http.authenticationProvider(authenticationProvider());

        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

Enter fullscreen mode Exit fullscreen mode

8. Implement UserDetailsImpl

This class is primarily used in Spring Security to represent the currently authenticated user.

When a user tries to log in:

  1. Spring Security calls UserDetailsService.loadUserByUsername() to load the user from the database.
  2. It creates an instance of UserDetailsImpl with the user's details (including their roles).
  3. This UserDetailsImpl object is then used by Spring Security to authenticate the user and to check their permissions throughout the session.

UserDetailsImpl is a bridge between your User entity and Spring Security's internal mechanisms for authentication and authorization.

package com.security.authmanager.security.service;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.security.authmanager.model.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

public class UserDetailsImpl implements UserDetails {

    private static final long serialVersionUID = 1L;
    private Long id;
    private String username;
    private String email;
    @JsonIgnore
    private String password;
    private Collection<? extends GrantedAuthority> authorities;

    public UserDetailsImpl(Long id, String username, String email, String password,
                           Collection<? extends GrantedAuthority> authorities) {
        this.id = id;
        this.username = username;
        this.email = email;
        this.password = password;
        this.authorities = authorities;
    }

    public static UserDetailsImpl build(User user) {
        List<GrantedAuthority> authorities = user.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority(role.getName().name()))
                .collect(Collectors.toList());

        return new UserDetailsImpl(
                user.getId(),
                user.getUsername(),
                user.getEmail(),
                user.getPassword(),
                authorities);
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    public Long getId() {
        return id;
    }

    public String getEmail() {
        return email;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;
        UserDetailsImpl user = (UserDetailsImpl) o;
        return Objects.equals(id, user.id);
    }
}

Enter fullscreen mode Exit fullscreen mode

Below are the queries : (QueryConstants.java)

public class QueryConstants {

    public static final String FETCH_USER = "SELECT u.uniqueid, u.username, u.email, u.password, r.id, r.name FROM ib.users u LEFT JOIN ib.user_roles ur ON u.uniqueid = ur.user_uniqueid LEFT JOIN ib.roles r ON ur.role_id = r.id WHERE UPPER(u.username) = ?";
    public static final String CHECK_USER_BY_USERNAME = "SELECT count(*) as count from ib.users where upper(username)=?";
    public static final String CHECK_USER_BY_EMAIL = "SELECT count(*) as count from ib.users where upper(email)=?";
    public static final String FETCH_ROLE_BY_NAME = "SELECT id,name from ib.roles where UPPER(name)=?";
    public static final String INSERT_TO_USERS = "INSERT INTO ib.users (email,password,username) values (?,?,?)";
    public static final String INSERT_TO_USER_ROLES = "INSERT INTO ib.user_roles (user_uniqueid,role_id) VALUES (?,?)";
    public static final String SAVE_REFRESH_TOKEN = "INSERT INTO ib.refresh_tokens (uniqueid, token, expiryDate) VALUES (?, ?, ?)";
    public static final String DELETE_REFRESH_TOKEN = "DELETE FROM ib.refresh_tokens WHERE token = ?";
    public static final String FIND_BY_TOKEN = "SELECT rt.id,rt.token,rt.expiryDate,us.uniqueid,us.email,us.username from ib.refresh_tokens rt inner join ib.users us on rt.uniqueid=us.uniqueid where rt.token=?";
}
Enter fullscreen mode Exit fullscreen mode

9. Implement UserDetailsServiceImpl

The UserDetailsServiceImpl class acts as a bridge between your application's database and Spring Security’s authentication process. It fetches user details from the database using CustomUserDetailsRepository, converts the User object into UserDetailsImpl (a Spring Security-friendly format), and handles cases where a user is not found by throwing an exception. This service allows Spring Security to authenticate users and manage authorization based on the user's roles and permissions.

package com.security.authmanager.security.service;

import com.security.authmanager.model.User;
import com.security.authmanager.repository.CustomUserDetailsRepository;
import org.springframework.beans.factory.annotation.Autowired;
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.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    CustomUserDetailsRepository userDetailsRepository;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userDetailsRepository.fetchUserByUserName(username);
        if(user==null){
            throw new UsernameNotFoundException("User Not Found with username: " + username);
        }else {
            return UserDetailsImpl.build(user);
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

10. Filter the Request

The AuthTokenFilter class extends Spring's OncePerRequestFilter, making it a filter that processes every HTTP request once in a request chain. Its primary role is to extract and validate a JWT (JSON Web Token) from the request and set the user’s authentication in Spring Security's SecurityContext if the token is valid.

package com.security.authmanager.security.jwt;

import com.security.authmanager.security.service.UserDetailsServiceImpl;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class AuthTokenFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            String jwt = parseJwt(request);
            if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
                String username = jwtUtils.getUserNameFromJwtToken(jwt);

                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(
                                userDetails,
                                null,
                                userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception e) {
            logger.error("Cannot set user authentication: {}", e);
        }

        filterChain.doFilter(request, response);
    }

    private String parseJwt(HttpServletRequest request) {
        String headerAuth = request.getHeader("Authorization");

        if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
            return headerAuth.substring(7);
        }

        return null;
    }
}

Enter fullscreen mode Exit fullscreen mode

Each time a request is made:

  • The filter checks if the request contains a valid JWT.
  • If valid, it authenticates the user by setting the UsernamePasswordAuthenticationToken in the SecurityContext.
  • If the JWT is invalid or missing, no authentication is set, and the request proceeds as an unauthenticated request.

This filter ensures that all requests carrying valid JWT tokens are authenticated automatically, without the need for the user to provide credentials (like username/password) in subsequent requests after logging in.

11. Implement RefreshTokenService

The RefreshTokenService class provides services related to managing refresh tokens in a token-based authentication system. Refresh tokens are used to obtain new JWT tokens after the initial JWT has expired without requiring the user to re-authenticate.

package com.security.authmanager.security.service;

import com.security.authmanager.exception.TokenRefreshException;
import com.security.authmanager.model.RefreshToken;
import com.security.authmanager.model.User;
import com.security.authmanager.repository.CustomUserDetailsRepository;
import com.security.authmanager.repository.RefreshTokenRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.util.Optional;
import java.util.UUID;

@Service
public class RefreshTokenService {
    @Value("${app.jwtRefreshExpirationMs}")
    private Long refreshTokenDurationMs;

    @Autowired
    private RefreshTokenRepository refreshTokenRepository;

    @Autowired
    private CustomUserDetailsRepository userRepository;

    public Optional<RefreshToken> findByToken(String token) {
        return refreshTokenRepository.findByToken(token);
    }

    public RefreshToken createRefreshToken(String userName) {
        RefreshToken refreshToken = new RefreshToken();
        refreshToken.setUser(userRepository.fetchUserByUserName(String.valueOf(userName)));
        refreshToken.setExpiryDate(Instant.now().plusMillis(refreshTokenDurationMs));
        refreshToken.setToken(UUID.randomUUID().toString());

        refreshToken = refreshTokenRepository.saveRefreshToken(refreshToken);
        return refreshToken;
    }

    public RefreshToken verifyExpiration(RefreshToken token) {
        if (token.getExpiryDate().compareTo(Instant.now()) < 0) {
            refreshTokenRepository.deleteRefreshToken(token);
            throw new TokenRefreshException(token.getToken(), "Refresh token was expired. Please make a new signin request");
        }

        return token;
    }

    @Transactional
    public int deleteByUserId(Long userId) {
        return refreshTokenRepository.deleteRefreshTokenByUser(userRepository.fetchUserByUserName(String.valueOf(userId)));
    }
}

Enter fullscreen mode Exit fullscreen mode

RefreshTokenService handles the creation, validation, and deletion of refresh tokens. It uses repositories for saving and fetching tokens and users from the database.

This service is an essential part of an authentication system where refresh tokens are used to keep users logged in without requiring them to provide credentials again after the JWT expires.

12. Implement JWT Utility

The JwtUtils class is a utility class that handles the creation, parsing, and validation of JWT (JSON Web Token) for authentication purposes in a Spring Boot application. It uses the jjwt library to work with JWTs.

package com.security.authmanager.security.jwt;

import com.security.authmanager.security.service.UserDetailsImpl;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.*;
import io.jsonwebtoken.*;

import java.security.Key;
import java.util.Date;

@Component
public class JwtUtils {
    private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);

    @Value("${app.jwtSecret}")
    private String jwtSecret;

    @Value("${app.jwtExpirationMs}")
    private int jwtExpirationMs;

    public String generateJwtToken(Authentication authentication) {

        UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();

        return Jwts.builder()
                .setSubject((userPrincipal.getUsername()))
                .setIssuedAt(new Date())
                .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
                .signWith(SignatureAlgorithm.HS256, key())
                .compact();
    }

    private Key key() {
        return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
    }

    public String generateTokenFromUsername(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
                .signWith(SignatureAlgorithm.HS256, key())
                .compact();
    }

    public String getUserNameFromJwtToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key()).build()
                .parseClaimsJws(token).getBody().getSubject();
    }

    public boolean validateJwtToken(String authToken) {
        try {
            Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken);
            return true;
        } catch (MalformedJwtException e) {
            logger.error("Invalid JWT token: {}", e.getMessage());
        } catch (ExpiredJwtException e) {
            logger.error("JWT token is expired: {}", e.getMessage());
        } catch (UnsupportedJwtException e) {
            logger.error("JWT token is unsupported: {}", e.getMessage());
        } catch (IllegalArgumentException e) {
            logger.error("JWT claims string is empty: {}", e.getMessage());
        }

        return false;
    }

}

Enter fullscreen mode Exit fullscreen mode

The JwtUtils class is responsible for generating, parsing, and validating JWT tokens. It securely signs tokens using a secret key (HMAC-SHA256) and ensures that the tokens can only be read or verified by parties that possess the correct secret key.

The class also extracts the username from the token and checks if the token is valid before granting access to the user. This utility is essential for maintaining secure, token-based authentication in your application.

13. Handle Authentication Exception

The AuthEntryPointJwt class implements Spring Security's AuthenticationEntryPoint interface. It handles what happens when an unauthorized request is made, typically when a user tries to access a protected resource without valid authentication (e.g., no JWT or an invalid JWT).

package com.security.authmanager.security.jwt;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {
    private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class);

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        logger.error("Unauthorized error: {}", authException.getMessage());

        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

        final Map<String, Object> body = new HashMap<>();
        body.put("status", HttpServletResponse.SC_UNAUTHORIZED);
        body.put("error", "Unauthorized");
        body.put("message", authException.getMessage());
        body.put("path", request.getServletPath());

        final ObjectMapper mapper = new ObjectMapper();
        mapper.writeValue(response.getOutputStream(), body);
    }
}

Enter fullscreen mode Exit fullscreen mode

The AuthEntryPointJwt class is a custom entry point that intercepts unauthorized access attempts and returns a structured JSON response with a 401 error code, an error message, and details about the request.
It logs the error and provides a clear, user-friendly response to clients when authentication fails.

14. Create payload class for Controller

Below are the payloads for our RestAPIs:

1. Requests:

- LoginRequest.java :

public class LoginRequest {
    @NotBlank
  private String username;
    @NotBlank
    private String password;

    //getters and setters
}
Enter fullscreen mode Exit fullscreen mode

- SignupRequest.java :

public class SignupRequest {
  @NotBlank
  @Size(min = 3, max = 20)
  private String username;

  @NotBlank
  @Size(max = 50)
  @Email
  private String email;

  private Set<String> role;

  @NotBlank
  @Size(min = 6, max = 40)
  private String password;

  //getters and setters
}
Enter fullscreen mode Exit fullscreen mode

- TokenRefreshRequest.java :

public class TokenRefreshRequest {
    @NotBlank
    private String refreshToken;

    //getters and setters
}
Enter fullscreen mode Exit fullscreen mode

2. Responses:

- JwtResponse.java

public class JwtResponse {
  private String token;
  private String type = "Bearer";
  private String refreshToken;
  private Long id;
  private String username;
  private String email;
  private List<String> roles;

  public JwtResponse(String accessToken, String refreshToken, Long id, String username, String email, List<String> roles) {
    this.token = accessToken;
    this.refreshToken = refreshToken;
    this.id = id;
    this.username = username;
    this.email = email;
    this.roles = roles;
  }

  //getters and setters
}
Enter fullscreen mode Exit fullscreen mode

- MessageResponse.java

public class MessageResponse {
  private String message;

  public MessageResponse(String message) {
    this.message = message;
  }

  //getters and setters
}
Enter fullscreen mode Exit fullscreen mode

- TokenRefreshResponse.java

public class TokenRefreshResponse {
    private String accessToken;
    private String refreshToken;
    private String tokenType = "Bearer";

    public TokenRefreshResponse(String accessToken, String refreshToken) {
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
    }

    //getters and setters
}
Enter fullscreen mode Exit fullscreen mode

15. Create Rest APIs Controller classes

- AuthController.java

The AuthController class is a Spring @RestController responsible for handling authentication-related endpoints in the application. It provides endpoints for user login, registration, and token refresh operations.

Key Functions:

  • User login with JWT and refresh token generation.
  • Token refresh using valid refresh tokens.
  • User registration with validation and role assignment.
package com.security.authmanager.controller;

import com.security.authmanager.exception.TokenRefreshException;
import com.security.authmanager.model.ERole;
import com.security.authmanager.model.RefreshToken;
import com.security.authmanager.model.Role;
import com.security.authmanager.model.User;
import com.security.authmanager.payloads.request.LoginRequest;
import com.security.authmanager.payloads.request.SignupRequest;
import com.security.authmanager.payloads.request.TokenRefreshRequest;
import com.security.authmanager.payloads.response.JwtResponse;
import com.security.authmanager.payloads.response.MessageResponse;
import com.security.authmanager.payloads.response.TokenRefreshResponse;
import com.security.authmanager.repository.CustomUserDetailsRepository;
import com.security.authmanager.security.jwt.JwtUtils;
import com.security.authmanager.security.service.RefreshTokenService;
import com.security.authmanager.security.service.UserDetailsImpl;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/v1.0/auth")
public class AuthController {
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    CustomUserDetailsRepository userRepository;

    @Value("${app.jwtSecret}")
    private String jwtSecret;

    @Autowired
    PasswordEncoder encoder;

    @Autowired
    JwtUtils jwtUtils;

    @Autowired
    RefreshTokenService refreshTokenService;

    @PostMapping("/login")
    public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {

        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));

        SecurityContextHolder.getContext().setAuthentication(authentication);
        String jwt = jwtUtils.generateJwtToken(authentication);

        UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
        List<String> roles = userDetails.getAuthorities().stream()
                .map(item -> item.getAuthority())
                .collect(Collectors.toList());

        RefreshToken refreshToken = refreshTokenService.createRefreshToken(userDetails.getUsername());

        return ResponseEntity.ok(new JwtResponse(jwt,
                refreshToken.getToken(),
                userDetails.getId(),
                userDetails.getUsername(),
                userDetails.getEmail(),
                roles));
    }

    @PostMapping("/refreshtoken")
    public ResponseEntity<?> refreshtoken(@Valid @RequestBody TokenRefreshRequest request) {
        String requestRefreshToken = request.getRefreshToken();

        return refreshTokenService.findByToken(requestRefreshToken)
                .map(refreshTokenService::verifyExpiration)
                .map(RefreshToken::getUser)
                .map(user -> {
                    String token = jwtUtils.generateTokenFromUsername(user.getUsername());
                    return ResponseEntity.ok(new TokenRefreshResponse(token, requestRefreshToken));
                })
                .orElseThrow(() -> new TokenRefreshException(requestRefreshToken,
                        "Refresh token is not in database!"));
    }

    @PostMapping("/signup")
    public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRequest) {
        if (userRepository.existsByUsername(signUpRequest.getUsername())) {
            return ResponseEntity
                    .badRequest()
                    .body(new MessageResponse("Error: Username is already taken!"));
        }

        if (userRepository.existsByEmail(signUpRequest.getEmail())) {
            return ResponseEntity
                    .badRequest()
                    .body(new MessageResponse("Error: Email is already in use!"));
        }

        // Create new user's account
        User user = new User(signUpRequest.getUsername().toLowerCase(),
                signUpRequest.getEmail().toUpperCase(),
                encoder.encode(signUpRequest.getPassword()));

        Set<String> strRoles = signUpRequest.getRole();
        Set<Role> roles = new HashSet<>();

        if (strRoles == null) {
            Role userRole = userRepository.findRoleByName(ERole.ROLE_USER);
            if(userRole==null) {
                throw new RuntimeException("Error: Role not found.");
            }else {
                roles.add(userRole);
            }
        } else {
            strRoles.forEach(role -> {
                switch (role) {
                    case "admin":
                        Role adminRole = userRepository.findRoleByName(ERole.ROLE_ADMIN);
                        if(adminRole==null) {
                            throw new RuntimeException("Error: Role not found.");
                        }
                        roles.add(adminRole);
                        break;
                    case "mod":
                        Role modRole = userRepository.findRoleByName(ERole.ROLE_MODERATOR);
                        if(modRole==null) {
                            throw new RuntimeException("Error: Role not found.");
                        }
                        roles.add(modRole);
                        break;
                    default:
                        Role userRole = userRepository.findRoleByName(ERole.ROLE_USER);
                        if(userRole==null) {
                            throw new RuntimeException("Error: Role not found.");
                        }
                        roles.add(userRole);
                }
            });
        }

        user.setRoles(roles);
        userRepository.createUser(user);

        return ResponseEntity.ok(new MessageResponse("User registered successfully!"));
    }
}

Enter fullscreen mode Exit fullscreen mode

- TestController.java

The TestController class is a Spring @RestController that provides several endpoints for testing access control based on user roles. It demonstrates how to use Spring Security's role-based authorization to restrict access to certain parts of the application.

Key Functions:

  • Public access endpoint that anyone can access.
  • Role-specific endpoints that restrict access based on user roles (USER, MODERATOR, ADMIN).
package com.security.authmanager.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/v1.0/test")
public class TestController {
    @GetMapping("/all")
    public String allAccess() {
        return "Public Content.";
    }

    @GetMapping("/user")
    @PreAuthorize("hasRole('USER') or hasRole('MODERATOR') or hasRole('ADMIN')")
    public String userAccess() {
        return "User Content.";
    }

    @GetMapping("/mod")
    @PreAuthorize("hasRole('MODERATOR')")
    public String moderatorAccess() {
        return "Moderator Board.";
    }

    @GetMapping("/admin")
    @PreAuthorize("hasRole('ADMIN')")
    public String adminAccess() {
        return "Admin Board.";
    }
}
Enter fullscreen mode Exit fullscreen mode

16. Test APIs

1. Register as mod and user. (Signin)

SignIn

2. Login to get access token.

Login

3. Get Refresh Token API.

Refresh Token

4. Testing the user access by passing the access token.

user

5. Testing the mod access by passing the access token.

mod

6. Testing the admin access by passing the same access token.

admin

Unauthorized since this user don't have admin access (user has only mod and user roles)

Happy learning! See you again.😀

Top comments (0)