spring-bootspring-securityoauth-2.0

spring oauth client (v6.4.2): does not redirect to oauth-server


I try to run all 3 oAuth components of oAuth using Spring Boot 3.4.1. I used the oauth-server-docs and client + resource-server docs to implement.

The oAuth client can have 2 modes:

  1. to log users in using OAuth 2.0 or OpenID Connect 1.0, and
  2. to obtain an access token for users (using RestClient/WebClient).

I want to use both, which is possible according to the docs.

My code - all components - is on GitHub: https://github.com/OhadR/oAuth2-sample

All seems to work fine except one thing: when the client-app tries to call the resource-server before the user has logged in, the resource-server return 401 (which is ok, i think) but the client-app, rather than redirecting to the oauth-server, just return 500.

I have tried to debug Spring, for no avail.

I see that in OAuth2AuthorizationRequestRedirectFilter.doFilterInternal(), when exception is thrown from the rest of the filter-chain, Spring checks whether it is ClientAuthorizationRequiredException and if so it sendRedirectForAuthorization(). But in my case from some reason this code never runs (I put a breakpoint there and it never stops there).

Also, I see that AuthorizationCodeOAuth2AuthorizedClientProvider.authorize(), which is the one that suppose to throw the ClientAuthorizationRequiredException, is not called. (It is called only in "login" flow, when client-app calls the resource-server after user already logged in).

what does work? mode (1) above, meaning the client-app has a "login" button, which calls /login/oauth2/code/{registrationId}. This redirects to the oauth-server, the user logs-in, and then when the client-app tries to call the resource-server, it works fine, it gets 200 with response.

But mode (2) is not working. What am I missing?

Thanks!


Solution

  • Let's call what sends the request F, the application in the middle G and the resource server at the end RS.

    As RS is configured as an OAuth2 resource server (oauth2ResourceServer in Spring Security DSL), it expects requests to be authorized with a Bearer token issued by an authorization server it trusts (or one of the authorization servers it trusts in case of multitenancy).

    Access tokens are issued to OAuth2 clients, which can be either

    There are two main OAuth2 flows to get an access token (both F and Gcan use either of it):

    Spring Security gives complete flexibility for implementing the various options from above. A few things to consider however:

    On G, RestClient and WebClient requests authorization is not directly tied to the state of the session and we should provide respectively ClientHttpRequestInterceptor or ExchangeFilterFunction to set the authorization header with a Bearer token.

    For now, Spring Security provides OAuth2ClientHttpRequestInterceptor and ServerOAuth2AuthorizedClientExchangeFilterFunction (ServletOAuth2AuthorizedClientExchangeFilterFunction when using WebClient in a servlet). Those set the authorization header with a token obtained using a client registration (can be an authorization code registration in an app with oauth2Login or a registration with client credentials in any kind of app).

    When using a registration with authorization code, this functions expect the session to be authorized (the user must be logged-in already). So, endpoints delegating some of their processing to RS must be protected with isAuthenticated() or the internal request will be answered with a 401.

    The 302 Redirect to login happens on applications with oauth2Login only if the endpoint receiving a request is configured with isAuthenticated() (or requires any authority). The requests sent by a REST client internal to this application are not concerned (the case of the request interceptors / filter functions referenced above and for which you are pulling your hair).

    If wanting to forward the access token in the security context, we need to write a ClientHttpRequestInterceptor or ExchangeFilterFunction ourselves (nothing complicated).

    I created a Spring Boot starter to auto-configure RestClient or WebClient beans with OAuth2 authorization (and HTTP proxy if needed) using just application properties. You should give it an eye, it might ease your life, whatever option you choose.

    Sample where G is a Boot app with Thymeleaf and oauth2Login

    I use here:

    <dependency>
        <groupId>com.c4-soft.springaddons</groupId>
        <artifactId>spring-addons-starter-rest</artifactId>
    </dependency>
    
    spring:
      security:
        oauth2:
          client:
            provider:
              sso:
               issuer-uri: ${issuer}
            registration:
              messaging-client-oidc:
                provider: sso
                authorization-grant-type: authorization_code
                client-id: messaging-oauth2-client
                client-secret: change-me
                scope: openid
    
    com:
      c4-soft:
        springaddons:
          rest:
            client:
              message-client:
                base-url: ${messages.backend-base-uri}
                authorization:
                  oauth2:
                    oauth2-registration-id: messaging-client-oidc
    
    // This can be generated from the OpenAPI spec of the remote service
    @HttpExchange
    public interface MessageApi {
      @GetExchange(value = "/messages")
      public List<String> getMessages();
    }
    
    @Configuration
    public class RestConfiguration {
      // messageClient @Bean is auto-configured by spring-addons using the properties above
      // messageApi @Bean is a generated proxy implementing the MessageApi  interface above
      @Bean
      MessageApi messageApi(RestClient messageClient) throws Exception {
        return new RestClientHttpExchangeProxyFactoryBean<>(MessageApi.class, mesageClient).getObject();
      }
    }
    
    @Controller
    @RequiredArgsConstructor
    public class MessagesController {
      private final MessageApi messageApi;
    
      @GetMapping(value = "/messages-restclient-attrs")
      public String messagesUsingRestClientWithAttributes(Model model) {
        model.addAttribute("messages", messageApi.getMessages());
        return "index";
      }
    }
    

    I skipped the security configuration with oauth2Login which can be rather cumbersome when needing to map Spring authorities from nested private claims (case of Keycloak and Auth0 for instance). See this other starter of mine to do it using just application properties.

    If G was an OAuth2 BFF, it would have a similar registration in properties, but would probably not declare and use REST client itself. However, the TokenRelay filter would work mostly like the messageClient above (using tokens from a registration for authorization-code & refresh-token flows).

    If G was a resource server, it would be configured either with a client-credentials registration, or to forward the Bearer in the security context (maybe obtained in the first place by F using authorization-code flow). The messageClient and messageApi beans would be created the same way (only the application properties would change).