Skip to content

Commit 93e35c9

Browse files
committed
Auto-configure a JwtAuthenticationConverter
Closes spring-projectsgh-33689
1 parent 3992817 commit 93e35c9

File tree

8 files changed

+348
-1
lines changed

8 files changed

+348
-1
lines changed

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

+56
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

+1
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

+33
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.boot.autoconfigure.security.oauth2.resource.IssuerUriCondition;
3333
import org.springframework.boot.autoconfigure.security.oauth2.resource.KeyValueCondition;
3434
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
35+
import org.springframework.boot.context.properties.PropertyMapper;
3536
import org.springframework.context.annotation.Bean;
3637
import org.springframework.context.annotation.Conditional;
3738
import org.springframework.context.annotation.Configuration;
@@ -49,6 +50,9 @@
4950
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder;
5051
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
5152
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;
5256
import org.springframework.security.web.server.SecurityWebFilterChain;
5357
import org.springframework.security.web.server.WebFilterChainProxy;
5458
import org.springframework.util.CollectionUtils;
@@ -64,6 +68,7 @@
6468
* @author Anastasiia Losieva
6569
* @author Mushtaq Ahmed
6670
* @author Roman Golovin
71+
* @author Yan Kardziyaka
6772
*/
6873
@Configuration(proxyBeanMethods = false)
6974
class ReactiveOAuth2ResourceServerJwkConfiguration {
@@ -163,6 +168,34 @@ SupplierReactiveJwtDecoder jwtDecoderByIssuerUri(
163168

164169
}
165170

171+
@Configuration(proxyBeanMethods = false)
172+
@ConditionalOnMissingBean(ReactiveJwtAuthenticationConverter.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+
166199
@Configuration(proxyBeanMethods = false)
167200
@ConditionalOnMissingBean(SecurityWebFilterChain.class)
168201
static class WebSecurityConfiguration {

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

+31
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.boot.autoconfigure.security.oauth2.resource.IssuerUriCondition;
3434
import org.springframework.boot.autoconfigure.security.oauth2.resource.KeyValueCondition;
3535
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
36+
import org.springframework.boot.context.properties.PropertyMapper;
3637
import org.springframework.context.annotation.Bean;
3738
import org.springframework.context.annotation.Conditional;
3839
import org.springframework.context.annotation.Configuration;
@@ -48,6 +49,8 @@
4849
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
4950
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder;
5051
import org.springframework.security.oauth2.jwt.SupplierJwtDecoder;
52+
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
53+
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
5154
import org.springframework.security.web.SecurityFilterChain;
5255
import org.springframework.util.CollectionUtils;
5356

@@ -63,6 +66,7 @@
6366
* @author HaiTao Zhang
6467
* @author Mushtaq Ahmed
6568
* @author Roman Golovin
69+
* @author Yan Kardziyaka
6670
*/
6771
@Configuration(proxyBeanMethods = false)
6872
class OAuth2ResourceServerJwtConfiguration {
@@ -173,4 +177,31 @@ SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) throws Exception {
173177

174178
}
175179

180+
@Configuration(proxyBeanMethods = false)
181+
@ConditionalOnMissingBean(JwtAuthenticationConverter.class)
182+
static class JwtConverterConfiguration {
183+
184+
private final OAuth2ResourceServerProperties.Jwt properties;
185+
186+
JwtConverterConfiguration(OAuth2ResourceServerProperties properties) {
187+
this.properties = properties.getJwt();
188+
}
189+
190+
@Bean
191+
JwtAuthenticationConverter getJwtAuthenticationConverter() {
192+
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
193+
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
194+
map.from(this.properties.getAuthorityPrefix()).to(grantedAuthoritiesConverter::setAuthorityPrefix);
195+
map.from(this.properties.getAuthoritiesClaimDelimiter())
196+
.to(grantedAuthoritiesConverter::setAuthoritiesClaimDelimiter);
197+
map.from(this.properties.getAuthoritiesClaimName())
198+
.to(grantedAuthoritiesConverter::setAuthoritiesClaimName);
199+
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
200+
map.from(this.properties.getPrincipalClaimName()).to(jwtAuthenticationConverter::setPrincipalClaimName);
201+
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
202+
return jwtAuthenticationConverter;
203+
}
204+
205+
}
206+
176207
}

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

+2-1
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,104 @@
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+
46+
String jwkSetUriProperty = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com";
47+
String authorityPrefixProperty = "spring.security.oauth2.resourceserver.jwt.authority-prefix=" + customPrefix;
48+
String authoritiesDelimiterProperty = "spring.security.oauth2.resourceserver.jwt.authorities-claim-delimiter="
49+
+ customDelimiter;
50+
String authoritiesClaimProperty = "spring.security.oauth2.resourceserver.jwt.authorities-claim-name="
51+
+ customAuthoritiesClaim;
52+
String principalClaimProperty = "spring.security.oauth2.resourceserver.jwt.principal-claim-name="
53+
+ customPrincipalClaim;
54+
55+
String[] noJwtConverterProps = { jwkSetUriProperty };
56+
String[] customPrefixProps = { jwkSetUriProperty, authorityPrefixProperty };
57+
String[] customDelimiterProps = { jwkSetUriProperty, authoritiesDelimiterProperty };
58+
String[] customAuthoritiesClaimProps = { jwkSetUriProperty, authoritiesClaimProperty };
59+
String[] customPrincipalClaimProps = { jwkSetUriProperty, principalClaimProperty };
60+
String[] allJwtConverterProps = { jwkSetUriProperty, authorityPrefixProperty, authoritiesDelimiterProperty,
61+
authoritiesClaimProperty, principalClaimProperty };
62+
63+
String[] jwtScopes = { "custom_scope0", "custom_scope1" };
64+
String subjectValue = UUID.randomUUID().toString();
65+
String customPrincipalValue = UUID.randomUUID().toString();
66+
67+
Jwt.Builder jwtBuilder = Jwt.withTokenValue("token")
68+
.header("alg", "none")
69+
.expiresAt(Instant.MAX)
70+
.issuedAt(Instant.MIN)
71+
.issuer("https://issuer.example.org")
72+
.jti("jti")
73+
.notBefore(Instant.MIN)
74+
.subject(subjectValue)
75+
.claim(customPrincipalClaim, customPrincipalValue);
76+
77+
Jwt noAuthoritiesCustomizationsJwt = jwtBuilder.claim("scp", jwtScopes[0] + " " + jwtScopes[1]).build();
78+
Jwt customAuthoritiesDelimiterJwt = jwtBuilder.claim("scp", jwtScopes[0] + "~" + jwtScopes[1]).build();
79+
Jwt customAuthoritiesClaimJwt = jwtBuilder.claim("scp", null)
80+
.claim(customAuthoritiesClaim, jwtScopes[0] + " " + jwtScopes[1])
81+
.build();
82+
Jwt customAuthoritiesClaimAndDelimiterJwt = jwtBuilder.claim("scp", null)
83+
.claim(customAuthoritiesClaim, jwtScopes[0] + "~" + jwtScopes[1])
84+
.build();
85+
86+
String[] customPrefixAuthorities = { customPrefix + jwtScopes[0], customPrefix + jwtScopes[1] };
87+
String[] defaultPrefixAuthorities = { "SCOPE_" + jwtScopes[0], "SCOPE_" + jwtScopes[1] };
88+
89+
return Stream.of(
90+
Arguments.of(Named.named("No JWT converter customizations", noJwtConverterProps),
91+
noAuthoritiesCustomizationsJwt, subjectValue, defaultPrefixAuthorities),
92+
Arguments.of(Named.named("Custom prefix for GrantedAuthority", customPrefixProps),
93+
noAuthoritiesCustomizationsJwt, subjectValue, customPrefixAuthorities),
94+
Arguments.of(Named.named("Custom delimiter for JWT scopes", customDelimiterProps),
95+
customAuthoritiesDelimiterJwt, subjectValue, defaultPrefixAuthorities),
96+
Arguments.of(Named.named("Custom JWT authority claim name", customAuthoritiesClaimProps),
97+
customAuthoritiesClaimJwt, subjectValue, defaultPrefixAuthorities),
98+
Arguments.of(Named.named("Custom JWT principal claim name", customPrincipalClaimProps),
99+
noAuthoritiesCustomizationsJwt, customPrincipalValue, defaultPrefixAuthorities),
100+
Arguments.of(Named.named("All JWT converter customizations", allJwtConverterProps),
101+
customAuthoritiesClaimAndDelimiterJwt, customPrincipalValue, customPrefixAuthorities));
102+
}
103+
104+
}

0 commit comments

Comments
 (0)