diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java index 31a8c265a04..2a656f98fc5 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java @@ -44,6 +44,7 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.introspection.JwtPrincipalConverter; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector; @@ -103,6 +104,8 @@ *
  • customizing the conversion from a {@link Jwt} to an * {@link org.springframework.security.core.Authentication} with * {@link JwtConfigurer#jwtAuthenticationConverter(Converter)}
  • + *
  • customizing the conversion from a {@link Jwt} to a principal {@link Object} with + * {@link JwtConfigurer#jwtPrincipalConverter(JwtPrincipalConverter)}
  • * * *

    @@ -382,6 +385,8 @@ public class JwtConfigurer { private Converter jwtAuthenticationConverter; + private JwtPrincipalConverter jwtPrincipalConverter; + JwtConfigurer(ApplicationContext context) { this.context = context; } @@ -408,6 +413,11 @@ public JwtConfigurer jwtAuthenticationConverter( return this; } + public JwtConfigurer jwtPrincipalConverter(JwtPrincipalConverter jwtPrincipalConverter) { + this.jwtPrincipalConverter = jwtPrincipalConverter; + return this; + } + /** * @deprecated For removal in 7.0. Use {@link #jwt(Customizer)} or * {@code jwt(Customizer.withDefaults())} to stick with defaults. See the and() { Converter getJwtAuthenticationConverter() { if (this.jwtAuthenticationConverter == null) { - if (this.context.getBeanNamesForType(JwtAuthenticationConverter.class).length > 0) { - this.jwtAuthenticationConverter = this.context.getBean(JwtAuthenticationConverter.class); - } - else { - this.jwtAuthenticationConverter = new JwtAuthenticationConverter(); - } + final var authenticationConverter = getOrCreateJwtAuthenticationConverter(); + authenticationConverter.setJwtPrincipalConverter(getJwtPrincipalConverter()); + this.jwtAuthenticationConverter = authenticationConverter; } return this.jwtAuthenticationConverter; } + JwtPrincipalConverter getJwtPrincipalConverter() { + if (this.jwtPrincipalConverter == null) { + if (this.context.getBeanNamesForType(JwtPrincipalConverter.class).length > 0) { + return this.context.getBean(JwtPrincipalConverter.class); + } else { + return (jwt, principalName) -> jwt; + } + } else { + return this.jwtPrincipalConverter; + } + } + + private JwtAuthenticationConverter getOrCreateJwtAuthenticationConverter() { + if (this.context.getBeanNamesForType(JwtAuthenticationConverter.class).length > 0) { + return this.context.getBean(JwtAuthenticationConverter.class); + } else { + return new JwtAuthenticationConverter(); + } + } + JwtDecoder getJwtDecoder() { if (this.decoder == null) { return this.context.getBean(JwtDecoder.class); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index c247a6d7fed..60245c2116e 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -120,6 +120,7 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver; +import org.springframework.security.oauth2.server.resource.introspection.JwtPrincipalConverter; import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; @@ -1398,6 +1399,60 @@ public void getJwtAuthenticationConverterWhenDuplicateConverterBeansThenThrowsEx .isThrownBy(jwtConfigurer::getJwtAuthenticationConverter); } + @Test + public void getJwtPrincipalConverterWhenNoConverterSpecifiedThenTheDefaultIsUsed() { + ApplicationContext context = this.spring.context(new GenericWebApplicationContext()).getContext(); + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = new OAuth2ResourceServerConfigurer(context).jwt(); + assertThat(jwtConfigurer.getJwtPrincipalConverter()).isInstanceOf(JwtPrincipalConverter.class); + } + + @Test + public void getJwtPrincipalConverterWhenConverterBeanSpecified() { + JwtPrincipalConverter converterBean = mock(JwtPrincipalConverter.class); + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean(JwtPrincipalConverter.class, () -> converterBean); + this.spring.context(context).autowire(); + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = new OAuth2ResourceServerConfigurer(context).jwt(); + assertThat(jwtConfigurer.getJwtPrincipalConverter()).isEqualTo(converterBean); + } + + @Test + public void getJwtPrincipalConverterWhenConverterBeanAndAnotherOnTheDslThenTheDslOneIsUsed() { + JwtPrincipalConverter converter = mock(JwtPrincipalConverter.class); + JwtPrincipalConverter converterBean = mock(JwtPrincipalConverter.class); + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean(JwtPrincipalConverter.class, () -> converterBean); + this.spring.context(context).autowire(); + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = new OAuth2ResourceServerConfigurer(context).jwt(); + jwtConfigurer.jwtPrincipalConverter(converter); + assertThat(jwtConfigurer.getJwtPrincipalConverter()).isEqualTo(converter); + } + + @Test + public void getJwtPrincipalConverterWhenDuplicateConverterBeansAndAnotherOnTheDslThenTheDslOneIsUsed() { + JwtPrincipalConverter converter = mock(JwtPrincipalConverter.class); + JwtPrincipalConverter converterBean = mock(JwtPrincipalConverter.class); + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean("converterOne", JwtPrincipalConverter.class, () -> converterBean); + context.registerBean("converterTwo", JwtPrincipalConverter.class, () -> converterBean); + this.spring.context(context).autowire(); + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = new OAuth2ResourceServerConfigurer(context).jwt(); + jwtConfigurer.jwtPrincipalConverter(converter); + assertThat(jwtConfigurer.getJwtPrincipalConverter()).isEqualTo(converter); + } + + @Test + public void getJwtPrincipalConverterWhenDuplicateConverterBeansThenThrowsException() { + JwtPrincipalConverter converterBean = mock(JwtPrincipalConverter.class); + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean("converterOne", JwtPrincipalConverter.class, () -> converterBean); + context.registerBean("converterTwo", JwtPrincipalConverter.class, () -> converterBean); + this.spring.context(context).autowire(); + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = new OAuth2ResourceServerConfigurer(context).jwt(); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) + .isThrownBy(jwtConfigurer::getJwtPrincipalConverter); + } + @Test public void getWhenCustomAuthenticationConverterThenUsed() throws Exception { this.spring diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverter.java index 959abc30e67..9b950101814 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverter.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -23,6 +23,7 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.resource.introspection.JwtPrincipalConverter; import org.springframework.util.Assert; /** @@ -35,15 +36,15 @@ public class JwtAuthenticationConverter implements Converter { private Converter> jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); - + private JwtPrincipalConverter principalConverter = (jwt, principalClaimValue) -> jwt; private String principalClaimName = JwtClaimNames.SUB; @Override public final AbstractAuthenticationToken convert(Jwt jwt) { Collection authorities = this.jwtGrantedAuthoritiesConverter.convert(jwt); - String principalClaimValue = jwt.getClaimAsString(this.principalClaimName); - return new JwtAuthenticationToken(jwt, authorities, principalClaimValue); + Object principal = principalConverter.convert(jwt, principalClaimValue); + return new JwtAuthenticationToken(jwt, principal, authorities, principalClaimValue); } /** @@ -69,4 +70,13 @@ public void setPrincipalClaimName(String principalClaimName) { this.principalClaimName = principalClaimName; } + /** + * Sets the principal converter. Defaults to {@link Jwt}. + * @param jwtPrincipalConverter The principal converter + */ + public void setJwtPrincipalConverter(JwtPrincipalConverter jwtPrincipalConverter) { + Assert.notNull(jwtPrincipalConverter, "jwtPrincipalConverter cannot be null"); + this.principalConverter = jwtPrincipalConverter; + } + } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java index e389e5e93c6..727edb56d4c 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -72,6 +72,19 @@ public JwtAuthenticationToken(Jwt jwt, Collection au this.name = name; } + /** + * Constructs a {@code JwtAuthenticationToken} using the provided parameters. + * @param jwt the JWT + * @param principal the principal converted from JWT + * @param authorities the authorities assigned to the JWT + * @param name the principal name + */ + public JwtAuthenticationToken(Jwt jwt, Object principal, Collection authorities, String name) { + super(jwt, principal, jwt, authorities); + this.setAuthenticated(true); + this.name = name; + } + @Override public Map getTokenAttributes() { return this.getToken().getClaims(); diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverter.java index c80fce57975..bd6f5bca363 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverter.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.server.resource.authentication; +import org.springframework.security.oauth2.server.resource.introspection.JwtPrincipalConverter; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -38,7 +39,7 @@ public final class ReactiveJwtAuthenticationConverter implements Converter> jwtGrantedAuthoritiesConverter = new ReactiveJwtGrantedAuthoritiesConverterAdapter( new JwtGrantedAuthoritiesConverter()); - + private JwtPrincipalConverter principalConverter = (jwt, principalName) -> jwt; private String principalClaimName = JwtClaimNames.SUB; @Override @@ -48,7 +49,8 @@ public Mono convert(Jwt jwt) { .collectList() .map((authorities) -> { String principalName = jwt.getClaimAsString(this.principalClaimName); - return new JwtAuthenticationToken(jwt, authorities, principalName); + Object principal = principalConverter.convert(jwt, principalName); + return new JwtAuthenticationToken(jwt, principal, authorities, principalName); }); // @formatter:on } @@ -75,4 +77,13 @@ public void setPrincipalClaimName(String principalClaimName) { this.principalClaimName = principalClaimName; } + /** + * Sets the principal converter. Defaults to {@link Jwt}. + * @param jwtPrincipalConverter The principal converter + */ + public void setJwtPrincipalConverter(JwtPrincipalConverter jwtPrincipalConverter) { + Assert.notNull(jwtPrincipalConverter, "jwtPrincipalConverter cannot be null"); + this.principalConverter = jwtPrincipalConverter; + } + } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/JwtPrincipalConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/JwtPrincipalConverter.java new file mode 100644 index 00000000000..d7a5a9fcf61 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/JwtPrincipalConverter.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2024 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.oauth2.server.resource.introspection; + +import org.springframework.security.oauth2.jwt.Jwt; + +/** + * @author Alex Vlasov + */ +public interface JwtPrincipalConverter { + + Object convert(Jwt jwt, String principalName); + +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverterTests.java index 6e35bb34336..d43cf4184f0 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverterTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -25,6 +25,7 @@ import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.TestJwts; @@ -112,4 +113,19 @@ public void convertWhenPrincipalClaimNameSetAndClaimValueIsNotString() { assertThat(authentication.getName()).isEqualTo("100"); } + @Test + public void convertWithCustomJwtPrincipalConverter() { + this.jwtAuthenticationConverter.setJwtPrincipalConverter((jwt, name) -> User.withUsername(name).password("").build()); + Jwt jwt = TestJwts.user(); + AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt); + assertThat(authentication.getPrincipal()).isInstanceOf(User.class).hasFieldOrPropertyWithValue("username", "mock-test-subject"); + } + + @Test + public void convertWithDefaultJwtPrincipalConverter() { + Jwt jwt = TestJwts.user(); + AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt); + assertThat(authentication.getPrincipal()).isInstanceOf(Jwt.class); + } + } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java index 2695f0f9fd7..b0658e0a54d 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -24,11 +24,14 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.User; import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.TestJwts; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; /** * Tests for {@link JwtAuthenticationToken} @@ -115,6 +118,20 @@ public void getNameWhenConstructedWithNoSubjectThenReturnsNull() { assertThat(new JwtAuthenticationToken(jwt).getName()).isNull(); } + @Test + public void testConstructorWithPrincipal() { + Collection authorities = AuthorityUtils.createAuthorityList("test"); + User principal = mock(User.class); + Jwt jwt = TestJwts.user(); + JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, principal, authorities, "Hayden"); + assertThat(token.getToken()).isSameAs(jwt); + assertThat(token.getCredentials()).isSameAs(jwt); + assertThat(token.getPrincipal()).isSameAs(principal); + assertThat(token.getAuthorities()).isEqualTo(authorities); + assertThat(token.isAuthenticated()).isTrue(); + assertThat(token.getName()).isEqualTo("Hayden"); + } + private Jwt.Builder builder() { return Jwt.withTokenValue("token").header("alg", JwsAlgorithms.RS256); } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverterTests.java index 31f250ab169..e3349999b7f 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverterTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -19,6 +19,7 @@ import java.util.Collection; import org.junit.jupiter.api.Test; +import org.springframework.security.core.userdetails.User; import reactor.core.publisher.Flux; import org.springframework.core.convert.converter.Converter; @@ -112,4 +113,19 @@ public void convertWhenPrincipalClaimNameSetAndClaimValueIsNotString() { assertThat(authentication.getName()).isEqualTo("100"); } + @Test + public void convertWithCustomJwtPrincipalConverter() { + this.jwtAuthenticationConverter.setJwtPrincipalConverter((jwt, name) -> User.withUsername(name).password("").build()); + Jwt jwt = TestJwts.user(); + AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt).block(); + assertThat(authentication.getPrincipal()).isInstanceOf(User.class).hasFieldOrPropertyWithValue("username", "mock-test-subject"); + } + + @Test + public void convertWithDefaultJwtPrincipalConverter() { + Jwt jwt = TestJwts.user(); + AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt).block(); + assertThat(authentication.getPrincipal()).isInstanceOf(Jwt.class); + } + }