skip to Main Content

I successfully set up a sample application to utilize AzureAD for user authentication following the steps outlined in this blog post. The post provides instructions on configuring AzureAD and the application’s identity using client-id and client-secret for the application configuration, and the setup working properly.

However I try to configure AzureAD and application’s identity using with public key I could not able to successfully setup.

I used following openssl commands to generate the public key.

$certname = "AzureCertificate"

$cert = New-SelfSignedCertificate -Subject "CN=$certname"
-CertStoreLocation "Cert:CurrentUserMy" -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256

Export-Certificate -Cert $cert -FilePath
"C:PersonalWork1AZUREKEY$certname.cer"

enter image description here

Then I generated the public key using following command in command line

openssl x509 -inform der -in AzureCertificate.cer -pubkey -noout >
your-public-key.pem

Then I uploaded the generated certificate AzureCertificate.cer file in to the AzurePortal application as show below.

enter image description here

Then I have configured the certificate location on the applciation.yml as show below and added publicKey.pem file in to resources folder. Please note I have replace correct my-tenant-id and my-client-id in applciation.yml file.

enter image description here

Then I have run the application It first correctly redirecting to Microsoft login page and then after user enters the credentials system giving error message as below.

[invalid_request] AADSTS900144: The request body must contain the following parameter: ‘client_id’. Trace ID: yyyyyyy Correlation ID: xxxxxxx Timestamp: 2023-11-18 15:14:37Z

enter image description here

Highly appreciated if someone share your experiencing this configuration process.

3

