Skip to content

Commit 4df49bd

Browse files
thomasdarimontThomas Darimont
authored and
Thomas Darimont
committed
GH-15201 Introduce ExpressionJwtGrantedAuthoritiesConverter to extract nested authorities with an expression
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 4df49bd

File tree

3 files changed

+223
-1
lines changed

3 files changed

+223
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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.authentication;
18+
19+
import org.apache.commons.logging.Log;
20+
import org.apache.commons.logging.LogFactory;
21+
import org.springframework.core.convert.converter.Converter;
22+
import org.springframework.core.log.LogMessage;
23+
import org.springframework.expression.Expression;
24+
import org.springframework.expression.ExpressionException;
25+
import org.springframework.security.core.GrantedAuthority;
26+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
27+
import org.springframework.security.oauth2.jwt.Jwt;
28+
import org.springframework.util.Assert;
29+
30+
import java.util.ArrayList;
31+
import java.util.Collection;
32+
import java.util.Collections;
33+
34+
import static org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter.castAuthoritiesToCollection;
35+
36+
/**
37+
* Uses an expression for extracting the token claim value to use for mapping
38+
* {@link GrantedAuthority authorities}.
39+
*
40+
* Note this can be used in combination with a
41+
* {@link DelegatingJwtGrantedAuthoritiesConverter}.
42+
*
43+
* @author Thomas Darimont
44+
* @since 6.4
45+
*/
46+
public class ExpressionJwtGrantedAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
47+
48+
private final Log logger = LogFactory.getLog(getClass());
49+
50+
private String authorityPrefix = "";
51+
52+
private final Expression authoritiesClaimExpression;
53+
54+
/**
55+
* Constructs a {@link ExpressionJwtGrantedAuthoritiesConverter} using the provided
56+
* {@code authoritiesClaimExpression}.
57+
* @param authoritiesClaimExpression The token claim SpEL Expression to map
58+
* authorities from.
59+
*/
60+
public ExpressionJwtGrantedAuthoritiesConverter(Expression authoritiesClaimExpression) {
61+
Assert.notNull(authoritiesClaimExpression, "authoritiesClaimExpression must not be null");
62+
this.authoritiesClaimExpression = authoritiesClaimExpression;
63+
}
64+
65+
/**
66+
* Sets the prefix to use for {@link GrantedAuthority authorities} mapped by this
67+
* converter. Defaults to {@code ""}.
68+
* @param authorityPrefix The authority prefix
69+
*/
70+
public void setAuthorityPrefix(String authorityPrefix) {
71+
Assert.notNull(authorityPrefix, "authorityPrefix cannot be null");
72+
this.authorityPrefix = authorityPrefix;
73+
}
74+
75+
/**
76+
* Extract {@link GrantedAuthority}s from the given {@link Jwt}.
77+
* @param jwt The {@link Jwt} token
78+
* @return The {@link GrantedAuthority authorities} read from the token scopes
79+
*/
80+
@Override
81+
public Collection<GrantedAuthority> convert(Jwt jwt) {
82+
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
83+
for (String authority : getAuthorities(jwt)) {
84+
grantedAuthorities.add(new SimpleGrantedAuthority(this.authorityPrefix + authority));
85+
}
86+
return grantedAuthorities;
87+
}
88+
89+
private Collection<String> getAuthorities(Jwt jwt) {
90+
Object authorities;
91+
try {
92+
if (this.logger.isTraceEnabled()) {
93+
this.logger.trace(LogMessage.format("Looking for authorities with expression. expression=%s",
94+
authoritiesClaimExpression.getExpressionString()));
95+
}
96+
authorities = authoritiesClaimExpression.getValue(jwt.getClaims(), Collection.class);
97+
if (this.logger.isTraceEnabled()) {
98+
this.logger.trace(LogMessage.format("Found authorities with expression. authorities=%s", authorities));
99+
}
100+
}
101+
catch (ExpressionException ee) {
102+
if (this.logger.isTraceEnabled()) {
103+
this.logger.trace(LogMessage.format("Failed to evaluate expression. error=%s", ee.getMessage()));
104+
}
105+
authorities = Collections.emptyList();
106+
}
107+
108+
if (authorities != null) {
109+
return castAuthoritiesToCollection(authorities);
110+
}
111+
return Collections.emptyList();
112+
}
113+
114+
public Expression getAuthoritiesClaimExpression() {
115+
return authoritiesClaimExpression;
116+
}
117+
118+
public String getAuthorityPrefix() {
119+
return authorityPrefix;
120+
}
121+
122+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ private Collection<String> getAuthorities(Jwt jwt) {
140140
}
141141

