Skip to content

Commit 1000733

Browse files
committed
Merge pull request #38105 from ykardziyaka
* pr/38105: Polish "Auto-configure a JwtAuthenticationConverter" Auto-configure a JwtAuthenticationConverter Closes gh-38105
2 parents 17e9f0c + baf5221 commit 1000733

File tree

8 files changed

+435
-1
lines changed

8 files changed

+435
-1
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.boot.context.properties.ConfigurationProperties;
2727
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
2828
import org.springframework.core.io.Resource;
29+
import org.springframework.security.core.GrantedAuthority;
2930
import org.springframework.util.Assert;
3031
import org.springframework.util.StreamUtils;
3132

@@ -35,6 +36,7 @@
3536
* @author Madhura Bhave
3637
* @author Artsiom Yudovin
3738
* @author Mushtaq Ahmed
39+
* @author Yan Kardziyaka
3840
* @since 2.1.0
3941
*/
4042
@ConfigurationProperties(prefix = "spring.security.oauth2.resourceserver")
@@ -80,6 +82,28 @@ public static class Jwt {
8082
*/
8183
private List<String> audiences = new ArrayList<>();
8284

85+
/**
86+
* Prefix to use for {@link GrantedAuthority authorities} mapped from JWT.
87+
*/
88+
private String authorityPrefix;
89+
90+
/**
91+
* Regex to use for splitting the value of the authorities claim into
92+
* {@link GrantedAuthority authorities}.
93+
*/
94+
private String authoritiesClaimDelimiter;
95+
96+
/**
97+
* Name of token claim to use for mapping {@link GrantedAuthority authorities}
98+
* from JWT.
99+
*/
100+
private String authoritiesClaimName;
101+
102+
/**
103+
* JWT principal claim name.
104+
*/
105+
private String principalClaimName;
106+
83107
public String getJwkSetUri() {
84108
return this.jwkSetUri;
85109
}
@@ -120,6 +144,38 @@ public void setAudiences(List<String> audiences) {
120144
this.audiences = audiences;
121145
}
122146

147+
public String getAuthorityPrefix() {
148+
return this.authorityPrefix;
149+
}
150+
151+
public void setAuthorityPrefix(String authorityPrefix) {
152+
this.authorityPrefix = authorityPrefix;
153+
}
154+
155+
public String getAuthoritiesClaimDelimiter() {
156+
return this.authoritiesClaimDelimiter;
157+
}
158+
159+
public void setAuthoritiesClaimDelimiter(String authoritiesClaimDelimiter) {
160+
this.authoritiesClaimDelimiter = authoritiesClaimDelimiter;
161+
}
162+
163+
public String getAuthoritiesClaimName() {
164+
return this.authoritiesClaimName;
165+
}
166+
167+
public void setAuthoritiesClaimName(String authoritiesClaimName) {
168+
this.authoritiesClaimName = authoritiesClaimName;
169+
}
170+
171+
public String getPrincipalClaimName() {
172+
return this.principalClaimName;
173+
}
174+
175+
public void setPrincipalClaimName(String principalClaimName) {
176+
this.principalClaimName = principalClaimName;
177+
}
178+
123179
public String readPublicKey() throws IOException {
124180
String key = "spring.security.oauth2.resourceserver.public-key-location";
125181
Assert.notNull(this.publicKeyLocation, "PublicKeyLocation must not be null");

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerConfiguration.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class ReactiveOAuth2ResourceServerConfiguration {
3434
@Configuration(proxyBeanMethods = false)
3535
@ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveJwtDecoder.class })
3636
@Import({ ReactiveOAuth2ResourceServerJwkConfiguration.JwtConfiguration.class,
37+
ReactiveOAuth2ResourceServerJwkConfiguration.JwtConverterConfiguration.class,
3738
ReactiveOAuth2ResourceServerJwkConfiguration.WebSecurityConfiguration.class })
3839
static class JwtConfiguration {
3940

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@
2626
import java.util.Set;
2727

2828
import org.springframework.beans.factory.ObjectProvider;
29+
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
2930
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
3031
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
3132
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3233
import org.springframework.boot.autoconfigure.security.oauth2.resource.IssuerUriCondition;
3334
import org.springframework.boot.autoconfigure.security.oauth2.resource.KeyValueCondition;
3435
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
36+
import org.springframework.boot.context.properties.PropertyMapper;
3537
import org.springframework.context.annotation.Bean;
3638
import org.springframework.context.annotation.Conditional;
3739
import org.springframework.context.annotation.Configuration;
@@ -48,6 +50,9 @@
4850
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder;
4951
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
5052
import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder;
53+
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
54+
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter;
55+
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtGrantedAuthoritiesConverterAdapter;
5156
import org.springframework.security.web.server.SecurityWebFilterChain;
5257
import org.springframework.util.CollectionUtils;
5358

@@ -62,6 +67,7 @@
6267
* @author Anastasiia Losieva
6368
* @author Mushtaq Ahmed
6469
* @author Roman Golovin
70+
* @author Yan Kardziyaka
6571
*/
6672
@Configuration(proxyBeanMethods = false)
6773
class ReactiveOAuth2ResourceServerJwkConfiguration {
@@ -161,6 +167,35 @@ SupplierReactiveJwtDecoder jwtDecoderByIssuerUri(
161167

162168
}
163169

170+
@Configuration(proxyBeanMethods = false)
171+
@ConditionalOnMissingBean(ReactiveJwtAuthenticationConverter.class)
172+
@Conditional(JwtConverterPropertiesCondition.class)
173+
static class JwtConverterConfiguration {
174+
175+
private final OAuth2ResourceServerProperties.Jwt properties;
176+
177+
JwtConverterConfiguration(OAuth2ResourceServerProperties properties) {
178+
this.properties = properties.getJwt();
179+
}
180+
181+
@Bean
182+
ReactiveJwtAuthenticationConverter reactiveJwtAuthenticationConverter() {
183+
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
184+
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
185+
map.from(this.properties.getAuthorityPrefix()).to(grantedAuthoritiesConverter::setAuthorityPrefix);
186+
map.from(this.properties.getAuthoritiesClaimDelimiter())
187+
.to(grantedAuthoritiesConverter::setAuthoritiesClaimDelimiter);
188+
map.from(this.properties.getAuthoritiesClaimName())
189+
.to(grantedAuthoritiesConverter::setAuthoritiesClaimName);
190+
ReactiveJwtAuthenticationConverter jwtAuthenticationConverter = new ReactiveJwtAuthenticationConverter();
191+
map.from(this.properties.getPrincipalClaimName()).to(jwtAuthenticationConverter::setPrincipalClaimName);
192+
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(
193+
new ReactiveJwtGrantedAuthoritiesConverterAdapter(grantedAuthoritiesConverter));
194+
return jwtAuthenticationConverter;
195+
}
196+
197+
}
198+
164199
@Configuration(proxyBeanMethods = false)
165200
@ConditionalOnMissingBean(SecurityWebFilterChain.class)
166201
static class WebSecurityConfiguration {
@@ -179,4 +214,27 @@ private void customDecoder(OAuth2ResourceServerSpec server, ReactiveJwtDecoder d
179214

180215
}
181216

217+
private static class JwtConverterPropertiesCondition extends AnyNestedCondition {
218+
219+
JwtConverterPropertiesCondition() {
220+
super(ConfigurationPhase.REGISTER_BEAN);
221+
}
222+
223+
@ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "authority-prefix")
224+
static class OnAuthorityPrefix {
225+
226+
}
227+
228+
@ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "principal-claim-name")
229+
static class OnPrincipalClaimName {
230+
231+
}
232+
233+
@ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "authorities-claim-name")
234+
static class OnAuthoritiesClaimName {
235+
236+
}
237+
238+
}
239+
182240
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@
2626
import java.util.Set;
2727

2828
import org.springframework.beans.factory.ObjectProvider;
29+
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
2930
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
3031
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
3132
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3233
import org.springframework.boot.autoconfigure.security.ConditionalOnDefaultWebSecurity;
3334
import org.springframework.boot.autoconfigure.security.oauth2.resource.IssuerUriCondition;
3435
import org.springframework.boot.autoconfigure.security.oauth2.resource.KeyValueCondition;
3536
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
37+
import org.springframework.boot.context.properties.PropertyMapper;
3638
import org.springframework.context.annotation.Bean;
3739
import org.springframework.context.annotation.Conditional;
3840
import org.springframework.context.annotation.Configuration;
@@ -48,6 +50,8 @@
4850
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
4951
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder;
5052
import org.springframework.security.oauth2.jwt.SupplierJwtDecoder;
53+
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
54+
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
5155
import org.springframework.security.web.SecurityFilterChain;
5256
import org.springframework.util.CollectionUtils;
5357

@@ -63,6 +67,7 @@
6367
* @author HaiTao Zhang
6468
* @author Mushtaq Ahmed
6569
* @author Roman Golovin
70+
* @author Yan Kardziyaka
6671
*/
6772
@Configuration(proxyBeanMethods = false)
6873
class OAuth2ResourceServerJwtConfiguration {
@@ -173,4 +178,55 @@ SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) throws Exception {
173178

174179
}
175180

181+
@Configuration(proxyBeanMethods = false)
182+
@ConditionalOnMissingBean(JwtAuthenticationConverter.class)
183+
@Conditional(JwtConverterPropertiesCondition.class)
184+
static class JwtConverterConfiguration {
185+
186+
private final OAuth2ResourceServerProperties.Jwt properties;
187+
188+
JwtConverterConfiguration(OAuth2ResourceServerProperties properties) {
189+
this.properties = properties.getJwt();
190+
}
191+
192+
@Bean
193+
JwtAuthenticationConverter getJwtAuthenticationConverter() {
194+
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
195+
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
196+
map.from(this.properties.getAuthorityPrefix()).to(grantedAuthoritiesConverter::setAuthorityPrefix);
197+
map.from(this.properties.getAuthoritiesClaimDelimiter())
198+
.to(grantedAuthoritiesConverter::setAuthoritiesClaimDelimiter);
199+
map.from(this.properties.getAuthoritiesClaimName())
200+
.to(grantedAuthoritiesConverter::setAuthoritiesClaimName);
201+
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
202+
map.from(this.properties.getPrincipalClaimName()).to(jwtAuthenticationConverter::setPrincipalClaimName);
203+
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
204+
return jwtAuthenticationConverter;
205+
}
206+
207+
}
208+
209+
private static class JwtConverterPropertiesCondition extends AnyNestedCondition {
210+
211+
JwtConverterPropertiesCondition() {
212+
super(ConfigurationPhase.REGISTER_BEAN);
213+
}
214+
215+
@ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "authority-prefix")
216+
static class OnAuthorityPrefix {
217+
218+
}
219+
220+
@ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "principal-claim-name")
221+
static class OnPrincipalClaimName {
222+
223+
}
224+
225+
@ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "authorities-claim-name")
226+
static class OnAuthoritiesClaimName {
227+
228+
}
229+
230+
}
231+
176232
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/Oauth2ResourceServerConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ class Oauth2ResourceServerConfiguration {
3232
@Configuration(proxyBeanMethods = false)
3333
@ConditionalOnClass(JwtDecoder.class)
3434
@Import({ OAuth2ResourceServerJwtConfiguration.JwtDecoderConfiguration.class,
35-
OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration.class })
35+
OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration.class,
36+
OAuth2ResourceServerJwtConfiguration.JwtConverterConfiguration.class })
3637
static class JwtConfiguration {
3738

3839
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2012-2023 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.boot.autoconfigure.security.oauth2.resource;
18+
19+
import java.time.Instant;
20+
import java.util.UUID;
21+
import java.util.stream.Stream;
22+
23+
import org.junit.jupiter.api.Named;
24+
import org.junit.jupiter.api.extension.ExtensionContext;
25+
import org.junit.jupiter.params.provider.Arguments;
26+
import org.junit.jupiter.params.provider.ArgumentsProvider;
27+
28+
import org.springframework.security.oauth2.jwt.Jwt;
29+
30+
/**
31+
* {@link ArgumentsProvider Arguments provider} supplying different Spring Boot properties
32+
* to customize JWT converter behavior, JWT token for conversion, expected principal name
33+
* and expected authorities.
34+
*
35+
* @author Yan Kardziyaka
36+
*/
37+
public final class JwtConverterCustomizationsArgumentsProvider implements ArgumentsProvider {
38+
39+
@Override
40+
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) {
41+
String customPrefix = "CUSTOM_AUTHORITY_PREFIX_";
42+
String customDelimiter = "[~,#:]";
43+
String customAuthoritiesClaim = "custom_authorities";
44+
String customPrincipalClaim = "custom_principal";
45+
String jwkSetUriProperty = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com";
46+
String authorityPrefixProperty = "spring.security.oauth2.resourceserver.jwt.authority-prefix=" + customPrefix;
47+
String authoritiesDelimiterProperty = "spring.security.oauth2.resourceserver.jwt.authorities-claim-delimiter="
48+
+ customDelimiter;
49+
String authoritiesClaimProperty = "spring.security.oauth2.resourceserver.jwt.authorities-claim-name="
50+
+ customAuthoritiesClaim;
51+
String principalClaimProperty = "spring.security.oauth2.resourceserver.jwt.principal-claim-name="
52+
+ customPrincipalClaim;
53+
String[] customPrefixProps = { jwkSetUriProperty, authorityPrefixProperty };
54+
String[] customDelimiterProps = { jwkSetUriProperty, authorityPrefixProperty, authoritiesDelimiterProperty };
55+
String[] customAuthoritiesClaimProps = { jwkSetUriProperty, authoritiesClaimProperty };
56+
String[] customPrincipalClaimProps = { jwkSetUriProperty, principalClaimProperty };
57+
String[] allJwtConverterProps = { jwkSetUriProperty, authorityPrefixProperty, authoritiesDelimiterProperty,
58+
authoritiesClaimProperty, principalClaimProperty };
59+
String[] jwtScopes = { "custom_scope0", "custom_scope1" };
60+
String subjectValue = UUID.randomUUID().toString();
61+
String customPrincipalValue = UUID.randomUUID().toString();
62+
Jwt.Builder jwtBuilder = Jwt.withTokenValue("token")
63+
.header("alg", "none")
64+
.expiresAt(Instant.MAX)
65+
.issuedAt(Instant.MIN)
66+
.issuer("https://issuer.example.org")
67+
.jti("jti")
68+
.notBefore(Instant.MIN)
69+
.subject(subjectValue)
70+
.claim(customPrincipalClaim, customPrincipalValue);
71+
Jwt noAuthoritiesCustomizationsJwt = jwtBuilder.claim("scp", jwtScopes[0] + " " + jwtScopes[1]).build();
72+
Jwt customAuthoritiesDelimiterJwt = jwtBuilder.claim("scp", jwtScopes[0] + "~" + jwtScopes[1]).build();
73+
Jwt customAuthoritiesClaimJwt = jwtBuilder.claim("scp", null)
74+
.claim(customAuthoritiesClaim, jwtScopes[0] + " " + jwtScopes[1])
75+
.build();
76+
Jwt customAuthoritiesClaimAndDelimiterJwt = jwtBuilder.claim("scp", null)
77+
.claim(customAuthoritiesClaim, jwtScopes[0] + "~" + jwtScopes[1])
78+
.build();
79+
String[] customPrefixAuthorities = { customPrefix + jwtScopes[0], customPrefix + jwtScopes[1] };
80+
String[] defaultPrefixAuthorities = { "SCOPE_" + jwtScopes[0], "SCOPE_" + jwtScopes[1] };
81+
return Stream.of(
82+
Arguments.of(Named.named("Custom prefix for GrantedAuthority", customPrefixProps),
83+
noAuthoritiesCustomizationsJwt, subjectValue, customPrefixAuthorities),
84+
Arguments.of(Named.named("Custom delimiter for JWT scopes", customDelimiterProps),
85+
customAuthoritiesDelimiterJwt, subjectValue, customPrefixAuthorities),
86+
Arguments.of(Named.named("Custom JWT authority claim name", customAuthoritiesClaimProps),
87+
customAuthoritiesClaimJwt, subjectValue, defaultPrefixAuthorities),
88+
Arguments.of(Named.named("Custom JWT principal claim name", customPrincipalClaimProps),
89+
noAuthoritiesCustomizationsJwt, customPrincipalValue, defaultPrefixAuthorities),
90+
Arguments.of(Named.named("All JWT converter customizations", allJwtConverterProps),
91+
customAuthoritiesClaimAndDelimiterJwt, customPrincipalValue, customPrefixAuthorities));
92+
}
93+
94+
}

0 commit comments

Comments
 (0)