Skip to content

Commit 7835460

Browse files
committed
gh-16231 add JwtPrincipalConverter.java support
1 parent ff7dbb4 commit 7835460

File tree

9 files changed

+210
-17
lines changed

9 files changed

+210
-17
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
4545
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
4646
import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider;
47+
import org.springframework.security.oauth2.server.resource.introspection.JwtPrincipalConverter;
4748
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter;
4849
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
4950
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
@@ -103,6 +104,8 @@
103104
* <li>customizing the conversion from a {@link Jwt} to an
104105
* {@link org.springframework.security.core.Authentication} with
105106
* {@link JwtConfigurer#jwtAuthenticationConverter(Converter)}</li>
107+
* <li>customizing the conversion from a {@link Jwt} to a principal {@link Object} with
108+
* {@link JwtConfigurer#jwtPrincipalConverter(JwtPrincipalConverter)}</li>
106109
* </ul>
107110
*
108111
* <p>
@@ -382,6 +385,8 @@ public class JwtConfigurer {
382385

383386
private Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter;
384387

388+
private JwtPrincipalConverter jwtPrincipalConverter;
389+
385390
JwtConfigurer(ApplicationContext context) {
386391
this.context = context;
387392
}
@@ -408,6 +413,11 @@ public JwtConfigurer jwtAuthenticationConverter(
408413
return this;
409414
}
410415

416+
public JwtConfigurer jwtPrincipalConverter(JwtPrincipalConverter jwtPrincipalConverter) {
417+
this.jwtPrincipalConverter = jwtPrincipalConverter;
418+
return this;
419+
}
420+
411421
/**
412422
* @deprecated For removal in 7.0. Use {@link #jwt(Customizer)} or
413423
* {@code jwt(Customizer.withDefaults())} to stick with defaults. See the <a href=
@@ -421,16 +431,33 @@ public OAuth2ResourceServerConfigurer<H> and() {
421431

422432
Converter<Jwt, ? extends AbstractAuthenticationToken> getJwtAuthenticationConverter() {
423433
if (this.jwtAuthenticationConverter == null) {
424-
if (this.context.getBeanNamesForType(JwtAuthenticationConverter.class).length > 0) {
425-
this.jwtAuthenticationConverter = this.context.getBean(JwtAuthenticationConverter.class);
426-
}
427-
else {
428-
this.jwtAuthenticationConverter = new JwtAuthenticationConverter();
429-
}
434+
final var authenticationConverter = getOrCreateJwtAuthenticationConverter();
435+
authenticationConverter.setJwtPrincipalConverter(getJwtPrincipalConverter());
436+
this.jwtAuthenticationConverter = authenticationConverter;
430437
}
431438
return this.jwtAuthenticationConverter;
432439
}
433440

441+
JwtPrincipalConverter getJwtPrincipalConverter() {
442+
if (this.jwtPrincipalConverter == null) {
443+
if (this.context.getBeanNamesForType(JwtPrincipalConverter.class).length > 0) {
444+
return this.context.getBean(JwtPrincipalConverter.class);
445+
} else {
446+
return (jwt, principalName) -> jwt;
447+
}
448+
} else {
449+
return this.jwtPrincipalConverter;
450+
}
451+
}
452+
453+
private JwtAuthenticationConverter getOrCreateJwtAuthenticationConverter() {
454+
if (this.context.getBeanNamesForType(JwtAuthenticationConverter.class).length > 0) {
455+
return this.context.getBean(JwtAuthenticationConverter.class);
456+
} else {
457+
return new JwtAuthenticationConverter();
458+
}
459+
}
460+
434461
JwtDecoder getJwtDecoder() {
435462
if (this.decoder == null) {
436463
return this.context.getBean(JwtDecoder.class);

config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
121121
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
122122
import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver;
123+
import org.springframework.security.oauth2.server.resource.introspection.JwtPrincipalConverter;
123124
import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
124125
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter;
125126
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
@@ -1398,6 +1399,60 @@ public void getJwtAuthenticationConverterWhenDuplicateConverterBeansThenThrowsEx
13981399
.isThrownBy(jwtConfigurer::getJwtAuthenticationConverter);
13991400
}
14001401

1402+
@Test
1403+
public void getJwtPrincipalConverterWhenNoConverterSpecifiedThenTheDefaultIsUsed() {
1404+
ApplicationContext context = this.spring.context(new GenericWebApplicationContext()).getContext();
1405+
OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = new OAuth2ResourceServerConfigurer(context).jwt();
1406+
assertThat(jwtConfigurer.getJwtPrincipalConverter()).isInstanceOf(JwtPrincipalConverter.class);
1407+
}
1408+
1409+
@Test
1410+
public void getJwtPrincipalConverterWhenConverterBeanSpecified() {
1411+
JwtPrincipalConverter converterBean = mock(JwtPrincipalConverter.class);
1412+
GenericWebApplicationContext context = new GenericWebApplicationContext();
1413+
context.registerBean(JwtPrincipalConverter.class, () -> converterBean);
1414+
this.spring.context(context).autowire();
1415+
OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = new OAuth2ResourceServerConfigurer(context).jwt();
1416+
assertThat(jwtConfigurer.getJwtPrincipalConverter()).isEqualTo(converterBean);
1417+
}
1418+
1419+
@Test
1420+
public void getJwtPrincipalConverterWhenConverterBeanAndAnotherOnTheDslThenTheDslOneIsUsed() {
1421+
JwtPrincipalConverter converter = mock(JwtPrincipalConverter.class);
1422+
JwtPrincipalConverter converterBean = mock(JwtPrincipalConverter.class);
1423+
GenericWebApplicationContext context = new GenericWebApplicationContext();
1424+
context.registerBean(JwtPrincipalConverter.class, () -> converterBean);
1425+
this.spring.context(context).autowire();
1426+
OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = new OAuth2ResourceServerConfigurer(context).jwt();
1427+
jwtConfigurer.jwtPrincipalConverter(converter);
1428+
assertThat(jwtConfigurer.getJwtPrincipalConverter()).isEqualTo(converter);
1429+
}
1430+
1431+
@Test
1432+
public void getJwtPrincipalConverterWhenDuplicateConverterBeansAndAnotherOnTheDslThenTheDslOneIsUsed() {
1433+
JwtPrincipalConverter converter = mock(JwtPrincipalConverter.class);
1434+
JwtPrincipalConverter converterBean = mock(JwtPrincipalConverter.class);
1435+
GenericWebApplicationContext context = new GenericWebApplicationContext();
1436+
context.registerBean("converterOne", JwtPrincipalConverter.class, () -> converterBean);
1437+
context.registerBean("converterTwo", JwtPrincipalConverter.class, () -> converterBean);
1438+
this.spring.context(context).autowire();
1439+
OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = new OAuth2ResourceServerConfigurer(context).jwt();
1440+
jwtConfigurer.jwtPrincipalConverter(converter);
1441+
assertThat(jwtConfigurer.getJwtPrincipalConverter()).isEqualTo(converter);
1442+
}
1443+
1444+
@Test
1445+
public void getJwtPrincipalConverterWhenDuplicateConverterBeansThenThrowsException() {
1446+
JwtPrincipalConverter converterBean = mock(JwtPrincipalConverter.class);
1447+
GenericWebApplicationContext context = new GenericWebApplicationContext();
1448+
context.registerBean("converterOne", JwtPrincipalConverter.class, () -> converterBean);
1449+
context.registerBean("converterTwo", JwtPrincipalConverter.class, () -> converterBean);
1450+
this.spring.context(context).autowire();
1451+
OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = new OAuth2ResourceServerConfigurer(context).jwt();
1452+
assertThatExceptionOfType(NoUniqueBeanDefinitionException.class)
1453+
.isThrownBy(jwtConfigurer::getJwtPrincipalConverter);
1454+
}
1455+
14011456
@Test
14021457
public void getWhenCustomAuthenticationConverterThenUsed() throws Exception {
14031458
this.spring

oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverter.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
2323
import org.springframework.security.core.GrantedAuthority;
2424
import org.springframework.security.oauth2.jwt.Jwt;
2525
import org.springframework.security.oauth2.jwt.JwtClaimNames;
26+
import org.springframework.security.oauth2.server.resource.introspection.JwtPrincipalConverter;
2627
import org.springframework.util.Assert;
2728

2829
/**
@@ -35,15 +36,15 @@
3536
public class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
3637

3738
private Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
38-
39+
private JwtPrincipalConverter principalConverter = (jwt, principalClaimValue) -> jwt;
3940
private String principalClaimName = JwtClaimNames.SUB;
4041

4142
@Override
4243
public final AbstractAuthenticationToken convert(Jwt jwt) {
4344
Collection<GrantedAuthority> authorities = this.jwtGrantedAuthoritiesConverter.convert(jwt);
44-
4545
String principalClaimValue = jwt.getClaimAsString(this.principalClaimName);
46-
return new JwtAuthenticationToken(jwt, authorities, principalClaimValue);
46+
Object principal = principalConverter.convert(jwt, principalClaimValue);
47+
return new JwtAuthenticationToken(jwt, principal, authorities, principalClaimValue);
4748
}
4849

4950
/**
@@ -69,4 +70,13 @@ public void setPrincipalClaimName(String principalClaimName) {
6970
this.principalClaimName = principalClaimName;
7071
}
7172

73+
/**
74+
* Sets the principal converter. Defaults to {@link Jwt}.
75+
* @param jwtPrincipalConverter The principal converter
76+
*/
77+
public void setJwtPrincipalConverter(JwtPrincipalConverter jwtPrincipalConverter) {
78+
Assert.notNull(jwtPrincipalConverter, "jwtPrincipalConverter cannot be null");
79+
this.principalConverter = jwtPrincipalConverter;
80+
}
81+
7282
}

oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -72,6 +72,19 @@ public JwtAuthenticationToken(Jwt jwt, Collection<? extends GrantedAuthority> au
7272
this.name = name;
7373
}
7474

75+
/**
76+
* Constructs a {@code JwtAuthenticationToken} using the provided parameters.
77+
* @param jwt the JWT
78+
* @param principal the principal converted from JWT
79+
* @param authorities the authorities assigned to the JWT
80+
* @param name the principal name
81+
*/
82+
public JwtAuthenticationToken(Jwt jwt, Object principal, Collection<? extends GrantedAuthority> authorities, String name) {
83+
super(jwt, principal, jwt, authorities);
84+
this.setAuthenticated(true);
85+
this.name = name;
86+
}
87+
7588
@Override
7689
public Map<String, Object> getTokenAttributes() {
7790
return this.getToken().getClaims();

oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverter.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.security.oauth2.server.resource.authentication;
1818

19+
import org.springframework.security.oauth2.server.resource.introspection.JwtPrincipalConverter;
1920
import reactor.core.publisher.Flux;
2021
import reactor.core.publisher.Mono;
2122

@@ -38,7 +39,7 @@ public final class ReactiveJwtAuthenticationConverter implements Converter<Jwt,
3839

3940
private Converter<Jwt, Flux<GrantedAuthority>> jwtGrantedAuthoritiesConverter = new ReactiveJwtGrantedAuthoritiesConverterAdapter(
4041
new JwtGrantedAuthoritiesConverter());
41-
42+
private JwtPrincipalConverter principalConverter = (jwt, principalName) -> jwt;
4243
private String principalClaimName = JwtClaimNames.SUB;
4344

4445
@Override
@@ -48,7 +49,8 @@ public Mono<AbstractAuthenticationToken> convert(Jwt jwt) {
4849
.collectList()
4950
.map((authorities) -> {
5051
String principalName = jwt.getClaimAsString(this.principalClaimName);
51-
return new JwtAuthenticationToken(jwt, authorities, principalName);
52+
Object principal = principalConverter.convert(jwt, principalName);
53+
return new JwtAuthenticationToken(jwt, principal, authorities, principalName);
5254
});
5355
// @formatter:on
5456
}
@@ -75,4 +77,13 @@ public void setPrincipalClaimName(String principalClaimName) {
7577
this.principalClaimName = principalClaimName;
7678
}
7779

80+
/**
81+
* Sets the principal converter. Defaults to {@link Jwt}.
82+
* @param jwtPrincipalConverter The principal converter
83+
*/
84+
public void setJwtPrincipalConverter(JwtPrincipalConverter jwtPrincipalConverter) {
85+
Assert.notNull(jwtPrincipalConverter, "jwtPrincipalConverter cannot be null");
86+
this.principalConverter = jwtPrincipalConverter;
87+
}
88+
7889
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.oauth2.server.resource.introspection;
18+
19+
import org.springframework.security.oauth2.jwt.Jwt;
20+
21+
/**
22+
* @author Alex Vlasov
23+
*/
24+
public interface JwtPrincipalConverter {
25+
26+
Object convert(Jwt jwt, String principalName);
27+
28+
}

oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverterTests.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@
2525
import org.springframework.security.authentication.AbstractAuthenticationToken;
2626
import org.springframework.security.core.GrantedAuthority;
2727
import org.springframework.security.core.authority.SimpleGrantedAuthority;
28+
import org.springframework.security.core.userdetails.User;
2829
import org.springframework.security.oauth2.jwt.Jwt;
2930
import org.springframework.security.oauth2.jwt.TestJwts;
3031

@@ -112,4 +113,19 @@ public void convertWhenPrincipalClaimNameSetAndClaimValueIsNotString() {
112113
assertThat(authentication.getName()).isEqualTo("100");
113114
}
114115

116+
@Test
117+
public void convertWithCustomJwtPrincipalConverter() {
118+
this.jwtAuthenticationConverter.setJwtPrincipalConverter((jwt, name) -> User.withUsername(name).password("").build());
119+
Jwt jwt = TestJwts.user();
120+
AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt);
121+
assertThat(authentication.getPrincipal()).isInstanceOf(User.class).hasFieldOrPropertyWithValue("username", "mock-test-subject");
122+
}
123+
124+
@Test
125+
public void convertWithDefaultJwtPrincipalConverter() {
126+
Jwt jwt = TestJwts.user();
127+
AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt);
128+
assertThat(authentication.getPrincipal()).isInstanceOf(Jwt.class);
129+
}
130+
115131
}

oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,11 +24,14 @@
2424

2525
import org.springframework.security.core.GrantedAuthority;
2626
import org.springframework.security.core.authority.AuthorityUtils;
27+
import org.springframework.security.core.userdetails.User;
2728
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
2829
import org.springframework.security.oauth2.jwt.Jwt;
30+
import org.springframework.security.oauth2.jwt.TestJwts;
2931

3032
import static org.assertj.core.api.Assertions.assertThat;
3133
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
34+
import static org.mockito.Mockito.mock;
3235

3336
/**
3437
* Tests for {@link JwtAuthenticationToken}
@@ -115,6 +118,20 @@ public void getNameWhenConstructedWithNoSubjectThenReturnsNull() {
115118
assertThat(new JwtAuthenticationToken(jwt).getName()).isNull();
116119
}
117120

121+
@Test
122+
public void testConstructorWithPrincipal() {
123+
Collection<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("test");
124+
User principal = mock(User.class);
125+
Jwt jwt = TestJwts.user();
126+
JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, principal, authorities, "Hayden");
127+
assertThat(token.getToken()).isSameAs(jwt);
128+
assertThat(token.getCredentials()).isSameAs(jwt);
129+
assertThat(token.getPrincipal()).isSameAs(principal);
130+
assertThat(token.getAuthorities()).isEqualTo(authorities);
131+
assertThat(token.isAuthenticated()).isTrue();
132+
assertThat(token.getName()).isEqualTo("Hayden");
133+
}
134+
118135
private Jwt.Builder builder() {
119136
return Jwt.withTokenValue("token").header("alg", JwsAlgorithms.RS256);
120137
}

0 commit comments

Comments
 (0)