Skip to content

JWT Client Authentication support design for Spring Cloud Azure AD

Moary Chen edited this page Jun 22, 2022 · 7 revisions

Design for JWT Client Authentication of Spring Cloud Azure Active Directory Starter

  • The client authentication method PRIVATE_KEY_JWT is not supported

The Azure AD supports the below client authentication methods, see the Client Authentication, rfc7523:

  • CLIENT_SECRET_BASIC
  • CLIENT_SECRET_POST
  • PRIVATE_KEY_JWT

Spring Security OAuth2 library already supports these client authentication methods, but the method 'PRIVATE_KEY_JWT' is not yet supported by Spring Cloud Azure Starter Active Directory. Currently, users must use the client id and client secret to finish the OAuth2 client authentication.

  • The Authorization grant type JWT_BEARER is not supported

Spring Cloud Azure Starter Active Directory already supports similar functionality to grant type JWT_BEARER via ON_BEHALF_OF, it does not follow the Spring Security OAuth2 pattern, so it's necessary to change to use the authorization grant type JWT_BEARER to support both grant types, but in order to better solve the first problem, this authorization type must also be supported together.

Goal

  • Support JWT Client Authentication in Azure AD starters.
  • Support JWT_BEARER Authorization grant type

Expected feature usage

Gives users an additional way to achieve OAuth2 client authentication.

Users can configure the client authentication method private_key_jwt to enable this feature, and the certificate path and certificate password are also required. The below properties should be added together:

  • Add client-authentication-method property to configure client authentication method

It will be set to client_secret_basic by default.

Users can only set the client authentication method for the default OAuth2 client azure.

spring.cloud.azure.active-directory.authorization-clients.azure.client-authentication-method=private_key_jwt

Users can set the client authentication method for the other OAuth2 clients.

spring.cloud.azure.active-directory.authorization-clients.xxx.client-authentication-method=private_key_jwt
  • Add client-certificate-path and client-certificate-password properties to configure local certificate info.

User can use the global properties:

spring.cloud.azure.credential.client-certificate-path=/etc/xxx
spring.cloud.azure.credential.client-certificate-password=xxx

User can use the azure active-directory service properties:

spring.cloud.azure.active-directory.credential.client-certificate-path=/etc/xxx
spring.cloud.azure.active-directory.credential.client-certificate-password=xxx

NOTE:

Only files with the '.pfx' or '.p12' extension are supported., the certificate requires a password.

The old usage of the client secret:

spring:
  cloud:
    azure:
# Properties like spring.cloud.azure.credential.client-id are global properties.
# Properties like spring.cloud.azure.active-directory.credential.client-id are AAD properties.
# If AAD properties is not configured, global properties will be used.
#      credential:
#        client-id:
#        client-secret:
#      profile:
#        tenant-id:
      active-directory:
        enabled: true
        credential:
          client-id: ${AZURE_CLIENT_ID}
          client-secret: ${AZURE_CLIENT_SECRET}
        profile:
          tenant-id: ${AZURE_TENANT_ID}
        user-group:
          allowed-group-names: group1,group2
          allowed-group-ids: <group1-id>,<group2-id>
        post-logout-redirect-uri: http://localhost:8080
        authorization-clients:
          arm:
            on-demand: true
            scopes: https://management.core.windows.net/user_impersonation
          graph:
            scopes:
              - https://graph.microsoft.com/User.Read
              - https://graph.microsoft.com/Directory.Read.All
          webapiA:
            scopes:
              - ${WEB_API_A_APP_ID_URL}/Obo.WebApiA.ExampleScope
          webapiB:
            scopes:
              - ${WEB_API_B_APP_ID_URL}/.default
            authorization-grant-type: client_credentials

The new usage of the client certificate:

# WebapiA is an optional client, we can access obo resource servers.
# We can also access a custom server according to the webapiA client.

spring:
  cloud:
    azure:
