Secure your application with Spring Security and Keycloak

Lejdi Prifti
5 min readApr 3, 2024

--

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!

Login page of Keycloak

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.

Step 1. Creating the medium-app client.

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.

Step 2. Specifying the authentication flows.

In Step 3, we set http://localhost:8080/* as a valid redirect URI.

Step 3. Specifying the valid redirect URIs.

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

--

--

Lejdi Prifti

Software Developer | ML Enthusiast | AWS Practitioner | Kubernetes Administrator