From e30d9240c9615d943e3f52b319844ff5d21e650f Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Thu, 26 Feb 2026 12:14:43 -0700 Subject: [PATCH] Add Docs for Custom Jwt Principal Converters Issue gh-6237 Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com> --- .../servlet/oauth2/resource-server/jwt.adoc | 16 +++++ .../UserDetailsJwtPrincipalConverter.java | 68 +++++++++++++++++++ ...ilsJwtPrincipalConverterConfiguration.java | 35 ++++++++++ ...UserDetailsJwtPrincipalConverterTests.java | 48 +++++++++++++ .../UserDetailsJwtPrincipalConverter.kt | 47 +++++++++++++ ...tailsJwtPrincipalConverterConfiguration.kt | 35 ++++++++++ .../UserDetailsJwtPrincipalConverterTests.kt | 47 +++++++++++++ 7 files changed, 296 insertions(+) create mode 100644 docs/src/test/java/org/springframework/security/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverter.java create mode 100644 docs/src/test/java/org/springframework/security/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverterConfiguration.java create mode 100644 docs/src/test/java/org/springframework/security/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverterTests.java create mode 100644 docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverter.kt create mode 100644 docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverterConfiguration.kt create mode 100644 docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverterTests.kt diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc index e6fd6e5b40..989621f657 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc @@ -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] diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverter.java b/docs/src/test/java/org/springframework/security/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverter.java new file mode 100644 index 0000000000..658bcaaec4 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverter.java @@ -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 { + + 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 getAttributes() { + return this.jwt.getClaims(); + } + + } + +} +// end::custom-converter[] diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverterConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverterConfiguration.java new file mode 100644 index 0000000000..f155240353 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverterConfiguration.java @@ -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[] + +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverterTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverterTests.java new file mode 100644 index 0000000000..9cbcb31478 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverterTests.java @@ -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"); + } + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverter.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverter.kt new file mode 100644 index 0000000000..d45027f9c9 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverter.kt @@ -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 { + + 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 = jwt.claims + + } + +} +// end::custom-converter[] diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverterConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverterConfiguration.kt new file mode 100644 index 0000000000..934c3ac01e --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverterConfiguration.kt @@ -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[] + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverterTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverterTests.kt new file mode 100644 index 0000000000..e01a0558ac --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverterTests.kt @@ -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") + } + +}