Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support jwt client authentication for Spring Cloud Azure AD Starter #29471

Merged
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ac52663
Support jwt client authentication for Azure AD starter
moarychan Jun 15, 2022
16f79c8
Merge branch 'main' of github.com:Azure/azure-sdk-for-java into moary…
moarychan Jun 15, 2022
be9dd91
Update CHANGELOG.md
moarychan Jun 15, 2022
cb1561f
Update jwt encoder for compatibility with spring-security 5.5.X and 5…
moarychan Jun 20, 2022
e1da7e1
Remove unused lines
moarychan Jun 20, 2022
519142e
Add Java doc
moarychan Jun 20, 2022
e974b51
Update ignore revapi json
moarychan Jun 20, 2022
9967623
Update ignore revapi json
moarychan Jun 20, 2022
6dafa18
Update ignore revapi json
moarychan Jun 21, 2022
a7bb791
Fix revapi json configuration
moarychan Jun 21, 2022
8aba3a1
Fix revapi json configuration
moarychan Jun 21, 2022
6de492f
Fix revapi json and deprecated obo provider
moarychan Jun 21, 2022
2b81a26
Fix revapi json and make the grant type on_behalf_of available as it …
moarychan Jun 21, 2022
b47d1a9
Update java doc
moarychan Jun 21, 2022
49d20ae
Fix comments
moarychan Jun 22, 2022
078a5c0
do some refactor
saragluna Jun 22, 2022
69b2ebd
Fix comments
moarychan Jun 22, 2022
c4e9863
Fix code smells
moarychan Jun 22, 2022
1d00df9
Fix conflicts
moarychan Jun 22, 2022
1ecf11a
Fix revapi json configuration
moarychan Jun 22, 2022
8371cb2
Reduce revapi configuration
moarychan Jun 23, 2022
c16615f
Reduce revapi configuration
moarychan Jun 23, 2022
dbd28b5
Reduce revapi configuration
moarychan Jun 23, 2022
7a0aac2
Remove unused code and update Java doc
moarychan Jun 23, 2022
b0d118c
Fix revapi issue
moarychan Jun 23, 2022
64b4a20
Fix comments
moarychan Jun 23, 2022
e3f283c
Fix comments and add more unit tests
moarychan Jun 24, 2022
97a0e78
Fix code smells
moarychan Jun 24, 2022
1949deb
Update justification
moarychan Jun 27, 2022
e700da0
Fix comments
moarychan Jun 28, 2022
c594437
Revert change
moarychan Jun 28, 2022
ac72afa
Fix comments
moarychan Jun 28, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions eng/code-quality-reports/src/main/resources/revapi/revapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,51 @@
"old": "method org.springframework.boot.autoconfigure.kafka.KafkaProperties com.azure.spring.cloud.autoconfigure.eventhubs.kafka.AzureEventHubsKafkaAutoConfiguration::azureKafkaProperties(com.azure.spring.cloud.core.provider.connectionstring.ServiceConnectionStringProvider<com.azure.spring.cloud.core.service.AzureServiceType.EventHubs>)",
"justification": "To move kafka properties customization to bean post processor."
},
{
"code": "java.annotation.attributeValueChanged",
"new": "class com.azure.spring.cloud.autoconfigure.aad.AadAutoConfiguration",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is "new" necessary here?

Copy link

@chenrujun chenrujun Jun 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to "support JWT Client Authentication" without this change?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same to all other changes in current file.
If breaking change is necessary, please add more description in "justification".

"justification": "The use of AadOboOAuth2AuthorizedClientProvider is not recommended, and there is no need to retain consideration of the different situations brought by this OBO provider."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you the not recommended but still supported? Is this an API exposed to users or not?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, users can override the bean OAuth2AuthorizedClientManager to take the OBO provider ability.

},
{
"code": "java.annotation.added",
"new": "class com.azure.spring.cloud.autoconfigure.aad.configuration.AadOAuth2ClientConfiguration",
"justification": "The use of AadOboOAuth2AuthorizedClientProvider is not recommended, and there is no need to retain consideration of the different situations brought by this OBO provider."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about the "justification" focus on the "new code" instead of "xx is no need"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old code is to meet the different situations of OBO provider usage, as this is deprecated, then it can be removed no matter which grant type is used.

},
{
"regex": true,
"code": "java.class.removed",
"old": "class com\\.azure\\.spring\\.cloud\\.autoconfigure\\.aad\\.configuration\\.AadOAuth2ClientConfiguration\\..*",
"justification": "The use of AadOboOAuth2AuthorizedClientProvider is not recommended, and there is no need to retain consideration of the different situations brought by this OBO provider."
},
{
"code": "java.method.returnTypeChanged",
"old:": "method com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType com.azure.spring.cloud.autoconfigure.aad.properties.AuthorizationClientProperties::getAuthorizationGrantType()",
"new": "method org.springframework.security.oauth2.core.AuthorizationGrantType com.azure.spring.cloud.autoconfigure.aad.properties.AuthorizationClientProperties::getAuthorizationGrantType()",
"justification": "To support authorization grant type JWT_BEARER, we should use the default AuthorizationGrantType instead of AadAuthorizationGrantType."
},
{
"code": "java.method.returnTypeChanged",
"old": "method com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType com.azure.spring.cloud.autoconfigure.aadb2c.properties.AuthorizationClientProperties::getAuthorizationGrantType()",
"new": "method org.springframework.security.oauth2.core.AuthorizationGrantType com.azure.spring.cloud.autoconfigure.aadb2c.properties.AuthorizationClientProperties::getAuthorizationGrantType()",
saragluna marked this conversation as resolved.
Show resolved Hide resolved
"justification": "To support authorization grant type JWT_BEARER, we should use the default AuthorizationGrantType instead of AadAuthorizationGrantType."
},
{
"code": "java.method.parameterTypeChanged",
"old": "parameter void com.azure.spring.cloud.autoconfigure.aad.properties.AuthorizationClientProperties::setAuthorizationGrantType(===com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType===)",
"new": "parameter void com.azure.spring.cloud.autoconfigure.aad.properties.AuthorizationClientProperties::setAuthorizationGrantType(===org.springframework.security.oauth2.core.AuthorizationGrantType===)",
"justification": "To support authorization grant type JWT_BEARER, we should use the default AuthorizationGrantType instead of AadAuthorizationGrantType."
},
{
"code": "java.method.parameterTypeChanged",
"old": "parameter void com.azure.spring.cloud.autoconfigure.aadb2c.properties.AuthorizationClientProperties::setAuthorizationGrantType(===com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType===)",
"new": "parameter void com.azure.spring.cloud.autoconfigure.aadb2c.properties.AuthorizationClientProperties::setAuthorizationGrantType(===org.springframework.security.oauth2.core.AuthorizationGrantType===)",
"justification": "To support authorization grant type JWT_BEARER, we should use the default AuthorizationGrantType instead of AadAuthorizationGrantType."
},
{
"code": "java.field.removedWithConstant",
"old": "field com.azure.spring.cloud.autoconfigure.aad.AadAuthenticationFilterAutoConfiguration.PROPERTY_PREFIX",
"justification": "Unused constant."
},
{
"code": "java.method.visibilityReduced",
"new": "method com.azure.spring.cloud.autoconfigure.context.AzureTokenCredentialAutoConfiguration.AzureServiceClientBuilderFactoryPostProcessor com.azure.spring.cloud.autoconfigure.context.AzureTokenCredentialAutoConfiguration::builderFactoryBeanPostProcessor()",
Expand Down
16 changes: 16 additions & 0 deletions sdk/spring/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features Added
- GA the `spring-cloud-azure-starter-storage`. This starter supports all features of Azure Storage.
- GA the `spring-cloud-azure-starter-keyvault`. This starter supports all features of Azure Key Vault.
- Support Jwt Client authentication for Azure AD Starter.

### Breaking Changes

Expand Down Expand Up @@ -48,13 +49,28 @@ This section includes changes in `spring-cloud-azure-starter-active-directory` m
#### Dependency Updates
- Upgrade spring-security to 5.6.4 to address [CVE-2022-22978](https://spring.io/blog/2022/05/15/cve-2022-22978-authorization-bypass-in-regexrequestmatcher) [#29304](https://github.com/Azure/azure-sdk-for-java/pull/29304).

#### Features Added
+ Support Jwt Client authentication [#29471](https://github.com/Azure/azure-sdk-for-java/pull/29471).

#### Breaking Changes
+ Deprecated classes and properties type changed [#29471](https://github.com/Azure/azure-sdk-for-java/pull/29471).
+ Deprecated ~~AadAuthorizationGrantType~~, use `AuthorizationGrantType` instead.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we just @deprecate an interface or changed the implementation? If we use deprecate, we mean the api is @deprecated and add a new one.

It seems this place should be "removed" and replaced with a new one.
Do we mean this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This @deprecated is to tell users not to use the AadAuthorizationGrantType, and still can be used for some users that depend on this type when upgrading the AAD starter to 4.3.0. It will be kept until 5.0.

+ Deprecated ~~AadOAuth2AuthenticatedPrincipal~~, ~~AadJwtBearerTokenAuthenticationConverter~~, use the default converter `JwtAuthenticationConverter` instead in `AadResourceServerWebSecurityConfigurerAdapter`.
+ The type of property *authorizationGrantType* is changed to `AuthorizationGrantType` in `AuthorizationClientProperties` class.
+ Deprecated ~~AadOboOAuth2AuthorizedClientProvider~~, use `JwtBearerOAuth2AuthorizedClientProvider` instead.

### Spring Cloud Azure Starter Active Directory B2C
This section includes changes in `spring-cloud-azure-starter-active-directory-b2c` module.

#### Dependency Updates
- Upgrade spring-security to 5.6.4 to address [CVE-2022-22978](https://spring.io/blog/2022/05/15/cve-2022-22978-authorization-bypass-in-regexrequestmatcher) [#29304](https://github.com/Azure/azure-sdk-for-java/pull/29304).


#### Breaking Changes
+ Deprecated classes and properties type changed [#29471](https://github.com/Azure/azure-sdk-for-java/pull/29471).
+ Deprecated *~~AadAuthorizationGrantType~~*, use `AuthorizationGrantType` instead.
+ The type of property *authorizationGrantType* is changed to `AuthorizationGrantType` in `AuthorizationClientProperties` class.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this duplicate with the above one? Or they sit in different packages?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One is for AAD starter, another is for AAD B2C starter.


## 4.2.0 (2022-05-26)

- This release is compatible with Spring Boot 2.5.0-2.5.14, 2.6.0-2.6.8, 2.7.0. (Note: 2.5.x (x>14), 2.6.y (y>8) and 2.7.z (z>0) should be supported, but they aren't tested with this release.)
Expand Down
1 change: 0 additions & 1 deletion sdk/spring/spring-cloud-azure-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,6 @@
<rules>
<bannedDependencies>
<includes>
<include>com.microsoft.azure:msal4j:[1.12.0]</include> <!-- {x-include-update;com.microsoft.azure:msal4j;external_dependency} -->
<include>com.nimbusds:nimbus-jose-jwt:[9.22]</include> <!-- {x-include-update;com.nimbusds:nimbus-jose-jwt;external_dependency} -->
<include>org.messaginghub:pooled-jms:[1.2.4]</include> <!-- {x-include-update;org.messaginghub:pooled-jms;external_dependency} -->
<include>org.apache.qpid:qpid-jms-client:[0.53.0]</include> <!-- {x-include-update;org.apache.qpid:qpid-jms-client;external_dependency} -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,8 @@
@ConditionalOnMissingClass({ "org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken" })
@Import(AadPropertiesConfiguration.class)
public class AadAuthenticationFilterAutoConfiguration {
/**
* The property prefix
*/
public static final String PROPERTY_PREFIX = "spring.cloud.azure.active-directory";

private static final Logger LOG = LoggerFactory.getLogger(AadAuthenticationProperties.class);
private static final Logger LOGGER = LoggerFactory.getLogger(AadAuthenticationProperties.class);

private final AadAuthenticationProperties properties;
private final AadAuthorizationServerEndpoints endpoints;
Expand All @@ -72,7 +68,7 @@ public AadAuthenticationFilterAutoConfiguration(AadAuthenticationProperties prop
@ConditionalOnMissingBean(AadAuthenticationFilter.class)
@ConditionalOnExpression("${spring.cloud.azure.active-directory.session-stateless:false} == false")
public AadAuthenticationFilter aadAuthenticationFilter(ResourceRetriever resourceRetriever, JWKSetCache jwkSetCache) {
LOG.info("AadAuthenticationFilter Constructor.");
LOGGER.info("AadAuthenticationFilter Constructor.");
return new AadAuthenticationFilter(
properties,
endpoints,
Expand All @@ -91,7 +87,7 @@ public AadAuthenticationFilter aadAuthenticationFilter(ResourceRetriever resourc
@ConditionalOnMissingBean(AadAppRoleStatelessAuthenticationFilter.class)
@ConditionalOnExpression("${spring.cloud.azure.active-directory.session-stateless:false} == true")
public AadAppRoleStatelessAuthenticationFilter aadStatelessAuthFilter(ResourceRetriever resourceRetriever) {
LOG.info("Creating AadStatelessAuthFilter bean.");
LOGGER.info("Creating AadStatelessAuthFilter bean.");
return new AadAppRoleStatelessAuthenticationFilter(
new UserPrincipalManager(
endpoints,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@
AadPropertiesConfiguration.class,
AadWebApplicationConfiguration.class,
AadResourceServerConfiguration.class,
AadOAuth2ClientConfiguration.OAuth2ClientRepositoryConfiguration.class,
AadOAuth2ClientConfiguration.WebApplicationWithoutResourceServerOAuth2AuthorizedClientManagerConfiguration.class,
AadOAuth2ClientConfiguration.ResourceServerWithOBOOAuth2AuthorizedClientManagerConfiguration.class,
AadOAuth2ClientConfiguration.WebApplicationAndResourceServiceOAuth2AuthorizedClientManagerConfiguration.class
AadOAuth2ClientConfiguration.class
})
public class AadAutoConfiguration {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
package com.azure.spring.cloud.autoconfigure.aad;

import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthenticationProperties;
import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType;
import com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationServerEndpoints;
import com.azure.spring.cloud.autoconfigure.aad.properties.AuthorizationClientProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.util.Assert;

import java.util.Collection;
Expand All @@ -21,8 +23,10 @@
import java.util.Set;
import java.util.stream.Collectors;

import static com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType.AUTHORIZATION_CODE;
import static com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthorizationGrantType.AZURE_DELEGATED;
import static com.azure.spring.cloud.autoconfigure.aad.implementation.constants.Constants.AZURE_DELEGATED;
import static com.azure.spring.cloud.autoconfigure.aad.implementation.constants.Constants.ON_BEHALF_OF;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we use this const instead of the original enum one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are all the same type AuthorizationGrantType.

import static org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE;
import static org.springframework.security.oauth2.core.AuthorizationGrantType.JWT_BEARER;


/**
Expand All @@ -34,6 +38,8 @@
*/
public class AadClientRegistrationRepository implements ClientRegistrationRepository, Iterable<ClientRegistration> {

private static final Logger LOGGER = LoggerFactory.getLogger(AadClientRegistrationRepository.class);

/**
* Azure client registration ID
*/
Expand Down Expand Up @@ -68,13 +74,25 @@ public AadClientRegistrationRepository(AadAuthenticationProperties properties) {
.stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> toClientRegistration(entry.getKey(), entry.getValue().getAuthorizationGrantType(),
entry.getValue().getScopes(), properties)));
entry -> toClientRegistration(entry.getKey(),
entry.getValue().getAuthorizationGrantType(),
entry.getValue().getScopes(),
entry.getValue().getClientAuthenticationMethod(),
properties)));
ClientAuthenticationMethod azureClientAuthMethod = getAzureDefaultClientAuthenticationMethod();
saragluna marked this conversation as resolved.
Show resolved Hide resolved
ClientRegistration azureClient =
toClientRegistration(AZURE_CLIENT_REGISTRATION_ID, AUTHORIZATION_CODE, authorizationCodeScopes, properties);
toClientRegistration(AZURE_CLIENT_REGISTRATION_ID, AUTHORIZATION_CODE,
authorizationCodeScopes, azureClientAuthMethod, properties);
allClients.put(AZURE_CLIENT_REGISTRATION_ID, azureClient);
}

private ClientAuthenticationMethod getAzureDefaultClientAuthenticationMethod() {
if (this.allClients.containsKey(AZURE_CLIENT_REGISTRATION_ID)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getOrDefault?

Copy link
Member Author

@moarychan moarychan Jun 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means getting the client authentication method of the default client(azure), there might be some ambiguity.

return this.allClients.get(AZURE_CLIENT_REGISTRATION_ID).getClientAuthenticationMethod();
}
return ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
}
moarychan marked this conversation as resolved.
Show resolved Hide resolved
saragluna marked this conversation as resolved.
Show resolved Hide resolved

/**
* Gets the set of Azure client access token scopes.
*
Expand All @@ -94,8 +112,7 @@ public ClientRegistration findByRegistrationId(String registrationId) {
public Iterator<ClientRegistration> iterator() {
return allClients.values()
.stream()
.filter(client ->
client.getAuthorizationGrantType().getValue().equals(AUTHORIZATION_CODE.getValue()))
.filter(client -> AUTHORIZATION_CODE.equals(client.getAuthorizationGrantType()))
.iterator();
}

Expand Down Expand Up @@ -132,19 +149,28 @@ private Set<String> delegatedClientsAccessTokenScopes(AadAuthenticationPropertie
}

private ClientRegistration toClientRegistration(String registrationId,
AadAuthorizationGrantType aadAuthorizationGrantType,
AuthorizationGrantType authorizationGrantType,
Collection<String> scopes,
ClientAuthenticationMethod clientAuthenticationMethod,
AadAuthenticationProperties properties) {
AadAuthorizationServerEndpoints endpoints =
new AadAuthorizationServerEndpoints(properties.getProfile().getEnvironment().getActiveDirectoryEndpoint(), properties.getProfile().getTenantId());
new AadAuthorizationServerEndpoints(properties.getProfile().getEnvironment().getActiveDirectoryEndpoint(),
properties.getProfile().getTenantId());

if (ON_BEHALF_OF.equals(authorizationGrantType)) {
authorizationGrantType = JWT_BEARER;
LOGGER.warn("The grant type 'on_behalf_of' of the client '{}' is not recommended, it will be "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we warn it here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It means the type value is changed by our logic, I will improve the log content.

+ "replaced with 'urn:ietf:params:oauth:grant-type:jwt-bearer'.", registrationId);
}
return ClientRegistration.withRegistrationId(registrationId)
.clientName(registrationId)
.authorizationGrantType(new AuthorizationGrantType((aadAuthorizationGrantType.getValue())))
.authorizationGrantType(authorizationGrantType)
.scope(scopes)
.redirectUri(properties.getRedirectUriTemplate())
.userNameAttributeName(properties.getUserNameAttribute())
.clientId(properties.getCredential().getClientId())
.clientSecret(properties.getCredential().getClientSecret())
.clientAuthenticationMethod(clientAuthenticationMethod)
.authorizationUri(endpoints.getAuthorizationEndpoint())
.tokenUri(endpoints.getTokenEndpoint())
.jwkSetUri(endpoints.getJwkSetEndpoint())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.util.Assert;

import java.util.Collection;
Expand All @@ -23,7 +24,10 @@

/**
* A {@link Converter} that takes a {@link Jwt} and converts it into a {@link BearerTokenAuthentication}.
*
* @deprecated use the default converter {@link JwtAuthenticationConverter} instead in {@link AadResourceServerWebSecurityConfigurerAdapter}.
*/
@Deprecated
public class AadJwtBearerTokenAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {

private final Converter<Jwt, Collection<GrantedAuthority>> converter;
Expand Down
Loading