The Spring Security family of libraries helps developers secure their Java applications with minimal effort. In this tutorial, you’ll learn how to implement an authorization server used for REST API security using the Spring Security OAuth2 Authorization Server library. The code examples and implementation will include the following features:
The following code examples were tested using the key technologies listed below:
1. Why Persistent Client Credentials?
2. Database Design
3. Create New Spring Starter Project
4. Enable Spring Authorization Server
5. Code Data Access Layer
6. Implement Registered Client Repository
7. Test Get Access Token Endpoint
8. Customize Access Token Generation
Let's get started.
According to the OAuth 2.0 Client Credentials grant type, clients must provide a valid client ID and client secret to obtain access tokens. In real-world applications, storing these credentials in a persistent storage solution—such as a database—is essential. This allows clients to be registered in real time and manage their credentials dynamically.
Moreover, persisting client credentials in a database enables flexible management, such as adding, disabling, or removing credentials as needed. This approach enhances both the security and scalability of the system.
To store client credentials in a relational database, we need a table named clients, which should include at least the following columns:
The picture below illustrates the structure of this table, including the data types for each column:
The data in this table should be managed by a separate application (e.g., an API Client Manager), while the authorization server will use it soley for authentication purposes.
Now, let’s create a new database schema named rest_api_tests for the sample project we’re going to build below. The table will be automatically created by the application at startup.
In your favorite Java IDE, create a new Spring Starter project with the following details:
And choose the following dependencies for the project besides Spring Boot:
For your reference, below is the XML code of the dependencies used in the project:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-authorization-server</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-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies>
Note that the Spring Test and Spring Security Test dependencies are added automatically by Spring Initializr.
Next, let’s create a new Spring configuration class to enable the authorization server, starting with the following initial code:
package net.codejava.oauth2; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; import org.springframework.security.web.SecurityFilterChain; @Configuration public class AuthorizationServerConfig { @Bean SecurityFilterChain authorizationServerFilterChain(HttpSecurity http) throws Exception { http.with(OAuth2AuthorizationServerConfigurer.authorizationServer(), Customizer.withDefaults()); return http.build(); } }
This code snippet activates the authorization server with default security settings for the application: all requests must be authenticated, except for the /oauth2/token endpoint, which handles client requests for obtaining new access tokens.
The server will not start until a RegisteredClientRepository is configured, which we’ll set up after implementing the data access layer.
Since we’re using aMySQL database to store client credentials, we need to update the application configuration file (application.properties) by specifying the necessary datasource properties. This allows Spring Data JPA/Hibernate to connect to the MySQL server. Add the following properties:
spring.datasource.url=jdbc:mysql://localhost:3306/rest_api_tests spring.datasource.username=root spring.datasource.password=password
Remember to update the database URL, username and password to match your MySQL server configuration. Also, add the following properties to enable Hibernate to generate database table from Java entity class (forward engineering) and to print SQL statements in the console for easier debugging and testing:
spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true
Next, we need to code a Java entity class that maps to the clients table using JPA annotations. The code is as follows:
package net.codejava.oauth2; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; @Entity @Table(name = "clients") public class Client { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String clientName; private String clientId; private String clientSecret; private String scope; public static Client clientId(String cid) { Client newClient = new Client(); newClient.setClientId(cid); return newClient; } public Client name(String name) { this.clientName = name; return this; } public Client scope(String scope) { this.scope = scope; return this; } // getters and setters are not shown for brevity }
And define the corresponding JPA repository as follows:
package net.codejava.oauth2; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface ClientRepository extends JpaRepository<Client, Long> { Optional<Client> findByClientId(String clientId); }
Spring Data JPA will generate a proxy object that implements the findByClientId() method, which will be used by the server to authenticate clients.
Use the following test class to perform unit tests:
package net.codejava.oauth2; import static org.assertj.core.api.Assertions.assertThat; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.test.annotation.Rollback; @DataJpaTest @AutoConfigureTestDatabase(replace = Replace.NONE) @Rollback(false) public class ClientRepositoryTests { @Autowired private ClientRepository repo; @Test public void testAddClients() { Client client1 = Client.clientId("client-1").name("John Doe").scope("read"); Client client2 = Client.clientId("client-2").name("Max One").scope("read"); Client client3 = Client.clientId("client-3").name("Devi Kumar").scope("write"); Client client4 = Client.clientId("client-4").name("Bob Kai").scope("write"); PasswordEncoder encoder = new BCryptPasswordEncoder(); client1.setClientSecret(encoder.encode("pass1")); client2.setClientSecret(encoder.encode("pass2")); client3.setClientSecret(encoder.encode("pass3")); client4.setClientSecret(encoder.encode("pass4")); repo.saveAll(List.of(client1, client2, client3, client4)); } @Test public void testFindByClientId() { String clientId = "client-3"; Optional<Client> result = repo.findByClientId(clientId); assertThat(result).isPresent(); } }
Run the testAddClients() method to persist four client credentials into the database. We’ll use this dummy data later to test the Get Access Token API. As you can see, we use BCryptPasswordEncoder to encode the client secrets.
Also, run the testFindByClientId() method to verify that a client can be retrieved by a given client ID.
Spring Boot REST APIs Ultimate Course
Hands-on REST API Development with Spring Boot: Design, Implement, Document, Secure, Test, Consume RESTful APIs
The next step is to configure a RegisteredClientRepository bean that uses the ClientRepository interface. This allows the server to authenticate clients by validating their credentials against the database. Add the following code to the AuthorizationServerConfig class:
@Bean public RegisteredClientRepository registeredClientRepository(ClientRepository clientRepo) { return new RegisteredClientRepository() { @Override public void save(RegisteredClient registeredClient) { } @Override public RegisteredClient findById(String id) { return null; } @Override public RegisteredClient findByClientId(String clientId) { Optional<Client> findResult = clientRepo.findByClientId(clientId); if (findResult.isEmpty()) return null; Client client = findResult.get(); return RegisteredClient.withId(client.getId().toString()) .clientId(client.getClientId()) .clientSecret(client.getClientSecret()) .scope(client.getScope()) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) .build(); } }; }
This code requires some explanation:
Additionally, we need to declare PasswordEncoder bean this configuration class, as shown below:
@Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
Spring Security will use this password encoder to verify the client secrets stored in the database, which are encoded using the BCrypt password encoder.
With that, the setup for the Spring authorization server is complete. Now we can start the application to test it out.
To obtain a new access token from the authorization server using the OAuth2.0 Client Credentials grant type, a client must send a request with the following details:
You can test this using curl with the following command pattern:
curl -d "grant_type=client_credentials&client-id=…&client-secret=…" localhost:8080/oauth2/token
For example, try the following command:
curl -d "grant_type=client_credentials&client_id=client-1&client_secret=abc" localhost:8080/oauth2/token -v
This command sends client_id as client-1 and client_secret as abc. The server will return an HTTP 401 Unauthorized status because the client secret is invalid, along with the following JSON in the response body:
{"error":"invalid_client"}
Now, let’s try making another request with valid a client id and secret, using a command like this:
curl -d "grant_type=client_credentials&client_id=client-1&client_secret=pass1" localhost:8080/oauth2/token -v | jq
The server will return an HTTP 200 OK status, along with a newly issued access token included in a JSON object in the response body, as shown below:
As you can see, the JSON response contains three fields:
If you decode the access token using an online tool, you will the decoded header as follows:
{ "kid": "77c7c6e3-ba27-4aa3-8389-d084cb9f4eeb", "alg": "RS256" }
And the decoded payload:
{ "sub": "client-1", "aud": "client-1", "nbf": 1743742740, "iss": "http://localhost:8080", "exp": 1743743040, "iat": 1743742740, "jti": "e33c35f5-7624-4c0f-972a-ef68856e1d0e" }
Read this article to learn more about the structure and meaning of the information contained in a JWT.
To test the Get Access Token API using Postman, create a new request with the following details:
Click the Send button, and you will see the following result:
You can then use the access token returned in the response to make API calls to protected resources on the server - for example, by including it in the HTTP Authorization request header.
However, the generated access token might not meet certain requirements, such as missing scope/authority information or having too short expiry time. In such cases, Spring authorization server allows you to customize the access token generation, as described below.
You can customize the claims included in the payload of access tokens issued to clients by declaring a bean of type OAuth2TokenCustomizer in your configuration class. For example:
@Bean public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() { return (context) -> { if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) { RegisteredClient client = context.getRegisteredClient(); Builder builder = context.getClaims(); builder.issuer("CodeJava"); builder.expiresAt(Instant.now().plus(10, ChronoUnit.MINUTES)); builder.claim("scope", client.getScopes()); } }; }
This code modifies the issued access token by changing the issuer to “CodeJava” (default is a URL), increasing the expiration time to 10 minutes, and adding the “scope” claim.
Test the Get Access Token API again and decode the new access token, you’ll see that the changes have been successfully applied:
{ "sub": "client-2", "aud": "client-2", "nbf": 1743746260, "scope": [ "read" ], "iss": "CodeJava", "exp": 1743746860, "iat": 1743746260, "jti": "54555db1-72a7-4eb0-a907-3b40502e1109" }
Read this article to learn more about access token customization with Spring authorization server.
Throughout this tutorial, you have learned how to implement a standard authorization server for securing REST APIs using the Spring Security OAuth2 Authorization Server library. Specifically, I showed you how to store client credentials in a relational database and configure Spring authorization server to authenticate the clients properly.
You also learned how to test the access token endpoint using both curl and Postman, as well as how to customize the generated JWTs to meet your specific requirements.
I hope you found the code examples and explanations in this guide helpful for building secure and reliable REST APIs. To watch the coding in action, watch the following video: