NashTech Blog

Secure Spring Boot APIs Using Keycloak

Table of Contents
Secure Spring Boot APIs Using Keycloak

Welcome back to this Keycloak series, where we’ve been understanding Keycloak step by step.

Before moving ahead, let’s quickly recap what we’ve covered so far:

With an Identity Provider and tokens in place, we’ll now secure backend APIs. This post integrates Keycloak with Spring Boot for JWT authentication and role-based access control, completing the end-to-end flow.

End-to-End Flow

  1. User authenticates → Keycloak login
  2. Keycloak issues JWT access token
  3. Client calls API: Authorization: Bearer <access_token>
  4. Spring Boot validates token signature/expiry (no Keycloak calls needed)
  5. Spring Security extracts roles → Makes authorization decision
  6. API responds (200 OK or 403 Forbidden)

1. Prerequisites and Setup

Before securing APIs, ensure the following (already done in previous blogs):

  • Keycloak is configured
  • Realm, client, users, and roles are set up
  • Access tokens are issued correctly

Spring Boot application:

Environment:

  • Spring Boot 3.x
  • Java 17+
  • Maven or Gradle

2. Add Dependency

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

The spring-boot-starter-oauth2-resource-server dependency already brings in Spring Security, enables JWT validation, and configures the application as a resource server. Therefore, adding spring-boot-starter-security separately is unnecessary in this setup.

Spring Boot acts as an OAuth 2.0 Resource Server:

  • It does not handle login or manage users.
  • Instead, it trusts Keycloak as the Identity Provider.
  • It checks the JWT in the Authorization: Bearer <token> header by validating the signature, expiry, and issuer.
  • If the token is missing or invalid, it returns 401 Unauthorized.
  • So, this is pure API security — there are no login pages, sessions, or form authentication.

3. Configure Keycloak Issuer

As, Spring Boot only needs to know:

  • Where Keycloak is
  • Which realm to trust

Add this to application.properties (Replace demo-realm if you used a different realm) :

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/realms/demo-realm

4. Enable JWT Security

With the dependency added, we configure security using a SecurityFilterChain.

Here, we define which endpoints require authentication and which roles can access them. We also configure the application as an OAuth2 Resource Server using JWT. By using Customizer.withDefaults(), we rely on Spring Security’s default setup to validate JWTs.

Since this is a stateless API, CSRF is disabled and no HTTP sessions are created.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> {})
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(
auth -> auth
.requestMatchers("/info/**").authenticated()
.requestMatchers("/admin/**").hasRole("admin")
.requestMatchers("/user/**").hasAnyRole("user")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.sessionManagement(t -> t.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

return http.build();
}
}

5. Testing the Authentication

Let’s now use the same user we created in the previous blogs:

User: john.doe
Credentials: (as previously configured)
Role: user

  1. Login via Keycloak and obtain the access token.
  2. Call the APIs using:
Authorization: Bearer <access_token>

Expected Behavior

  • Accessing /info → ✅ Works, since this endpoint requires only authentication and does not enforce any role-based check.
Testing endpoint that requires authentication only, without any role check
  • Accessing /user/** → ❌ 403 Forbidden, because the role check fails even though the token is valid.
Testing an endpoint that requires role-based checking

Why Role-Based Checks Fail

Keycloak stores roles in the token like this:

“realm_access”: {
“roles”: [“admin”, “user”]
}

Spring Security expects roles in the format ROLE_<role-name>, such as ROLE_admin or ROLE_user.

Keycloak does not add the ROLE_ prefix to its roles.

Because of this, when Spring Security checks hasRole("user"), it does not find a matching role.

So, even if the token is valid, the request is denied with 403 Forbidden.

6. Making Role Checks Work

There are two approaches to fix the mismatch between Keycloak roles and Spring Security checks:

Approach 1. Quick Fix: Update role checks to include ROLE_

.authorizeHttpRequests(auth -> auth
.requestMatchers("/info/**").authenticated()
.requestMatchers("/admin/**").hasRole("ROLE_admin")
.requestMatchers("/user/**").hasAnyRole("ROLE_user")
.anyRequest().authenticated()
)

Approach 2. Recommended: Map Keycloak roles using a JwtAuthenticationConverter

This configuration extracts roles from realm_access.roles in the JWT and adds the ROLE_ prefix.

It converts them into GrantedAuthority objects so Spring Security can correctly authorize requests using hasRole().


    @Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();

converter.setJwtGrantedAuthoritiesConverter(jwt -> {
List<GrantedAuthority> authorities = new ArrayList<>();

Map<String, Object> realmAccess = jwt.getClaim("realm_access");
if (realmAccess != null) {
List<String> roles = (List<String>) realmAccess.get("roles");
if (roles != null) {
roles.forEach(r ->
authorities.add(new SimpleGrantedAuthority("ROLE_" + r))
);
}
}
return authorities;
});
return converter;
}

Update JWT part in the security configuration class to plug the converter into your resource server configuration:

Remove:

.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))

Add:

      .oauth2ResourceServer(
oauth2 -> oauth2.jwt(
jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
)

7. Testing the Authorization

Expected Behavior

  • Accessing /info → ✅ Works
  • Accessing /user/** → ✅ Works
Role-based check successful
  • Accessing /admin/** → ❌ 403 Forbidden

For /admin/** to work, assign John Doe both roles (admin and user) in Keycloak, generate a new token, and access will succeed for all three endpoints.

Conclusion

This completes the Keycloak series.

We started with Keycloak fundamentals: realms, clients, users, and roles.
Then we explored tokens and the full authentication flow.
Finally, we integrated Keycloak with a Spring Boot application, validated JWTs, mapped roles, and enforced role-based API security.

From setup → token understanding → secured APIs, the flow is now complete.

You now have a fully working, stateless Spring Boot API secured by Keycloak and driven entirely by roles inside the access token.

Picture of Nadra Ibrahim

Nadra Ibrahim

Software Consultant

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top