Home > Software design >  Oauth2 resource server with Keycloack - cannot map roles
Oauth2 resource server with Keycloack - cannot map roles

Time:01-16

I try to write auth application using Keycloack and Oauth2 resource server and still cannot recognize mapped roles:

plugins {
    id("org.springframework.boot") version "2.7.8-SNAPSHOT"
}

implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
implementation("com.c4-soft.springaddons:spring-addons-webmvc-jwt-resource-server:5.3.2")

part of JWT:

 "allowed-origins": [],
  "realm_access": {
    "roles": [
      "user"
    ]
  },
  "resource_access": {
    "myapp": {
      "roles": [
        "user"
      ]
    },
    "account": {
      "roles": [
        "manage-account",
        "manage-account-links",
        "view-profile"
      ]
    }
  },

Config class:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
class JWTSecurityConfig {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain =
        http
            .cors()
            .and()
            .authorizeRequests { auth ->
                auth.antMatchers(HttpMethod.GET, "/user")
                    .hasRole("user")
                    .antMatchers(HttpMethod.GET, "/admin")
                    .hasRole("admin")
                    .anyRequest()
                    .authenticated()
            }
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer<HttpSecurity>::jwt)
            .build()

    @Bean
    fun jwtAuthenticationConverterForKeycloak(): JwtAuthenticationConverter? {
        val jwtGrantedAuthoritiesConverter =
            Converter<Jwt, Collection<GrantedAuthority>> { jwt: Jwt ->
                val realmAccess = jwt.getClaim<Map<String, Collection<String>>>("realm_access")
                val roles = realmAccess["roles"]!!
                roles.stream()
                    .map { role -> SimpleGrantedAuthority("ROLE_$role") }
                    .collect(Collectors.toList())
            }
        
        val jwtAuthenticationConverter = JwtAuthenticationConverter()
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter)
        return jwtAuthenticationConverter
    }
}

application.yml:

spring:
  security:
    oauth2:
      resource-server:
        jwt:
          issuer-uri: http://localhost:8181/auth/realms/myapp
          jwk-set-uri: http://localhost:8181/auth/realms/myapp/protocol/openid-connect/certs

com:
  c4-soft:
    springaddons:
      security:
        issuers[0]:
          location: http://localhost:8181/auth/realms/myapp
          authorities:
            claims: realm_access.roles,resource_access.my-app.roles,resource_access.account.roles
            prefix: ROLE_
        cors[0]:
          path: /user
        cors[1]:
          path: /admin

RestController:

@RestController
class TestController {

    @GetMapping("/user")
    @PreAuthorize("hasRole('user')")
    fun helloUser(): ResponseEntity<Foo> =  ResponseEntity.ok().body(Foo("Hello User"))

    @GetMapping("/admin")
    @PreAuthorize("hasRole('admin')")
    fun helloAdmin(): ResponseEntity<Foo> =  ResponseEntity.ok().body(Foo("Hello Admin"))
}

data class Foo(val value: String)

in this config my

@GetMapping("/auth")
    fun auth(jwt : JwtAuthenticationToken) = jwt.authorities.map(GrantedAuthority::getAuthority);

generates:

[
    "ROLE_USER",
    "ROLE_USER",
    "ROLE_MANAGE-ACCOUNT",
    "ROLE_MANAGE-ACCOUNT-LINKS",
    "ROLE_VIEW-PROFILE"
]

I have no idea what's going on with this code. I found this "working' solution in another question but is not working for me. Has someone idea what can be wrong ? Users with role "admin" can get correctly response (200) from /user

CodePudding user response:

You have a case issue: your roles are lowercase in the access token and you expect uppercase roles in your resource-server.

Two solutions:

  • change @PreAuthorize SpEL and authorizeRequests from hasRole("USER") and hasRole("ADMIN") to hasRole("user") and hasRole("admin")
  • update jwtAuthenticationConverter to transform roles to uppercase

P.S. Have a look at the starters I maintain. Your resource-server config could be as simple as (I remove the authorizeRequests to keep only @PreAuthorize):

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "3.0.1"
    id("io.spring.dependency-management") version "1.1.0"
    kotlin("jvm") version "1.7.22"
    kotlin("plugin.spring") version "1.7.22"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.c4-soft.springaddons:spring-addons-webmvc-jwt-resource-server:6.0.10")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "17"
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}
package com.c4soft

import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity

@Configuration
@EnableMethodSecurity
class JWTSecurityConfig {
}
com:
  c4-soft:
    springaddons:
      security:
        issuers:
          - location: http://localhost:8181/auth/realms/myapp
            authorities:
              claims:
                - realm_access.roles
                - resource_access.myapp.roles
                - resource_access.account.roles
              caze: upper
              prefix: ROLE_
        cors:
          - path: /user
          - path: /admin
          - path: /auth
package com.c4soft

import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class TestController {

    @GetMapping("/user")
    @PreAuthorize("hasRole('USER')")
    fun helloUser(): ResponseEntity<Foo> =  ResponseEntity.ok().body(Foo("Hello User"))

    @GetMapping("/admin")
    @PreAuthorize("hasRole('ADMIN')")
    fun helloAdmin(): ResponseEntity<Foo> =  ResponseEntity.ok().body(Foo("Hello Admin"))

    @GetMapping("/auth")
    fun auth(jwt : JwtAuthenticationToken) = jwt.authorities.map(GrantedAuthority::getAuthority);
}

data class Foo(val value: String)

Other thing, if you use @PreAuthorize("hasAuthority('user')") instead of @PreAuthorize("hasRole('USER')"), then you won't need to force case and prefix.

  • Related