Add Docs for Custom Jwt Principal Converters

Issue gh-6237

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
This commit is contained in:
Josh Cummings 2026-02-26 12:14:43 -07:00
parent c208410a91
commit e30d9240c9
7 changed files with 296 additions and 0 deletions

View File

@ -1673,3 +1673,19 @@ NOTE: Spring isn't a cache provider, so you'll need to make sure to include the
NOTE: Whether it's socket or cache timeouts, you may instead want to work with Nimbus directly.
To do so, remember that `NimbusJwtDecoder` ships with a constructor that takes Nimbus's `JWTProcessor`.
[[custom-principal]]
== Customizing the Principal
Sometimes it can be helpful to look up a pre-existing user based on the JWTs subject or other identifier.
[[custom-user-details-service]]
=== Wiring a UserDetailsService
For example, if your application has a `UserDetailsService` and you want the corresponding user in the resulting `Authentication`, you can create a class to adapt your `UserDetails` into an `OAuth2AuthenticatedPrincipal`:
include-code::./UserDetailsJwtPrincipalConverter[tag=custom-converter,indent=0]
And then apply your principal converter to a `JwtAuthenticationConverter` `@Bean` like so:
include-code::./UserDetailsJwtPrincipalConverterConfiguration[tag=configure-converter,indent=0]

View File

@ -0,0 +1,68 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.servlet.oauth2.resourceserver.customuserdetailsservice;
import java.util.Map;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
// tag::custom-converter[]
@Component
public final class UserDetailsJwtPrincipalConverter implements Converter<Jwt, OAuth2AuthenticatedPrincipal> {
private final UserDetailsService users;
public UserDetailsJwtPrincipalConverter(UserDetailsService users) {
this.users = users;
}
@Override
public OAuth2AuthenticatedPrincipal convert(Jwt jwt) {
UserDetails user = this.users.loadUserByUsername(jwt.getSubject());
return new JwtUser(jwt, user);
}
private static final class JwtUser extends User implements OAuth2AuthenticatedPrincipal {
private final Jwt jwt;
private JwtUser(Jwt jwt, UserDetails user) {
super(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),
user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
this.jwt = jwt;
}
@Override
public String getName() {
return this.jwt.getSubject();
}
@Override
public Map<String, Object> getAttributes() {
return this.jwt.getClaims();
}
}
}
// end::custom-converter[]

View File

@ -0,0 +1,35 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.servlet.oauth2.resourceserver.customuserdetailsservice;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
@Configuration
class UserDetailsJwtPrincipalConverterConfiguration {
// tag::configure-converter[]
@Bean
JwtAuthenticationConverter authenticationConverter(UserDetailsJwtPrincipalConverter principalConverter) {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtPrincipalConverter(principalConverter);
return converter;
}
// end::configure-converter[]
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.servlet.oauth2.resourceserver.customuserdetailsservice;
import org.junit.jupiter.api.Test;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.TestJwts;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import static org.assertj.core.api.Assertions.assertThat;
class UserDetailsJwtPrincipalConverterTests {
@Test
void convertWhenUserFoundThenPrincipalIsUserDetails() {
UserDetailsService users = (username) -> User.withDefaultPasswordEncoder()
.username(username)
.password("password")
.roles("USER")
.build();
UserDetailsJwtPrincipalConverter principalConverter = new UserDetailsJwtPrincipalConverter(users);
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtPrincipalConverter(principalConverter);
Jwt jwt = TestJwts.jwt().subject("user").build();
OAuth2AuthenticatedPrincipal principal = (OAuth2AuthenticatedPrincipal) converter.convert(jwt).getPrincipal();
assertThat(principal.getName()).isEqualTo("user");
assertThat(principal.getAttributes()).containsKey("sub");
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.customuserdetailsservice
import org.springframework.core.convert.converter.Converter
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.stereotype.Component
// tag::custom-converter[]
@Component
class UserDetailsJwtPrincipalConverter(private val users: UserDetailsService) : Converter<Jwt, OAuth2AuthenticatedPrincipal> {
override fun convert(jwt: Jwt): OAuth2AuthenticatedPrincipal {
val user = users.loadUserByUsername(jwt.subject)
return JwtUser(jwt, user)
}
private class JwtUser(private val jwt: Jwt, user: UserDetails) :
User(user.username, user.password, user.isEnabled, user.isAccountNonExpired, user.isCredentialsNonExpired, user.isAccountNonLocked, user.authorities),
OAuth2AuthenticatedPrincipal {
override fun getName(): String = jwt.subject
override fun getAttributes(): Map<String, Any> = jwt.claims
}
}
// end::custom-converter[]

View File

@ -0,0 +1,35 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.customuserdetailsservice
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
@Configuration
open class UserDetailsJwtPrincipalConverterConfiguration {
// tag::configure-converter[]
@Bean
open fun authenticationConverter(principalConverter: UserDetailsJwtPrincipalConverter): JwtAuthenticationConverter {
val converter = JwtAuthenticationConverter()
converter.setJwtPrincipalConverter(principalConverter)
return converter
}
// end::configure-converter[]
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.customuserdetailsservice
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.security.core.userdetails.User
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal
import org.springframework.security.oauth2.jwt.TestJwts
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
class UserDetailsJwtPrincipalConverterTests {
@Test
fun convertWhenUserFoundThenPrincipalIsUserDetails() {
@Suppress("DEPRECATION")
val users = { username: String ->
User.withDefaultPasswordEncoder()
.username(username)
.password("password")
.roles("USER")
.build()
}
val principalConverter = UserDetailsJwtPrincipalConverter(users)
val converter = JwtAuthenticationConverter()
converter.setJwtPrincipalConverter(principalConverter)
val jwt = TestJwts.jwt().subject("user").build()
val principal = converter.convert(jwt).principal as OAuth2AuthenticatedPrincipal
assertThat(principal.name).isEqualTo("user")
assertThat(principal.attributes).containsKey("sub")
}
}