# Properties like spring.cloud.azure.credential.client-id are global properties.
# Properties like spring.cloud.azure.active-directory.credential.client-id are AAD properties.
# If AAD properties is not configured, global properties will be used.
#      credential:
#        client-id:
#        client-certificate-path: ${AZURE_CERTIFICATE_PATH}
#        client-certificate-password: ${AZURE_CERTIFICATE_PASSWORD}
#      profile:
#        tenant-id:
      active-directory:
        enabled: true
        credential:
          client-id: ${AZURE_CLIENT_ID}
          client-certificate-path: ${AZURE_CERTIFICATE_PATH}
          client-certificate-password: ${AZURE_CERTIFICATE_PASSWORD}
        profile:
          tenant-id: ${AZURE_TENANT_ID}
        user-group:
          allowed-group-names: group1,group2
          allowed-group-ids: <group1-id>,<group2-id>
        post-logout-redirect-uri: http://localhost:8080
        authorization-clients:
          azure:
            client-authentication-method: private_key_jwt
          arm:
            client-authentication-method: private_key_jwt
            on-demand: true
            scopes: https://management.core.windows.net/user_impersonation
          graph:
            client-authentication-method: private_key_jwt
            scopes:
              - https://graph.microsoft.com/User.Read
              - https://graph.microsoft.com/Directory.Read.All
          webapiA:
            client-authentication-method: private_key_jwt
            scopes:
              - ${WEB_API_A_APP_ID_URL}/Obo.WebApiA.ExampleScope
          webapiB:
            client-authentication-method: private_key_jwt
            scopes:
              - ${WEB_API_B_APP_ID_URL}/.default
            authorization-grant-type: client_credentials

Use Spring Security's authorization grant type JWT_BEARER instead of ON_BEHALF_OF

It's recommended to use the authorization grant type urn:ietf:params:oauth:grant-type:jwt-bearer in Spring Cloud Azure Active Directory Starter 4.3 and above, the legacy authorization grant type ON_BEHALF_OF will be deprecated.

If you still configure the authorization grant type on_behalf_of, it will be replaced with urn:ietf:params:oauth:grant-type:jwt-bearer automatically.

Compatible usage:

spring.cloud.azure.active-directory.authorization-clients.xxx.authorization-grant-type=on_behalf_of

New usage:

spring.cloud.azure.active-directory.authorization-clients.xxx.authorization-grant-type=urn:ietf:params:oauth:grant-type:jwt-bearer

How does the Azure AD support these features?

How does Spring Security achieve these features?

Spring security provides the base implementation for each authorization grant type and client authentication.

You can follow this method stack:

org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter#attemptAuthentication

org.springframework.security.authentication.ProviderManager#authenticate

org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider#authenticate

org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider#getResponse

org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient#getTokenResponse

it will convert the request to the authorization server, invoke the HTTP request, and return the token response.

Each authorization type flow will exchange for an access token from the Azure authorization server using its own response client.

The unity process is required to call the method OAuth2AccessTokenResponse getTokenResponse(T authorizationGrantRequest) to return the access token.

Each authorization type has its own token response client implementation:

token-response-client

Each token response client provides a method to receive a request entity converter.

void setRequestEntityConverter(
			Converter<T extends AbstractOAuth2AuthorizationGrantRequest, RequestEntity<?>> requestEntityConverter)

Before the token request is sent, the request entity converter will be called to enhance the header and parameter.

The base implementation of the request entity converter is OAuth2AuthorizationCodeGrantRequestEntityConverter, which provides the below method to custom the request data.

public final void addParametersConverter(Converter<T, MultiValueMap<String, String>> parametersConverter) {
    Assert.notNull(parametersConverter, "parametersConverter cannot be null");
    Converter<T, MultiValueMap<String, String>> currentParametersConverter = this.parametersConverter;
    this.parametersConverter = (authorizationGrantRequest) -> {
        // Append parameters using a Composite Converter
        MultiValueMap<String, String> parameters = currentParametersConverter.convert(authorizationGrantRequest);
        if (parameters == null) {
            parameters = new LinkedMultiValueMap<>();
        }
        MultiValueMap<String, String> parametersToAdd = parametersConverter.convert(authorizationGrantRequest);
        if (parametersToAdd != null) {
            parameters.addAll(parametersToAdd);
        }
        return parameters;
    };
}

Each request entity converter of authorization grant type has a special implementation, the extensions are the header converter and parameter converter, these converters can be used to custom additional information for your own authorization server requirement.

auth-grant-request-entity-converter

