How to configure Spring Cloud Azure Active Directory and Spring Boot OAuth2 together?

138 Views Asked by At

I've configured GitHub and Google via Spring Security application.properties and now I'm trying to add Active Directory.

I've added spring-cloud-azure-starter-active-directory and I also configured it via application.properties, so the effective configuration file looks like this:

spring:
  cloud:
    azure:
      active-directory:
        enabled: true
        profile:
          tenant-id: common
        credential:
          client-id: ##REDACTED##
          client-secret: ##REDACTED##
        redirect-uri-template: "{baseUrl}/oauth2/callback/azure"
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: ##REDACTED##
            client-secret: ##REDACTED##
            redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}"
          google:
            client-id: ##REDACTED##
            client-secret: ##REDACTED##
            redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}"

My security configuration:


@Configuration
public class WebSecurityConfig
{
  @Bean
  public SecurityFilterChain defaultSecurityFilterChain(
          HttpSecurity http,
          OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler,
          OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler,
          OAuth2AuthorizationRequestRepository requestRepository
  )
          throws Exception
  {
    http
            .cors(AbstractHttpConfigurer::disable)
            .oauth2Login(oauth2Login -> oauth2Login
                    .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint
                            .baseUri("/oauth2/authorize")
                            .authorizationRequestRepository(requestRepository))
                    .redirectionEndpoint(redirectionEndpoint -> redirectionEndpoint
                            .baseUri("/oauth2/callback/*"))
                    .successHandler(oAuth2AuthenticationSuccessHandler)
                    .failureHandler(oAuth2AuthenticationFailureHandler)
            );
    return http.build();
  }

Now, the Single Sign On option now works only for Microsoft, while opening URL for SSO for GitHub and Google results with exception like below. It looks like Azure starter overwrites ClientRegistrationRepository bean and does not include other clients.

2024-02-21 15:20:29.322 DEBUG [-nio-8080-exec-2] [FilterChainProxy          ] Securing GET /oauth2/authorize/google?redirect_uri=http://localhost:3000/redirect/
2024-02-21 15:20:29.325  WARN [-nio-8080-exec-2] [ationRequestRedirectFilter] Authorization Request failed: org.springframework.security.oauth2.client.web.InvalidClientRegistrationIdException: Invalid Client Registration with Id: google
org.springframework.security.oauth2.client.web.InvalidClientRegistrationIdException: Invalid Client Registration with Id: google
    at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver.resolve(DefaultOAuth2AuthorizationRequestResolver.java:152)
    at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver.resolve(DefaultOAuth2AuthorizationRequestResolver.java:111)
    at org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.doFilterInternal(OAuth2AuthorizationRequestRedirectFilter.java:170)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240)
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227)
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93)
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240)
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227)
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137)
    at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240)
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227)
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137)
    at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82)
    at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69)
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240)
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227)
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240)
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227)
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137)
    at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240)
    at org.springframework.security.web.ObservationFilterChainDecorator$AroundFilterObservation$SimpleAroundFilterObservation.lambda$wrap$0(ObservationFilterChainDecorator.java:323)
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:224)
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191)
    at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
    at org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:195)
    at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
    at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74)
    at org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:225)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:268)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
    at org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:109)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
    at org.springframework.web.filter.CorsFilter.doFilterInternal(CorsFilter.java:91)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:340)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:896)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1744)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.base/java.lang.Thread.run(Thread.java:1583)

pom.xml

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.2</version>
    <relativePath/>
    <!-- lookup parent from repository -->
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-jose</artifactId>
    </dependency>
    <dependency>
        <groupId>com.azure.spring</groupId>
        <artifactId>spring-cloud-azure-starter-active-directory</artifactId>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.azure.spring</groupId>
            <artifactId>spring-cloud-azure-dependencies</artifactId>
            <version>5.9.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
2

There are 2 best solutions below

0
ch4mp On

When using several OpenID Providers, it is generally easier to choose a main one emitting JWT access tokens and to configure the others as identity providers for it. For instance, you could configure "Login with Google" in AAD (if in your case it issues just JWTs).

You could also choose to have n+1 identity sources model: a Keycloak instance (or Spring Authorization Server or whatever) being used just to federate others and as only "source of truth" for your backend.

Also, you really don't have to use Microsoft Boot starter to use AAD as OpenID Provider for a Spring OAuth2 client. You can use just spring-boot-starter-oauth2-client. In addition to Spring "official" starter, you may use this other starter I wrote to ease security conf with any OpenID Providers (works with AAD, but also Keycloak, Auth0, Amazon Cognito and many others, and possibly with several at a time).

6
Jakub On

Okay, I end up with removing spring-cloud-azure-starter-active-directory and configuring Microsoft SSO via spring-boot-starter-oauth2-client only.

I checked client registry configuration for Azure and rewrote it to Spring Security OAuth2 config. Please see spring.security.oauth2.client.provider.entra

The configuration works for Microsoft personal, and organization accounts.

  security:
    oauth2:
      client:
        provider:
          entra:
            authorization-uri: https://login.microsoftonline.com/common/oauth2/v2.0/authorize
            token-uri: https://login.microsoftonline.com/common/oauth2/v2.0/token
            jwk-set-uri: https://login.microsoftonline.com/common/discovery/v2.0/keys
            user-info-authentication-method: header
        registration:
          github:
            client-id: 
            client-secret: 
            redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}"
          google:
            client-id: 
            client-secret: 
            redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}"
          azure:
            provider: entra
            client-id: 
            client-secret: 
            client-authentication-method: client_secret_basic
            redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}"
            authorization-grant-type: authorization_code
            scope:
              - openid
              - profile
              - offline_access