Skip to content

Commit dc962ce

Browse files
authored
Enhance the token authentication converter to accept the custom jwt granted authorities converter (Azure#32335)
1 parent 3df0013 commit dc962ce

File tree

7 files changed

+180
-18
lines changed

7 files changed

+180
-18
lines changed

eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ the main ServiceBusClientBuilder. -->
399399
<suppress checks="PackageName" files="com.azure.spring.cloud.autoconfigure.aadb2c.*" />
400400
<!-- Suppress package name warning: oauth2 is the protocol name. -->
401401
<suppress checks="PackageName" files="com.azure.spring.cloud.autoconfigure.aad.implementation.oauth2" />
402+
<suppress checks="EnforceFinalFields" files="com.azure.spring.cloud.autoconfigure.aad.AadResourceServerWebSecurityConfigurerAdapter"/>
402403
<!-- The package name length limit -->
403404
<suppress checks="PackageName" files="com.azure.spring.cloud.autoconfigure.implementation.keyvault.certificates.properties.AzureKeyVaultCertificateProperties"/>
404405
<suppress checks="PackageName" files="com.azure.spring.cloud.autoconfigure.implementation.keyvault.certificates.properties.package-info.java"/>

sdk/spring/CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ Upgrade Spring Boot dependencies version to 2.7.4 and Spring Cloud dependencies
77
### Spring Cloud Azure Autoconfigure
88
This section includes changes in `spring-cloud-azure-autoconfigure` module.
99

10-
#### Dependency Updates
11-
- Upgrade spring-security to 5.7.5 to address [CVE-2022-31690](https://tanzu.vmware.com/security/cve-2022-31690) [#32145](https://github.com/Azure/azure-sdk-for-java/pull/32145).
12-
1310
#### Features Added
1411
- Remove warning logs of Kafka passwordless autoconfiguration. [#31182](https://github.com/Azure/azure-sdk-for-java/issues/31182)
12+
- Enhance the token authentication converter and Azure AD Resource Server configurer adapter to accept the custom jwt granted authorities converter. [#28665](https://github.com/Azure/azure-sdk-for-java/issues/28665)
13+
14+
#### Dependency Updates
15+
- Upgrade spring-security to 5.7.5 to address [CVE-2022-31690](https://tanzu.vmware.com/security/cve-2022-31690) [#32145](https://github.com/Azure/azure-sdk-for-java/pull/32145).
1516

1617
## 4.4.1 (2022-10-31)
1718

sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadJwtBearerTokenAuthenticationConverter.java

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
@Deprecated
3131
public class AadJwtBearerTokenAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
3232

33-
private final Converter<Jwt, Collection<GrantedAuthority>> converter;
33+
private final Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter;
3434
private final String principalClaimName;
3535

3636
/**
@@ -58,20 +58,42 @@ public AadJwtBearerTokenAuthenticationConverter(String authoritiesClaimName) {
5858
*/
5959
public AadJwtBearerTokenAuthenticationConverter(String authoritiesClaimName,
6060
String authorityPrefix) {
61-
this(null, buildClaimToAuthorityPrefixMap(authoritiesClaimName, authorityPrefix));
61+
this(AadJwtClaimNames.SUB, buildClaimToAuthorityPrefixMap(authoritiesClaimName, authorityPrefix));
6262
}
6363

6464
/**
6565
* Using spring security provides JwtGrantedAuthoritiesConverter, it can resolve the access token of scp or roles.
6666
*
67-
* @param principalClaimName authorities claim name
67+
* @param principalClaimName the claim name for the principal
6868
* @param claimToAuthorityPrefixMap the authority name and prefix map
6969
*/
7070
public AadJwtBearerTokenAuthenticationConverter(String principalClaimName,
7171
Map<String, String> claimToAuthorityPrefixMap) {
7272
Assert.notNull(claimToAuthorityPrefixMap, "claimToAuthorityPrefixMap cannot be null");
7373
this.principalClaimName = principalClaimName;
74-
this.converter = new AadJwtGrantedAuthoritiesConverter(claimToAuthorityPrefixMap);
74+
this.jwtGrantedAuthoritiesConverter = new AadJwtGrantedAuthoritiesConverter(claimToAuthorityPrefixMap);
75+
}
76+
77+
/**
78+
* Using the custom JwtGrantedAuthoritiesConverter.
79+
*
80+
* @param jwtGrantedAuthoritiesConverter the custom granted authority converter
81+
*/
82+
public AadJwtBearerTokenAuthenticationConverter(Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter) {
83+
this(AadJwtClaimNames.SUB, jwtGrantedAuthoritiesConverter);
84+
}
85+
86+
/**
87+
* Using the principal claim name and the custom JwtGrantedAuthoritiesConverter.
88+
*
89+
* @param principalClaimName the claim name for the principal
90+
* @param jwtGrantedAuthoritiesConverter the custom granted authority converter
91+
*/
92+
public AadJwtBearerTokenAuthenticationConverter(String principalClaimName,
93+
Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter) {
94+
Assert.notNull(jwtGrantedAuthoritiesConverter, "jwtGrantedAuthoritiesConverter cannot be null");
95+
this.principalClaimName = principalClaimName;
96+
this.jwtGrantedAuthoritiesConverter = jwtGrantedAuthoritiesConverter;
7597
}
7698

7799
/**
@@ -85,7 +107,7 @@ public AbstractAuthenticationToken convert(Jwt jwt) {
85107
OAuth2AccessToken accessToken = new OAuth2AccessToken(
86108
OAuth2AccessToken.TokenType.BEARER, jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt());
87109
Map<String, Object> claims = jwt.getClaims();
88-
Collection<GrantedAuthority> authorities = converter.convert(jwt);
110+
Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
89111
OAuth2AuthenticatedPrincipal principal = new AadOAuth2AuthenticatedPrincipal(
90112
jwt.getHeaders(), claims, authorities, jwt.getTokenValue(), (String) claims.get(principalClaimName));
91113
return new BearerTokenAuthentication(principal, accessToken, authorities);

sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/AadResourceServerWebSecurityConfigurerAdapter.java

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@
1010
import org.springframework.security.authentication.AbstractAuthenticationToken;
1111
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
1212
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
13+
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
14+
import org.springframework.security.core.GrantedAuthority;
1315
import org.springframework.security.oauth2.jwt.Jwt;
1416
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
17+
import org.springframework.util.Assert;
1518
import org.springframework.util.StringUtils;
1619

20+
import java.util.Collection;
21+
1722
/**
1823
* Abstract configuration class, used to make JwtConfigurer and AADJwtBearerTokenAuthenticationConverter take effect.
1924
*
@@ -22,10 +27,32 @@
2227
public abstract class AadResourceServerWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
2328

2429
@Autowired
25-
AadResourceServerProperties properties;
30+
private AadResourceServerProperties properties;
31+
32+
private Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter;
2633

2734
/**
28-
* configure
35+
* Creates a new instance with the default configuration.
36+
*/
37+
public AadResourceServerWebSecurityConfigurerAdapter() {
38+
}
39+
40+
/**
41+
* Sets the Azure AD properties and custom granted authority converter to creates a new instance,
42+
* the custom granted authority converter can be null.
43+
*
44+
* @param properties the Azure AD properties for Resource Server
45+
* @param jwtGrantedAuthoritiesConverter the custom converter for JWT granted authority
46+
*/
47+
public AadResourceServerWebSecurityConfigurerAdapter(AadResourceServerProperties properties,
48+
Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter) {
49+
Assert.notNull(jwtGrantedAuthoritiesConverter, "jwtGrantedAuthoritiesConverter cannot be null");
50+
this.properties = properties;
51+
this.jwtGrantedAuthoritiesConverter = jwtGrantedAuthoritiesConverter;
52+
}
53+
54+
/**
55+
* Apply the {@link OAuth2ResourceServerConfigurer} for Azure AD OAuth2 Resource Server scenario.
2956
*
3057
* @param http the {@link HttpSecurity} to use
3158
* @throws Exception Configuration failed
@@ -45,8 +72,22 @@ private Converter<Jwt, AbstractAuthenticationToken> jwtAuthenticationConverter()
4572
if (StringUtils.hasText(properties.getPrincipalClaimName())) {
4673
converter.setPrincipalClaimName(properties.getPrincipalClaimName());
4774
}
48-
converter.setJwtGrantedAuthoritiesConverter(
49-
new AadJwtGrantedAuthoritiesConverter(properties.getClaimToAuthorityPrefixMap()));
75+
76+
this.jwtGrantedAuthoritiesConverter = jwtGrantedAuthoritiesConverter();
77+
if (this.jwtGrantedAuthoritiesConverter != null) {
78+
converter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
79+
} else {
80+
converter.setJwtGrantedAuthoritiesConverter(
81+
new AadJwtGrantedAuthoritiesConverter(properties.getClaimToAuthorityPrefixMap()));
82+
}
5083
return converter;
5184
}
85+
86+
/**
87+
* Customize the Jwt granted authority converter, and return the {@link AadResourceServerWebSecurityConfigurerAdapter#jwtGrantedAuthoritiesConverter} by default.
88+
* @return the Jwt granted authority converter.
89+
*/
90+
protected Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter() {
91+
return this.jwtGrantedAuthoritiesConverter;
92+
}
5293
}

sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/aad/configuration/AadResourceServerConfiguration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public static class DefaultAadResourceServerWebSecurityConfigurerAdapter extends
109109
AadResourceServerWebSecurityConfigurerAdapter {
110110

111111
/**
112-
* configure
112+
* Configure the default Resource Server for Azure AD.
113113
*
114114
* @param http the {@link HttpSecurity} to use
115115
* @throws Exception Configuration failed

sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/AadJwtBearerTokenAuthenticationConverterTests.java

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,29 @@
22
// Licensed under the MIT License.
33
package com.azure.spring.cloud.autoconfigure.aad;
44

5+
import com.azure.spring.cloud.autoconfigure.aad.implementation.constants.AadJwtClaimNames;
56
import com.azure.spring.cloud.autoconfigure.aad.implementation.oauth2.AadOAuth2AuthenticatedPrincipal;
67
import net.minidev.json.JSONArray;
78
import org.junit.jupiter.api.Assertions;
89
import org.junit.jupiter.api.BeforeAll;
910
import org.junit.jupiter.api.Test;
1011
import org.junit.jupiter.api.TestInstance;
12+
import org.springframework.core.convert.converter.Converter;
1113
import org.springframework.security.authentication.AbstractAuthenticationToken;
14+
import org.springframework.security.core.GrantedAuthority;
1215
import org.springframework.security.core.authority.SimpleGrantedAuthority;
1316
import org.springframework.security.oauth2.jwt.Jwt;
1417

1518
import java.time.Instant;
19+
import java.util.Collection;
1620
import java.util.HashMap;
1721
import java.util.Map;
1822

1923
import static org.assertj.core.api.Assertions.assertThat;
2024
import static org.mockito.Mockito.mock;
25+
import static org.mockito.Mockito.verify;
2126
import static org.mockito.Mockito.when;
27+
import static org.mockito.internal.verification.VerificationModeFactory.times;
2228

2329
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
2430
class AadJwtBearerTokenAuthenticationConverterTests {
@@ -32,6 +38,7 @@ class AadJwtBearerTokenAuthenticationConverterTests {
3238
void init() {
3339
claims.put("iss", "fake-issuer");
3440
claims.put("tid", "fake-tid");
41+
claims.put("name", "fake-user");
3542
headers.put("kid", "kg2LYs2T0CTjIfj4rt6JIynen38");
3643
when(jwt.getClaim("scp")).thenReturn("Order.read Order.write");
3744
when(jwt.getClaim("roles")).thenReturn(jsonArray);
@@ -62,7 +69,7 @@ void testNoArgumentsConstructorDefaultScopeAndRoleAuthorities() {
6269
AadOAuth2AuthenticatedPrincipal principal = (AadOAuth2AuthenticatedPrincipal) authenticationToken
6370
.getPrincipal();
6471
assertThat(principal.getAttributes()).isNotEmpty();
65-
assertThat(principal.getAttributes()).hasSize(2);
72+
assertThat(principal.getAttributes()).hasSize(3);
6673
assertThat(principal.getAuthorities()).hasSize(4);
6774
}
6875

@@ -74,7 +81,7 @@ void testNoArgumentsConstructorExtractScopeAuthorities() {
7481
AadOAuth2AuthenticatedPrincipal principal = (AadOAuth2AuthenticatedPrincipal) authenticationToken
7582
.getPrincipal();
7683
assertThat(principal.getAttributes()).isNotEmpty();
77-
assertThat(principal.getAttributes()).hasSize(2);
84+
assertThat(principal.getAttributes()).hasSize(3);
7885
assertThat(principal.getAuthorities()).hasSize(4);
7986
}
8087

@@ -86,7 +93,7 @@ void testParameterConstructorExtractScopeAuthorities() {
8693
AadOAuth2AuthenticatedPrincipal principal = (AadOAuth2AuthenticatedPrincipal) authenticationToken
8794
.getPrincipal();
8895
assertThat(principal.getAttributes()).isNotEmpty();
89-
assertThat(principal.getAttributes()).hasSize(2);
96+
assertThat(principal.getAttributes()).hasSize(3);
9097
assertThat(principal.getAuthorities()).hasSize(2);
9198
}
9299

@@ -99,7 +106,7 @@ void testParameterConstructorExtractRoleAuthorities() {
99106
AadOAuth2AuthenticatedPrincipal principal = (AadOAuth2AuthenticatedPrincipal) authenticationToken
100107
.getPrincipal();
101108
assertThat(principal.getAttributes()).isNotEmpty();
102-
assertThat(principal.getAttributes()).hasSize(2);
109+
assertThat(principal.getAttributes()).hasSize(3);
103110
assertThat(principal.getAuthorities()).hasSize(2);
104111
}
105112

@@ -113,9 +120,34 @@ void testConstructorExtractRoleAuthoritiesWithAuthorityPrefixMapParameter() {
113120
AadOAuth2AuthenticatedPrincipal principal = (AadOAuth2AuthenticatedPrincipal) authenticationToken
114121
.getPrincipal();
115122
assertThat(principal.getAttributes()).isNotEmpty();
116-
assertThat(principal.getAttributes()).hasSize(2);
123+
assertThat(principal.getAttributes()).hasSize(3);
117124
assertThat(principal.getAuthorities()).hasSize(2);
118125
Assertions.assertTrue(principal.getAuthorities().contains(new SimpleGrantedAuthority("APPROLE_User.read")));
119126
Assertions.assertTrue(principal.getAuthorities().contains(new SimpleGrantedAuthority("APPROLE_User.write")));
120127
}
128+
129+
@Test
130+
void createTokenAuthenticationConverterWithGrantedAuthorityConverter() {
131+
Converter<Jwt, Collection<GrantedAuthority>> grantedAuthorityConverter = mock(TestJwtGrantedAuthoritiesConverter.class);
132+
AadJwtBearerTokenAuthenticationConverter tokenAuthenticationConverter = new AadJwtBearerTokenAuthenticationConverter(grantedAuthorityConverter);
133+
tokenAuthenticationConverter.convert(jwt);
134+
verify(grantedAuthorityConverter, times(1)).convert(this.jwt);
135+
}
136+
137+
@Test
138+
void createTokenAuthenticationConverterWithClaimNameAndGrantedAuthorityConverter() {
139+
Converter<Jwt, Collection<GrantedAuthority>> grantedAuthorityConverter = mock(TestJwtGrantedAuthoritiesConverter.class);
140+
AadJwtBearerTokenAuthenticationConverter tokenAuthenticationConverter = new AadJwtBearerTokenAuthenticationConverter(AadJwtClaimNames.NAME, grantedAuthorityConverter);
141+
AbstractAuthenticationToken abstractAuthenticationToken = tokenAuthenticationConverter.convert(jwt);
142+
AadOAuth2AuthenticatedPrincipal principal = (AadOAuth2AuthenticatedPrincipal) abstractAuthenticationToken.getPrincipal();
143+
Assertions.assertEquals(principal.getName(), "fake-user");
144+
verify(grantedAuthorityConverter, times(1)).convert(this.jwt);
145+
}
146+
147+
class TestJwtGrantedAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
148+
@Override
149+
public Collection<GrantedAuthority> convert(Jwt source) {
150+
return null;
151+
}
152+
}
121153
}

sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/aad/implementation/webapi/AadResourceServerConfigurationTests.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,34 @@
22
// Licensed under the MIT License.
33
package com.azure.spring.cloud.autoconfigure.aad.implementation.webapi;
44

5+
import com.azure.spring.cloud.autoconfigure.aad.AadResourceServerWebSecurityConfigurerAdapter;
56
import com.azure.spring.cloud.autoconfigure.aad.configuration.AadResourceServerConfiguration;
67
import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthenticationProperties;
78
import com.nimbusds.jwt.proc.JWTClaimsSetAwareJWSKeySelector;
89
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.params.ParameterizedTest;
11+
import org.junit.jupiter.params.provider.ValueSource;
912
import org.springframework.boot.test.context.FilteredClassLoader;
13+
import org.springframework.core.convert.converter.Converter;
14+
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
15+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
16+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
1017
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
18+
import org.springframework.security.core.GrantedAuthority;
1119
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
1220
import org.springframework.security.oauth2.jwt.Jwt;
1321
import org.springframework.security.oauth2.jwt.JwtDecoder;
1422
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
1523
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
24+
import org.springframework.test.util.ReflectionTestUtils;
1625

26+
import java.util.Collection;
1727
import java.util.List;
1828

1929
import static com.azure.spring.cloud.autoconfigure.aad.implementation.WebApplicationContextRunnerUtils.resourceServerContextRunner;
2030
import static org.assertj.core.api.Assertions.assertThat;
2131
import static org.junit.jupiter.api.Assertions.assertThrows;
32+
import static org.mockito.Mockito.mock;
2233

2334
class AadResourceServerConfigurationTests {
2435

@@ -79,4 +90,58 @@ void testCreateWebSecurityConfigurerAdapter() {
7990
assertThat(webSecurityConfigurerAdapter).isNotNull();
8091
});
8192
}
93+
@ParameterizedTest
94+
@ValueSource(classes = { TestResourceServerConfigurationUsingConstructor.class, TestResourceServerConfigurationUsingMethod.class })
95+
@SuppressWarnings("unchecked")
96+
void useCustomJwtGrantedAuthoritiesConverterUsingConstructor(Class<? extends WebSecurityConfigurerAdapter> configurationClass) {
97+
resourceServerContextRunner()
98+
.withPropertyValues("spring.cloud.azure.active-directory.enabled=true")
99+
.withUserConfiguration(configurationClass)
100+
.run(context -> {
101+
WebSecurityConfigurerAdapter webSecurityConfigurerAdapter = (WebSecurityConfigurerAdapter) context.getBean(configurationClass);
102+
Converter<Jwt, Collection<GrantedAuthority>> converter =
103+
(Converter) ReflectionTestUtils.getField(webSecurityConfigurerAdapter, "jwtGrantedAuthoritiesConverter");
104+
assertThat(converter).isNotNull();
105+
assertThat(converter).isInstanceOfAny(TestJwtGrantedAuthoritiesConverter.class);
106+
assertThat(webSecurityConfigurerAdapter).isNotNull();
107+
});
108+
}
109+
110+
@EnableWebSecurity
111+
@EnableGlobalMethodSecurity(prePostEnabled = true)
112+
static class TestResourceServerConfigurationUsingConstructor extends
113+
AadResourceServerWebSecurityConfigurerAdapter {
114+
115+
TestResourceServerConfigurationUsingConstructor() {
116+
super(null, mock(TestJwtGrantedAuthoritiesConverter.class));
117+
}
118+
119+
@Override
120+
protected void configure(HttpSecurity http) throws Exception {
121+
super.configure(http);
122+
}
123+
}
124+
125+
@EnableWebSecurity
126+
@EnableGlobalMethodSecurity(prePostEnabled = true)
127+
static class TestResourceServerConfigurationUsingMethod extends
128+
AadResourceServerWebSecurityConfigurerAdapter {
129+
130+
@Override
131+
protected void configure(HttpSecurity http) throws Exception {
132+
super.configure(http);
133+
}
134+
135+
@Override
136+
protected Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter() {
137+
return mock(TestJwtGrantedAuthoritiesConverter.class);
138+
}
139+
}
140+
141+
class TestJwtGrantedAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
142+
@Override
143+
public Collection<GrantedAuthority> convert(Jwt source) {
144+
return null;
145+
}
146+
}
82147
}

0 commit comments

Comments
 (0)