A Spring Boot demo project that provides authentication and authorization through a Keycloak server.
- Configures and runs a Keycloak server in a Docker container
- Configures a Spring Boot application to use the Keycloak server for authentication and authorization
- Provides a REST API to authenticate and authorize users
- Provides a REST API to access a protected resource
- Provides a REST API to refresh the access token
- Java 17
- Docker
- Docker Compose
This section provides a step-by-step guide on how to run the project.
- Clone the repository by executing the following command:
git clone https://github.com/andrecaiado/spring-boot-oauth2-keycloak.git
- Navigate into the project directory:
cd your-repository-name
- Install the dependencies by executing the following command:
./mvnw clean install
- Run the application by executing the following command:
./mvnw spring-boot:run
The best practices recommend not to store sensitive information in the source code or other project files, e.g., the
.env
file.One usually follows the following steps to secure sensitive information contained in the
.env
file:
Add the
.env
file to the.gitignore
file to prevent it from being committed to the repository.Provide a
.env
template file with empty values and instructions on how to fill it.For the sake of simplicity of this example project and because some of the sensitive data is provided in the sbok-dev-realm.json, the above steps were not followed.
Of course, if you are going to use this project in a production environment, please follow the best practices and change the sensitive data accordingly.
When running the application, Keycloak will be available at http://localhost:8080/auth
. The default credentials are:
- Username:
admin
- Password:
password
For more information on configuring Keycloak server, please refer to the Keycloak Getting started guides.
This section describes the Keycloak configuration used in the project.
The configuration was first created manually in the Keycloak server and then exported to a JSON files that is loaded when the Keycloak server starts.
The exported file is the following: sbok-dev-realm.json
Note:
It's possible to export realm settings through the Keycloak admin console however, the users are not included in the exported file.
To export all settings, including the users, the following command was executed inside the Keycloak server container:
/opt/keycloak/bin/kc.sh export --dir /opt/keycloak/data/import --users realm_file --realm sbok-dev
The configurations contained in the exported file are:
- Realm:
sbok-dev
- Client:
sbok-auth-srv
- Realm roles:
admin
anduser
- Users and roles:
john
:- Username:
john
- Password:
password
- Realm roles assigned:
user
- Username:
admin
:- Username:
admin
- Password:
admin
- Realm roles assigned:
admin
- Username:
Solve the issue that impacts the userinfo endpoint
To solve the issue that impacts the userinfo endpoint (returning http status code 403), the following steps were taken:
- Access the Keycloak admin console
- Switch to the
sbok-dev
realm - Click on
Clients scopes
in left menu - Create a client scope with the following settings:
- Name:
openid
- Assign type:
Default
- Protocol:
OpenID Connect
- Name:
- Click on
Clients
in left menu and select thesbok-auth-srv
client in the list - Navigate to the
Client Scopes
tab - Add the
openid
client scope to the assigned scopes
The above steps were taken based on the following discussion:
These steps are also included in the exported Keycloak configuration file.
To automatically import the realm configuration when the Keycloak server starts, the following configuration was added to the docker-compose.yaml file:
keycloak:
...
command: "-v start-dev --import-realm"
...
volumes:
- ./sbok-dev-realm.json:/opt/keycloak/data/import/sbok-dev-realm.json
Access the OpenID Endpoint Configuration URL to get the necessary URLs to authenticate and authorize users.
The above URL can be found at the Realm settings page of the Keycloak server.
The URL to authenticate users is the token_endpoint
URL. In this project, it is http://localhost:8080/realms/sbok-dev/protocol/openid-connect/token.
Create a POST request to the token_endpoint
URL with the following parameters:
- Body type:
x-www-form-urlencoded
- Key-value pairs:
- grant_type:
password
- client_id:
sbok-auth-srv
- username:
john
- password:
password
- client_secret:
p201PFVJpK5a5jRkkKcwzYZvO7Yc0lUT
- grant_type:
The client_secret
can be found at the client details page, in the credentials tab, of the Keycloak server.
If the request is successful, the response will contain, among other fields, the access_token
.
This section describes the implementation of the project focused on the integration with Keycloak.
The security configuration is defined in the SecurityConfig class.
In the filter chain, we added the oauth2ResourceServer
so Spring Boot knows what resource server to use in order to validate the JWT token that will be received in the requests.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
...
http
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer
.jwt(Customizer.withDefaults())
);
...
return http.build();
}
}
The resource server properties are defined in the application.yml file:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${KEYCLOAK_URL}/realms/sbok-dev
jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs
Some properties are loaded from the .env file.
Once again, please refer to the OpenID Endpoint Configuration URL to get the necessary URIs.
This section describes how to implement role-based authorization in the project.
Enable the @PreAutorize
annotation by adding the @WebMethodSecurity
annotation in the SecurityConfig class:
@Configuration
@EnableWebSecurity
@EnableWebMethodSecurity
public class SecurityConfig {
...
}
Add the @PreAuthorize
annotation to the methods that need authorization:
@RestController
@RequestMapping("/api/v1/user")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/info")
@PreAuthorize("hasRole('user')")
public ResponseEntity<JsonNode> getUserInfo(@RequestHeader("Authorization") String bearerToken) {
bearerToken = bearerToken.replace("Bearer ", "");
return ResponseEntity.ok(userService.getUserInfo(bearerToken));
}
}
We need to implement a custom JwtAuthenticationConverter
to extract the roles from the JWT token so the @PreAuthorize annotation can work properly.
The roles are extracted from the realm_access
and resource_access
claims of the JWT token.
The JwtAuthenticationConverter
is defined in the JwtAuthConverter.java class.
The project provides the following endpoints:
/api/v1/auth/login
: Authenticates a user and returns the access token/api/v1/auth/refresh
: Refreshes the access token/api/v1/user/info
: Returns the user information (users with roleuser
)/api/v1/admin/info
: Returns a protected resource (users with roleadmin
)
A postman collection is provided to test the endpoints.