Securing a REST API Created With Spring Boot 3 Using Spring Security with username-password And JWT Authentication

İbrahim Gündüz
5 min readMay 27, 2024

--

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.

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.

Create a configuration class like the following one to declare the access rules and required Java beans.

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.

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.

Create an instance of JwtUtil with the secret key specified in the configuration and register as a bean.

Create A Login Endpoint

Create the following request and response models.

Create a login controller to handle the login requests.

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.

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.

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.

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 to read.

You can find the full source code of the example application on Github.

Thanks for reading!

Credits

--

--