142142
@SuppressWarnings("unchecked")
143-
private Collection<String> castAuthoritiesToCollection(Object authorities) {
143+
static Collection<String> castAuthoritiesToCollection(Object authorities) {
144144
return (Collection<String>) authorities;
145145
}
146146

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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.authentication;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.springframework.expression.spel.standard.SpelExpression;
21+
import org.springframework.expression.spel.standard.SpelExpressionParser;
22+
import org.springframework.security.core.GrantedAuthority;
23+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
24+
import org.springframework.security.oauth2.jwt.Jwt;
25+
import org.springframework.security.oauth2.jwt.TestJwts;
26+
27+
import java.util.Arrays;
28+
import java.util.Collection;
29+
import java.util.Collections;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
33+
/**
34+
* Tests for {@link ExpressionJwtGrantedAuthoritiesConverter}
35+
*
36+
* @author Thomas Darimont
37+
* @since 6.4
38+
*/
39+
public class ExpressionJwtGrantedAuthoritiesConverterTests {
40+
41+
@Test
42+
public void convertWhenTokenHasCustomClaimNameExpressionThenCustomClaimNameAttributeIsTranslatedToAuthorities() {
43+
// @formatter:off
44+
Jwt jwt = TestJwts.jwt()
45+
.claim("nested", Collections.singletonMap("roles", Arrays.asList("role1", "role2")))
46+
.build();
47+
// @formatter:on
48+
SpelExpression expression = new SpelExpressionParser().parseRaw("[nested][roles]");
49+
ExpressionJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new ExpressionJwtGrantedAuthoritiesConverter(
50+
expression);
51+
Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
52+
assertThat(authorities).containsExactly(new SimpleGrantedAuthority("role1"),
53+
new SimpleGrantedAuthority("role2"));
54+
}
55+
56+
@Test
57+
public void convertToEmptyListWhenTokenClaimExpressionYieldsNull() {
58+
// @formatter:off
59+
Jwt jwt = TestJwts.jwt()
60+
.claim("nested", Collections.singletonMap("roles", null))
61+
.build();
62+
// @formatter:on
63+
SpelExpression expression = new SpelExpressionParser().parseRaw("[nested][roles]");
64+
ExpressionJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new ExpressionJwtGrantedAuthoritiesConverter(
65+
expression);
66+
Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
67+
assertThat(authorities).isEmpty();
68+
}
69+
70+
@Test
71+
public void convertWhenTokenHasCustomClaimNameExpressionThenCustomClaimNameAttributeIsTranslatedToAuthoritiesWithPrefix() {
72+
// @formatter:off
73+
Jwt jwt = TestJwts.jwt()
74+
.claim("nested", Collections.singletonMap("roles", Arrays.asList("role1", "role2")))
75+
.build();
76+
// @formatter:on
77+
SpelExpression expression = new SpelExpressionParser().parseRaw("[nested][roles]");
78+
ExpressionJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new ExpressionJwtGrantedAuthoritiesConverter(
79+
expression);
80+
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("CUSTOM_");
81+
Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
82+
assertThat(authorities).containsExactly(new SimpleGrantedAuthority("CUSTOM_role1"),
83+
new SimpleGrantedAuthority("CUSTOM_role2"));
84+
}
85+
86+
@Test
87+
public void convertWhenTokenHasCustomInvalidClaimNameExpressionThenCustomClaimNameAttributeIsTranslatedToEmptyAuthorities() {
88+
// @formatter:off
89+
Jwt jwt = TestJwts.jwt()
90+
.claim("other", Collections.singletonMap("roles", Arrays.asList("role1", "role2")))
91+
.build();
92+
// @formatter:on
93+
SpelExpression expression = new SpelExpressionParser().parseRaw("[nested][roles]");
94+
ExpressionJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new ExpressionJwtGrantedAuthoritiesConverter(
95+
expression);
96+
Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
97+
assertThat(authorities).isEmpty();
98+
}
99+
100+
}

0 commit comments

Comments
 (0)