Secure your application with Spring Security and Keycloak
Nowadays, writing secure apps is becoming essential, and security is a major component of application development. For this reason, we will examine in this post how to use Spring Security and Keycloak to secure Spring Boot applications.
Keycloak Setup
We’ll use Docker to create a Keycloak container. We rapidly initialize a container in Docker by running following command.
docker run \
-p 8081:8080 \
-e KEYCLOAK_ADMIN=admin \
-e CLOAK_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:24.0.2 \
start-dev
If we go to http://localhost:8081, we should see the login page of our Keycloak instance. Nice and simple!
After logging in by using the credentials we specified in the command above, we will create a realm named medium.
In the following images, starting with Step 1, we create a client with the client id medium-app.
In Step 2, we select Client authentication and in addition to the default values, we select also the Implicit flow as an authentication flow. The Implicit Flow is suitable for client-side applications, such as single-page applications (SPAs), where it’s not safe to store secrets because the client code is easily accessible to users. The client receives an access token directly from the authorization server after the user authenticates, without the need for a client secret. This eliminates the risk of the client secret being exposed in client-side code. In our application, we configure OpenAPI page to automatically redirect to the Keycloak login page for authentication.
In Step 3, we set http://localhost:8080/* as a valid redirect URI.
Finally, we create a user in the realm medium
we just created. We will use the user to authenticate later.
Spring Boot Application Development
Dependencies
The dependencies we will use in our application are Spring Security, Spring Web, OAuth Resource Server and OpenAPI (Swagger).
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-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.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
</dependencies>
Security Configuration
In the SecurityConfig class, we define @EnableWebSecurity
which indicates that Spring Security should be enabled for this application.
We define a bean method configurePaths
which returns a SecurityFilterChain
. This method configures security settings for different paths based on the provided HttpSecurity
and a list of unsecured paths specified in the application properties. We convert the list of unsecured paths to an array of AntPathRequestMatcher
objects, which are used for path matching in Spring Security configuration.
Then, we configure session management to be stateless, meaning no session will be created or maintained on the server side.
Finally, we configure the application to use OAuth 2.0 resource server with JWT tokens. We set up a JWT token authentication converter using a customJwtAuthenticationConverter
. We set to the JwtAuthenticationConverter
a custom granted authorities converter named KeycloakClientRoleConverter
.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private KeycloakClientRoleConverter keycloakClientRoleConverter;
@Bean
public SecurityFilterChain configurePaths(HttpSecurity http,
@Value("${security.authentication.unsecure.paths}") List<String> springSecurityAllowedPaths)
throws Exception {
AntPathRequestMatcher[] allowedPaths = springSecurityAllowedPaths.stream().map(AntPathRequestMatcher::new)
.toArray(AntPathRequestMatcher[]::new);
http.sessionManagement(
sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests.requestMatchers(allowedPaths)
.permitAll().anyRequest().authenticated());
http.oauth2ResourceServer(
oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())));
return http.build();
}
private Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter() {
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(keycloakClientRoleConverter);
return jwtConverter;
}
}
KeycloakClientRoleConverter
uses the convert
method to simply extract the roles from the jwt object and convert them into a list of SimpleGrantedAuthority
objects. These roles are then automatically set into the SecurityContext
of Spring Security.
@Component
public class KeycloakClientRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
private static final String ROLES = "roles";
private static final String REALM_ACCESS = "realm_access";
@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
Map<String, Object> realmAccess = Optional.ofNullable(jwt.getClaimAsMap(REALM_ACCESS))
.orElseGet(() -> Collections.emptyMap());
return Optional.ofNullable((List<?>) realmAccess.get(ROLES)).orElseGet(() -> Collections.emptyList()).stream()
.map(Object::toString).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
}
Swagger Configuration
In order to be able to test quickly and easily from the OpenAPI interface, let’s define some OpenAPI configurations.
We create a new class named SwaggerConfig
and annotate it as a configuration class. Additonally, it is annotated with @OpenAPIDefinition
, which marks it as the starting point for OpenAPI documentation generation. This annotation includes security requirements for the OpenAPI documentation, specifying that the "bearer" token is required for accessing the endpoints. Moreover, the class is annotated with @SecuritySchemes
, where a security scheme named "bearer" is defined. This scheme utilizes OAuth 2.0 for authentication, specifically employing the Implicit Flow, as indicated by @OAuthFlows(implicit = @OAuthFlow(authorizationUrl = "${security.authorization.url}"))
. This flow defines the authorization URL used for obtaining access tokens, with the URL specified via the property ${security.authorization.url}
.
@Configuration
@OpenAPIDefinition(security = { @SecurityRequirement(name = "bearer") })
@SecuritySchemes(value = {
@SecurityScheme(name = "bearer", type = SecuritySchemeType.OAUTH2,
flows = @OAuthFlows(implicit = @OAuthFlow(authorizationUrl = "${security.authorization.url}"))) })
public class SwaggerConfig {
}
Properties file
To put everything together, we need the following properties in the application.properties
file.
spring.application.name=keycloak-integration
server.port=8080
# openapi
springdoc.api-docs.enabled=true
springdoc.api-docs.path=/api/json
springdoc.swagger-ui.enabled=true
springdoc.swagger-ui.path=/api/ui
springdoc.cache.disabled=true
# security
security.authentication.unsecure.paths=/api/**
security.authorization.url=http://localhost:8081/realms/medium/protocol/openid-connect/auth
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8081/realms/medium
Great! We have all we need. Let’s simply create a controller and test.
@RestController
@RequestMapping("/")
public class TestController {
@GetMapping("/health")
public String isOkay() {
return "OK";
}
}
Testing
If not authenticated, we receive a 401 Unauthorized
response.
The endpoint can only be correctly triggered once we have authenticated.
Hope it was helpful! Thanks for reading!
Repository: https://github.com/lejdiprifti/medium/tree/main/keycloak-integration