Answers


  1. You have followed the steps to generate a certificate, uploaded it to Azure, and configured your application.yml with the public key, but you are receiving an error stating that the ‘client_id’ parameter is missing.

    From the error message in the last screenshot, it seems that the AzureAD authentication process is not receiving the client_id parameter as part of the request, as in this question or this one.

    From your application.yml, I have a few suggestions:

    • The provider under registration should match the name of the provider under provider. In your configuration, the provider is named azure in both sections, which is correct. But make sure this consistency is maintained throughout the application.

    • The scopes specified under registration are typically correct for OpenID Connect (openid, email, profile). Check that the scopes align with what your application requires and what Azure AD is configured to allow.

    • The jwt.key-set-uri under provider should be unnecessary in the context of client authentication using private_key_jwt. That setting is typically used by resource servers to validate JWTs, not by clients to authenticate to the authorization server.

    • If using private_key_jwt for authentication, the client-secret should not be present, as this authentication method uses a private key instead of a client secret. (as confirmed in "How to Get an Azure Access Token Using Self-signed Certificate with Spring Boot" from Milos Zivkovic)

    • The Baeldung guide mentions setting up redirection URIs for the authorization code flow, but this is not directly represented in the application.yml. If using the authorization code flow, make sure the redirect URIs are correctly configured in the Azure AD application registration and, if necessary, specified in your Spring Security configuration.

      # This is an optional setting depending on your setup
      registration:
         azure-dev:
           redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
      
    • Make sure the URLs for issuer-uri and jwk-set-uri are correctly typed and match your Azure AD configuration. Typographical errors in URLs can lead to failures in fetching the necessary metadata and keys.

    • If you are using private_key_jwt, make sure Azure AD is configured to accept a JWT signed with your private key as a means of client authentication.

    • Depending on the OAuth flow you are using (e.g., authorization code vs. client credentials), make sure the authorization-grant-type is correctly specified in the registration section. That is not included in the provided configuration but is necessary for some OAuth flows.

      registration:
         azure-dev:
           authorization-grant-type: authorization_code
           # Use "client_credentials" for service-to-service communication
      
    Login or Signup to reply.
  2. Since your error message complains about not having client-id in request body, I’d suggest you to set the client-authentication-method parameter (next to the client-id property) in your application.yml file to client_secret_post.

    By looking at org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequestEntityConverter.createParameters(OAuth2ClientCredentialsGrantRequest) method, by setting this parameter, it’ll send the client_id in request body

        @Override
        protected MultiValueMap<String, String> createParameters(
                OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) {
            ClientRegistration clientRegistration = clientCredentialsGrantRequest.getClientRegistration();
            MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
            parameters.add(OAuth2ParameterNames.GRANT_TYPE, clientCredentialsGrantRequest.getGrantType().getValue());
            if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) {
                parameters.add(OAuth2ParameterNames.SCOPE,
                        StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), " "));
            }
            
            // HERE IS THE SPRING MAGIC :)
            if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientRegistration.getClientAuthenticationMethod())) {
                parameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
                parameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
            }
            return parameters;
        }
    

    Same logic applies to other related classes :

    • org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter
    • org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequestEntityConverter
    • org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequestEntityConverter

    Hope it’ll solve your problem

    Login or Signup to reply.
  3. The Microsoft documentation describes the necessary information that should be provided in order to perform an access token request with a certificate.

    You need to perform a HTTP request like the following:

    POST /{tenant}/oauth2/v2.0/token HTTP/1.1               // Line breaks for clarity
    Host: login.microsoftonline.com:443
    Content-Type: application/x-www-form-urlencoded
    
    scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
    &client_id=97e0a5b7-d745-40b6-94fe-5f77d35c6e05
    &client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
    &client_assertion=eyJhbGciOiJSUzI1NiIsIng1dCI6Imd4OHRHeXN5amNScUtqRlBuZDdSRnd2d1pJMCJ9.eyJ{a lot of characters here}M8U3bSUKKJDEg
    &grant_type=client_credentials
    

    In addition to the Entra ID tenant and the app client id, you need to indicate the client assertion type, it is a constant value, and a client assertion.

    The client assertion is a JSON web token created and signed with the certificate registered as credentials for the application in the Azure Portal.

    The characteristics of this JSON web token are described in detail here.

    Using this type of authentication mechanism with Spring Boot is not obvious but fortunately you can find different examples that exemplifies how to that.

    Consider for instance this excellent one provided as part of the azure samples repository in Github.

    Basically, it defines a set of helper classes, especially one defined in order to provide the client_assertion and client_assertion_type parameters when performing the request:

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.core.convert.converter.Converter;
    import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest;
    import org.springframework.security.oauth2.client.registration.ClientRegistration;
    import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
    import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
    import org.springframework.util.Assert;
    import org.springframework.util.LinkedMultiValueMap;
    import org.springframework.util.MultiValueMap;
    
    import java.util.Map;
    
    public class AzureActiveDirectoryJwtClientAuthenticationParametersConverter<T extends AbstractOAuth2AuthorizationGrantRequest>
        implements Converter<T, MultiValueMap<String, String>> {
    
        private final static Logger LOGGER =
            LoggerFactory.getLogger(AzureActiveDirectoryJwtClientAuthenticationParametersConverter.class);
        private static final String CLIENT_ASSERTION_TYPE_VALUE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
    
        private final Map<String, AzureActiveDirectoryCertificateSignedJwtAssertionFactory> factories;
    
        public AzureActiveDirectoryJwtClientAuthenticationParametersConverter(
            Map<String, AzureActiveDirectoryCertificateSignedJwtAssertionFactory> factories) {
            this.factories = factories;
        }
    
        @Override
        public MultiValueMap<String, String> convert(T authorizationGrantRequest) {
            Assert.notNull(authorizationGrantRequest, "authorizationGrantRequest cannot be null");
    
            ClientRegistration registration = authorizationGrantRequest.getClientRegistration();
            ClientAuthenticationMethod method = registration.getClientAuthenticationMethod();
            if (!ClientAuthenticationMethod.PRIVATE_KEY_JWT.equals(registration.getClientAuthenticationMethod())) {
                return null;
            }
    
            try {
                return createParameters(registration);
            } catch (AzureActiveDirectoryAssertionException exception) {
                LOGGER.error("Failed to create parameters.", exception);
            }
            return null;
        }
    
        private MultiValueMap<String, String> createParameters(ClientRegistration registration) throws AzureActiveDirectoryAssertionException {
            MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
            parameters.set(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, CLIENT_ASSERTION_TYPE_VALUE);
            parameters.set(OAuth2ParameterNames.CLIENT_ASSERTION, createAssertion(registration));
            return parameters;
        }
    
        private String createAssertion(ClientRegistration registration) throws AzureActiveDirectoryAssertionException {
            return factories.get(registration.getRegistrationId()).createJwtAssertion();
        }
    }
    

    and another one used to generate the appropriate JWT web token:

    import com.nimbusds.jose.JOSEException;
    import com.nimbusds.jose.JOSEObjectType;
    import com.nimbusds.jose.JWSAlgorithm;
    import com.nimbusds.jose.JWSHeader;
    import com.nimbusds.jose.JWSSigner;
    import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory;
    import com.nimbusds.jose.jwk.JWK;
    import com.nimbusds.jose.jwk.RSAKey;
    import com.nimbusds.jose.util.Base64URL;
    import com.nimbusds.jwt.JWTClaimsSet;
    import com.nimbusds.jwt.SignedJWT;
    import org.springframework.util.Assert;
    
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.security.KeyStore;
    import java.security.KeyStoreException;
    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;
    import java.security.PrivateKey;
    import java.security.PublicKey;
    import java.security.UnrecoverableKeyException;
    import java.security.cert.CertificateEncodingException;
    import java.security.cert.CertificateException;
    import java.security.cert.X509Certificate;
    import java.security.interfaces.RSAPublicKey;
    import java.util.Date;
    import java.util.UUID;
    
    /**
     * A factory used to create Microsoft Entra JWT assertion signed with a certificate.
     *
     * @author Rujun Chen
     * @see <a href="https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials">
     * Certificate credentials</a>
     * @since 2022-02-18
     */
    public class AzureActiveDirectoryCertificateSignedJwtAssertionFactory {
    
        private final JWSSigner signer;
        private final JWSHeader header;
        private final JWTClaimsSet templateClaims;
    
        /**
         * @param file Path of certificate file. The file should contain encrypted private key and certificate.
         * And the file name should have ".pfx" as suffix.
         * @param password The password of the encrypted private key in certificate file.
         * @throws AzureActiveDirectoryAssertionException if failed to create factory.
         */
        public AzureActiveDirectoryCertificateSignedJwtAssertionFactory(String file, String password, String tenantId,
                                                                        String clientId) throws AzureActiveDirectoryAssertionException {
            try {
                String fileExtension = file.substring(file.lastIndexOf(".") + 1);
                Assert.isTrue("pfx".equals(fileExtension), "Only support file with '.pfx' extension.");
                KeyStore keyStore = KeyStore.getInstance("PKCS12");
                keyStore.load(new FileInputStream(file), password.toCharArray());
                String alias = keyStore.aliases().nextElement();
                PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, password.toCharArray());
                X509Certificate x509Certificate = (X509Certificate) keyStore.getCertificate(alias);
                PublicKey publicKey = x509Certificate.getPublicKey();
                signer = createJWSSigner(publicKey, privateKey);
                header = createJWSHeader(x509Certificate);
                templateClaims = createTemplateJWTClaims(tenantId, clientId);
            } catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException |
                UnrecoverableKeyException | JOSEException exception) {
                throw new AzureActiveDirectoryAssertionException("Failed to create factory.", exception);
            }
        }
    
        /**
         * Create JWT assertion
         *
         * @throws AzureActiveDirectoryAssertionException If failed to create assertion.
         */
        public String createJwtAssertion() throws AzureActiveDirectoryAssertionException {
            JWTClaimsSet claims = createJWTClaimsSet();
            SignedJWT signedJwt = new SignedJWT(header, claims);
            try {
                signedJwt.sign(signer);
            } catch (JOSEException exception) {
                throw new AzureActiveDirectoryAssertionException("Failed to sign JWT.", exception);
            }
            return signedJwt.serialize();
        }
    
        private JWSSigner createJWSSigner(PublicKey publicKey, PrivateKey privateKey) throws JOSEException {
            // Microsoft Entra ID currently supports only RSA.
            // Refs: https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-self-signed-certificate
            JWK jwk = new RSAKey.Builder((RSAPublicKey) publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
            return new DefaultJWSSignerFactory().createJWSSigner(jwk);
        }
    
        @SuppressWarnings("deprecation")
        private JWSHeader createJWSHeader(X509Certificate x509Certificate) throws CertificateEncodingException,
            NoSuchAlgorithmException {
            return new JWSHeader.Builder(JWSAlgorithm.RS256)
                .type(JOSEObjectType.JWT)
                .x509CertThumbprint(Base64URL.encode(getX5t(x509Certificate)))
                .build();
        }
    
        private byte[] getX5t(X509Certificate cert)
            throws NoSuchAlgorithmException, CertificateEncodingException {
            MessageDigest digest = MessageDigest.getInstance("SHA-1");
            byte[] der = cert.getEncoded();
            digest.update(der);
            return digest.digest();
        }
    
        private JWTClaimsSet createTemplateJWTClaims(String tenantId, String clientId) {
            return new JWTClaimsSet.Builder()
                .audience(String.format("https://login.microsoftonline.com/%s/v2.0", tenantId))
                .issuer(clientId)
                .subject(clientId)
                .build();
        }
    
        private JWTClaimsSet createJWTClaimsSet() {
            Date currentTime = new Date();
            return new JWTClaimsSet.Builder(templateClaims)
                .expirationTime(Date.from(currentTime.toInstant().plusSeconds(300))) // 5 minutes after currentTime.
                .jwtID(UUID.randomUUID().toString())
                .notBeforeTime(currentTime)
                .issueTime(currentTime)
                .build();
        }
    
    
    }
    

    This configuration class glues everything together:

    import com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.azure.activedirectory.AzureActiveDirectoryAssertionException;
    import com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.azure.activedirectory.AzureActiveDirectoryCertificateSignedJwtAssertionFactory;
    import com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.azure.activedirectory.AzureActiveDirectoryJwtClientAuthenticationParametersConverter;
    import org.springframework.core.env.Environment;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
    import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter;
    import org.springframework.security.oauth2.client.registration.ClientRegistration;
    import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
    import org.springframework.util.StringUtils;
    
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    @EnableWebSecurity
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
        private static final Pattern ISSUER_URI_PATTERN = Pattern.compile("https://login.microsoftonline.com/(.*?)/v2.0");
    
        private final Environment environment;
        private final ClientRegistrationRepository repository;
    
        public WebSecurityConfiguration(Environment environment, ClientRegistrationRepository repository) {
            this.environment = environment;
            this.repository = repository;
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // @formatter:off
            http.oauth2Login()
                    .tokenEndpoint()
                      // Please, note the name of the client provided, 
                      // as we will see it will match the one in the configuration
                        .accessTokenResponseClient(accessTokenResponseClient(Collections.singletonList("client-1"), repository))
                        .and()
                    .and()
                .authorizeRequests()
                    .anyRequest().authenticated();
            // @formatter:off
        }
    
        private DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient(
            List<String> registrationIds, ClientRegistrationRepository repository
        ) throws AzureActiveDirectoryAssertionException {
            OAuth2AuthorizationCodeGrantRequestEntityConverter converter =
                new OAuth2AuthorizationCodeGrantRequestEntityConverter();
            converter.addParametersConverter(
                new AzureActiveDirectoryJwtClientAuthenticationParametersConverter<>(createFactoryMap(registrationIds, repository)));
            DefaultAuthorizationCodeTokenResponseClient client = new DefaultAuthorizationCodeTokenResponseClient();
            client.setRequestEntityConverter(converter);
            return client;
        }
    
        private Map<String, AzureActiveDirectoryCertificateSignedJwtAssertionFactory> createFactoryMap(
            List<String> registrationIds, ClientRegistrationRepository repository
        ) throws AzureActiveDirectoryAssertionException {
            Map<String, AzureActiveDirectoryCertificateSignedJwtAssertionFactory> factories = new HashMap<>();
            for (String registrationId: registrationIds) {
                AzureActiveDirectoryCertificateSignedJwtAssertionFactory factory = createFactory(registrationId, repository);
                if (factory != null) {
                    factories.put(registrationId, factory);
                }
            }
            return factories;
        }
    
        private AzureActiveDirectoryCertificateSignedJwtAssertionFactory createFactory(
            String registrationId, ClientRegistrationRepository repository
        ) throws AzureActiveDirectoryAssertionException {
            String clientCertificatePath = environment.getProperty(
                String.format("spring.security.oauth2.client.registration.%s.client-certificate-path", registrationId));
            if (!StringUtils.hasText(clientCertificatePath)) {
                return null;
            }
            String clientCertificatePassword = environment.getProperty(
                String.format("spring.security.oauth2.client.registration.%s.client-certificate-password", registrationId));
            if (!StringUtils.hasText(clientCertificatePassword)) {
                return null;
            }
            ClientRegistration registration = repository.findByRegistrationId(registrationId);
            String tenantId = getTenantIdFromIssuerUri(registration.getProviderDetails().getIssuerUri());
            String clientId = registration.getClientId();
            return new AzureActiveDirectoryCertificateSignedJwtAssertionFactory(
                clientCertificatePath, clientCertificatePassword, tenantId, clientId);
        }
    
        static String getTenantIdFromIssuerUri(String issuerUri) {
            Matcher matcher = ISSUER_URI_PATTERN.matcher(issuerUri);
            if (matcher.find()) {
                return matcher.group(1);
            }
            return null;
        }
    
    }
    

    Please, pay especial attention to the accessTokenResponseClient method.

    Finally, this is the relevant configuration:

    spring:
      security:
        oauth2:
          client:
            provider: # Refs: https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2login-common-oauth2-provider
              azure-active-directory:
                issuer-uri: https://login.microsoftonline.com/${tenant-id}/v2.0 # Refs: https://docs.spring.io/spring-security/site/docs/current/reference/html5/#webflux-oauth2-login-openid-provider-configuration
            registration:
              client-1:
                provider: azure-active-directory
                client-id: ${client-1-client-id}
                client-authentication-method: private_key_jwt
                client-certificate-path: ${client-1-certificate-path}
                client-certificate-password: ${client-1-certificate-password}
                scope: openid, profile
                redirect-uri: http://localhost:8080/login/oauth2/code/
    

    Please, don’t forget to set the appropriate value for the different placeholders defined in the above configuration.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search