Skip to content

Commit 20a08b7

Browse files
GH-15201 Support extracting nested authorities in JwtGrantedAuthoritiesConverter
This helps to reduce custom code necessary to extract roles from deeply nested claims. Fixes #15201 Signed-off-by: Thomas Darimont <[email protected]>
1 parent 3acd2c6 commit 20a08b7

File tree

2 files changed

+65
-8
lines changed

2 files changed

+65
-8
lines changed

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

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@
2424
import org.apache.commons.logging.Log;
2525
import org.apache.commons.logging.LogFactory;
2626

27+
import org.springframework.core.ParameterizedTypeReference;
2728
import org.springframework.core.convert.converter.Converter;
2829
import org.springframework.core.log.LogMessage;
30+
import org.springframework.expression.Expression;
31+
import org.springframework.expression.ExpressionException;
2932
import org.springframework.security.core.GrantedAuthority;
3033
import org.springframework.security.core.authority.SimpleGrantedAuthority;
3134
import org.springframework.security.oauth2.jwt.Jwt;
@@ -55,6 +58,8 @@ public final class JwtGrantedAuthoritiesConverter implements Converter<Jwt, Coll
5558

5659
private String authoritiesClaimName;
5760

61+
private Expression authoritiesClaimExpression;
62+
5863
/**
5964
* Extract {@link GrantedAuthority}s from the given {@link Jwt}.
6065
* @param jwt The {@link Jwt} token
@@ -117,16 +122,38 @@ private String getAuthoritiesClaimName(Jwt jwt) {
117122
return null;
118123
}
119124

125+
/**
126+
* Sets the expression for extracting the token claim to use for mapping {@link GrantedAuthority
127+
* authorities} by this converter.
128+
* Note that this takes precedence over {@link #setAuthoritiesClaimName(String)}.
129+
* @param authoritiesClaimExpression The token claim SpEL Expression to map authorities
130+
* @since 6.5
131+
*/
132+
public void setAuthoritiesClaimExpression(Expression authoritiesClaimExpression) {
133+
Assert.notNull(authoritiesClaimExpression, "authoritiesClaimExpression must not be null");
134+
this.authoritiesClaimExpression = authoritiesClaimExpression;
135+
}
136+
120137
private Collection<String> getAuthorities(Jwt jwt) {
121-
String claimName = getAuthoritiesClaimName(jwt);
122-
if (claimName == null) {
123-
this.logger.trace("Returning no authorities since could not find any claims that might contain scopes");
124-
return Collections.emptyList();
125-
}
126-
if (this.logger.isTraceEnabled()) {
127-
this.logger.trace(LogMessage.format("Looking for scopes in claim %s", claimName));
138+
139+
Object authorities;
140+
if (authoritiesClaimExpression != null) {
141+
try {
142+
authorities = authoritiesClaimExpression.getValue(jwt.getClaims(), Collection.class);
143+
} catch (ExpressionException ee) {
144+
authorities = Collections.emptyList();
145+
}
146+
} else {
147+
String claimName = getAuthoritiesClaimName(jwt);
148+
if (claimName == null) {
149+
this.logger.trace("Returning no authorities since could not find any claims that might contain scopes");
150+
return Collections.emptyList();
151+
}
152+
if (this.logger.isTraceEnabled()) {
153+
this.logger.trace(LogMessage.format("Looking for scopes in claim %s", claimName));
154+
}
155+
authorities = jwt.getClaim(claimName);
128156
}
129-
Object authorities = jwt.getClaim(claimName);
130157
if (authorities instanceof String) {
131158
if (StringUtils.hasText((String) authorities)) {
132159
return Arrays.asList(((String) authorities).split(this.authoritiesClaimDelimiter));

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@
1919
import java.util.Arrays;
2020
import java.util.Collection;
2121
import java.util.Collections;
22+
import java.util.Map;
2223

2324
import org.junit.jupiter.api.Test;
2425

26+
import org.springframework.expression.Expression;
27+
import org.springframework.expression.spel.standard.SpelExpressionParser;
2528
import org.springframework.security.core.GrantedAuthority;
2629
import org.springframework.security.core.authority.SimpleGrantedAuthority;
2730
import org.springframework.security.oauth2.jwt.Jwt;
@@ -229,6 +232,33 @@ public void convertWhenTokenHasCustomClaimNameThenCustomClaimNameAttributeIsTran
229232
new SimpleGrantedAuthority("SCOPE_message:write"));
230233
}
231234

235+
@Test
236+
public void convertWhenTokenHasCustomClaimNameExpressionThenCustomClaimNameAttributeIsTranslatedToAuthorities() {
237+
// @formatter:off
238+
Jwt jwt = TestJwts.jwt()
239+
.claim("nested", Map.of("roles", Arrays.asList("role1", "role2")))
240+
.build();
241+
// @formatter:on
242+
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
243+
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimExpression(new SpelExpressionParser().parseRaw("[nested][roles]"));
244+
Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
245+
assertThat(authorities).containsExactly(new SimpleGrantedAuthority("SCOPE_role1"),
246+
new SimpleGrantedAuthority("SCOPE_role2"));
247+
}
248+
249+
@Test
250+
public void convertWhenTokenHasCustomInvalidClaimNameExpressionThenCustomClaimNameAttributeIsTranslatedToEmptyAuthorities() {
251+
// @formatter:off
252+
Jwt jwt = TestJwts.jwt()
253+
.claim("other", Map.of("roles", Arrays.asList("role1", "role2")))
254+
.build();
255+
// @formatter:on
256+
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
257+
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimExpression(new SpelExpressionParser().parseRaw("[nested][roles]"));
258+
Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
259+
assertThat(authorities).isEmpty();
260+
}
261+
232262
@Test
233263
public void convertWhenTokenHasEmptyCustomClaimNameThenCustomClaimNameAttributeIsTranslatedToNoAuthorities() {
234264
// @formatter:off

0 commit comments

Comments
 (0)