The answer from Gary Archer is correct, but it has some pitfalls.
A full simple example in Kotlin:
The user interface, which is specific to your application:
interface User {
val id : String
val email : String
val displayName : String
val avatar : String
val roles : Set<String>
val domain : String
get() = this.email.split('@').last()
}
A principal should implement this interface:
class ClaimsNotFull(claim: String) : Exception("Claims not full, not found $claim")
data class IdelPrincipal(
override val id: String,
override val email: String,
override val displayName: String,
override val avatar: String,
override val roles: Set<String>,
) : User {
companion object {
fun fromJwt(jwt: Jwt): IdelPrincipal {
fun loadClaim(key: String): String = jwt.claims.getOrElse(key) {throw ClaimsNotFull(key)} as String
return IdelPrincipal(
id = jwt.subject,
email = loadClaim("email"),
displayName = loadClaim("displayName"),
avatar = loadClaim("avatar"),
roles = loadClaim("roles").split(",").toSet()
)
}
fun copyToClaims(user : User, jwt: JWTClaimsSet.Builder) : JWTClaimsSet.Builder {
return jwt.subject(user.id)
.claim("email", user.email)
.claim("displayName",user.displayName)
.claim("avatar",user.avatar)
.claim("roles",user.roles.joinToString(","))
}
}
}
Custom token and JwtConvertor:
class IdelAuthenticationToken(val jwt: Jwt, val user: IdelPrincipal) :
AbstractAuthenticationToken(IdelAuthorities.from(user.roles)) {
override fun getCredentials() = jwt // borrowed from JwtAuthenticationToken
override fun getPrincipal() = user
override fun isAuthenticated() = true // decoding of jwt is authentication
}
class IdelPrincipalJwtConvertor : Converter<Jwt, IdelAuthenticationToken> {
override fun convert(jwt: Jwt): IdelAuthenticationToken {
val principal = IdelPrincipal.fromJwt(jwt)
return IdelAuthenticationToken(jwt,principal)
}
}
Configure Spring Security:
override fun configure(http: HttpSecurity) {
http.authorizeRequests {
it.anyRequest().authenticated()
}
.csrf{it.ignoringAntMatchers("/token")}
.oauth2ResourceServer {it.jwt()}
.oauth2ResourceServer().jwt {cstm ->
cstm.jwtAuthenticationConverter(IdelPrincipalJwtConvertor())
}
http.exceptionHandling {
it
.authenticationEntryPoint(BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(BearerTokenAccessDeniedHandler())
}
http .sessionManagement {it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)}
http.anonymous().disable()
}
And a method in your controller for testing:
@GetMapping("/me2")
fun me2(@AuthenticationPrincipal user : User) : User {
val result = MeResult(
id = user.id,
domain = user.domain,
displayName = user.displayName,
avatar = user.avatar,
email = user.email,
authorities = user.roles.toList()
)
return user
}
That's return:
{
"id": "10777",
"email": "leonid.vygovsky@gmail.com",
"displayName": "Leonid Vygovskiy",
"avatar": "",
"roles": [
"ROLE_USER"
],
"domain": "gmail.com"
}