Spring Security provides a default implementation for JWT Client Authentication NimbusJwtClientAuthenticationParametersConverter, this will be our functional prototype.

The grant type ON_BEHALF_OF is the grant type org.springframework.security.oauth2.core.AuthorizationGrantType#JWT_BEARER, it's available to replace the default implementation of Spring Security since the version 5.5.8.

How does Spring Cloud Azure achieve these features?

JWT Client Authentication

Unify the default value of each client-authentication-method

It will be set to client_secret_basic by default. The built-in method client_secret_jwt is not supported in our Azure AD starter. I create an issue to ask MS doc about this feature https://github.com/MicrosoftDocs/azure-docs/issues/93819

Add interface to resolve JWK

It's necessary to follow the implementation NimbusJwtClientAuthenticationParametersConverter , although Spring Security does not provide such an interface.

/**
 * Resolver interface to resolve a function that returns a {@link JWK} implementation through a {@link ClientRegistration}.
 */
public interface OAuth2ClientAuthenticationJWKResolver {

    /**
     * @return a resolver function.
     */
    Function<ClientRegistration, JWK> resolve();
}

public NimbusJwtClientAuthenticationParametersConverter(Function<ClientRegistration, JWK> jwkResolver) {
    Assert.notNull(jwkResolver, "jwkResolver cannot be null");
    this.jwkResolver = jwkResolver;
}

Spring Cloud Azure 4.3.0 provides the default implementation AadOAuth2ClientAuthenticationJWKResolver, which will load the local certificate to create the JWT token for client authentication. This resolver will be used for all the authorization grant types, except ON_BEHALF_OF processes, so it's necessary to replace the ON_BEHALF_OF flow with JWT_BEARER flow.

Use JWT_BEARER Authorization grant type instead of ON_BEHALF_OF

Due to the inconsistency between the previously implemented OBO provider process and Spring Security, there is no way for us to add JWT token generation logic in common.

so we may use the below 2 solutions:

  • Enhance the current ON_BEHALF_OF flow to follow the Spring Security pattern.

    Tried, but it's not available to create a new authorization grant type based on the process provided by Spring Security. I created an issue https://github.com/spring-projects/spring-security/issues/11350

  • Replace the current ON_BEHALF_OF with JWT_BEARER, and enhance it if necessary.

    Yes, we will do it this way.

    Legacy issues:

    • The AadOboOAuth2AuthorizedClientProvider should be deprecated?

      Yes, part of the Conditional Access functionality will also be deprecated too because it is customized based on the MSAL4J library.

    • How do the users want to use the old usage of ON_BEHALF_OF?

      The configuration of the grant type ON_BEHALF_OF is still exposed to users, it's more readable than the value of JWT_BEARER, but it's not meant to enable AadOboOAuth2AuthorizedClientProvider's functionality.

Design the client authentication parameter converter

Redesign a class AadJwtClientAuthenticationParametersConverter against NimbusJwtClientAuthenticationParametersConverter

  • The default implementation does not meet the Azure AD authorization parameters requirement.
  • Simplify the JWT encoder(AadJwtEncoder), because there are some compatibility issues when the user uses the Spring Boot 2.5.14 version.
Extend the default JwtBearerGrantRequestEntityConverter

Aditional parameter is required for authorization grant type JWT_BEARER. https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#first-case-access-token-request-with-a-shared-secret

Match JWT_BEARER authorized client provider

Due to enabling the default authorization grant type JWT_BEARER , the JwtBearerOAuth2AuthorizedClientProvider limits the principal type must be Jwt, so we must change the default jwt authentication converter, and use the default converter JwtAuthenticationConverter instead in AadResourceServerWebSecurityConfigurerAdapter, it's required when the authorization grant type is JWT_BEARER; so we should deprecate the AadOAuth2AuthenticatedPrincipal, and AadJwtBearerTokenAuthenticationConverter classes.

Research: support this feature in Azure AD B2C starter

The built-in user flows do not support using JWT client authentication, my test failed, and I create the below issues to ask MS doc:

After my research, this feature must be turned on through a custom policy, the custom is an advanced usage of Azure AD B2C that requires further study. So I and Rujun suggest not to do it now. The spring-cloud-azure-starter-active-directory-b2cshould be supported later and currently, there are no customers asking.