I want to integrate Keycloak with spring boot application. The problem is that at the end, I got 403 forbidden error when calling the protected endpoints.
Following is my decoded JWT token, which is issued by Keycloak. I have a client, which is named clientApp1, and a realm role, which is named clientApp1User and mapped to the created user. Following is my decoded JWT token:
{
alg: "RS256",
typ: "JWT",
kid: "ZWDbgcSI8nD2Yq4LA6hxYcsTbnf6y6Zj8PKyUobE_qE"
}.
{
exp: 1666444432,
iat: 1666444132,
jti: "e6883855-ef20-4fac-95dd-8f13bd0ae552",
iss: "http://localhost:12500/auth/realms/sampleRealm",
aud: "account",
sub: "80e1e45f-49fb-4a5a-9a60-b0057d291c53",
typ: "Bearer",
azp: "clientApp1",
session_state: "c22af762-7be9-4150-94d5-8bd35065ac57",
acr: "1",
allowed-origins: [
"http://localhost:11501"
],
realm_access: {
roles: [
"clientApp1User",
"offline_access",
"uma_authorization",
"default-roles-samplerealm"
]
},
resource_access: {
account: {
roles: [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
scope: "email profile",
sid: "c22af762-7be9-4150-94d5-8bd35065ac57",
email_verified: false,
name: "user1FirstName User1LastName",
preferred_username: "user1",
given_name: "user1FirstName",
family_name: "User1LastName"
}.
[signature]
Moreover, here is my pom.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>ResourceServerSample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ResourceServerSample</name>
<description>ResourceServerSample</description>
<properties>
<java.version>17</java.version>
</properties>
<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.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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Since, I want to use Security annotations to secure my end points I have set the security configuration as following:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true)
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
.anyRequest().permitAll()
.and().oauth2ResourceServer().jwt();
http.csrf().disable();
return http.build();
}
Finally, in order to protect my endpoints I have used annotations like following:
@RestController
public class TestControllers {
// Public endpoint
@GetMapping("/welcome")
public ResponseEntity<String> welcome() {
return ResponseEntity.status(HttpStatus.OK).body("Welcome to the unprotected endpoint");
}
// @RolesAllowed("clientApp1User")
// @Secured("clientApp1User")
@PreAuthorize("hasAuthority('clientApp1User')")
@GetMapping("/clientApp1User")
public ResponseEntity<String> clientApp1User() {
return ResponseEntity.status(HttpStatus.OK).body("clientApp1User protected endpoint sends its regards");
}
@PreAuthorize("hasAuthority('SCOPE_email')")
@GetMapping("/testScope")
public ResponseEntity<String> testScope() {
return ResponseEntity.status(HttpStatus.OK).body("testScope protected endpoint sends its regards");
}
}
The problem that I face is that the endpoint, which is protected with @RolesAllowed("clientApp1User") or @Secured("clientApp1User") or @PreAuthorize("hasAuthority('clientApp1User')") returns 403 forbidden, when it's called with a valid access token.
On the other hand endpoints with annotations like @PreAuthorize("hasAuthority('SCOPE_email')") or @PreAuthorize("hasAuthority('SCOPE_profile')") return 200 Ok.
I believe spring boot can not accurately parse the JWT token and only excepts values in scope claim with the prefix <SCOPE_> and as an authority.
Can any one help me to fix the problem and use the RolesAllowed/Secured/PreAuthorize annotations to secure the endpoint with declared roles in realm_access and resource_access claims?
CodePudding user response:
You can have a look at this example on github. The problem right now is that you need to add your roles to the Security Context of Spring Boot.
public static class CustomJwtConfigure implements Converter<Jwt, JwtAuthenticationToken> {
@Override
public JwtAuthenticationToken convert(Jwt jwt) {
var tokenAttributes = jwt.getClaims();
var jsonObject = (JSONObject) tokenAttributes.get(REALM);
var roles = (JSONArray) jsonObject.get(ROLES);
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
roles.forEach(role -> grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" role)));
return new JwtAuthenticationToken(jwt, grantedAuthorities);
}
}
CodePudding user response:
Roles are private claims: it is neither in OAuth2 nor OpenID specs and each authorization-server provider uses its own.
You have to provide your own Converter<Jwt, AbstractAuthenticatonToken>
@Bean to map authorities from realm_access.roles
(and maybe resource_access.clientApp1.roles
if you enable client level roles in Keycloak) or Spring will try to map it from scope
claim.
Complete samples here: https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials which cover various servlets scenarios with @controller tests.
For reactive apps or secured @Service & @Repository tests, see in https://github.com/ch4mpy/spring-addons/tree/master/samples (but it contain less explanations than tutorials, so start with tutorials and move up to samples for your exact use-case)
Bonus
As already spoiled, in tutorials and samples, you'll see how to mock OAuth2 identities (with authorities) during unit and integration tests.