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 andauthorizeRequests
fromhasRole("USER")
andhasRole("ADMIN")
tohasRole("user")
andhasRole("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.