spring-bootspring-security

Spring Security oauth2Login vs oauth2Client


I want my application to be able to make REST API requests on behalf of the users of a certain platform. I've registered my app on the platform, and they have OAuth2 support with the following endpoints:

{
  "access_token": "...",
  "refresh_token": "...",
  "user": {"id":5, "name": "First Last"}
}

At first I used Spring Security's oauth2Login:

spring:
  security:
    oauth2:
      client:
        provider:
          the-provider:
            authorization-uri: https://provider.domain.com/login/oauth2/auth
            token-uri: https://provider.domain.com/login/oauth2/token
            user-info-authentication-method: Bearer
            user-info-uri: https://provider.domain.com/api/users/me
        registration:
          the-provider:
            authorization-grant-type: authorization_code
            client-id: my-client-id
            client-secret: myClientS3cret
            redirect-uri: "{baseUrl}/login/oauth2/code/the-provider"
@Bean
public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
    http.oauth2Login(Customizer.withDefaults());
    http.authorizeHttpRequests(r -> r.anyRequest().authenticated());
    return http.build();
}

Out of the box a user who accesses my app at "/" is automatically is taken through the OAuth2 flow and Spring Security puts the user info into the security context. This is nice but I don't see how to get/use the access token to make subsequent API requests on behalf of the user.

The Authentication is a OAuth2AuthenticationToken whose principal is a DefaultOAuth2User whose authorities are [ "OAUTH2_USER" ] and whose attributes are data from the user-info-uri, but I don't see anywhere that would contain the access token and refresh token.

Doing a little more research, I realized maybe I should be using oauth2Client instead of oauth2Login but it's not entirely clear to me how that would work.

Just to clarify a bit of what I'm imagining, it would be something like

@Controller
public class MyController {
    
    private final ReportService reportService;
    
    public MyController(final ReportService reportService) {
        this.reportService = reportService;
    }
    
    @GetMapping({"", "/"})
    public String index(final Model model) {
        model.addAttribute("report", reportService.generateReport());
        return "index";
    }
}

where the ReportService makes a couple of REST API requests using the access_token generated for the user, which I imagine it would get off of the security context authentication.

I guess my question is sort of geared towards is this something that I can get out of the box with Spring Security (via oauth2Login or oauth2Client, or would I need to build something custom with the Spring Security primitives?


Solution

  • oauth2Login configures the app with authorization code and refresh token flows. So by conception, an app with oauth2Login is an OAuth2 client.

    An oauth2Client is a Spring application that gets tokens from an authorization server. With Boot, OAuth2 clients are configured with spring.security.oauth2.client.* properties. For oauth2Login to work, those properties must expose at least one registration with authorization_code.

    Note that you can make about any kind of application (not only oauth2Login, but also oauth2ResourceServer, formLogin, etc.) an OAuth2 client if it needs to authorize requests to a resource server.

    In a Spring OAuth2 client application, you can get tokens from the (Reactive)OAuth2AuthorizedClientManager.

    Do It Yourself

    @Service
    public class ReportServiceImpl implements ReportService {
        private final OAuth2AuthorizedClientManager clients;
        private final OAuth2AuthorizeRequest req;
        
    
        public ReportServiceImpl(OAuth2AuthorizedClientManager clients, @Value("${report-client-registration-id:the-provider}") String reportClientRegistrationId) {
            super();
            this.clients = clients;
            this.req = OAuth2AuthorizeRequest.withClientRegistrationId(reportClientRegistrationId).build();
        }
    
    
        @Override
        public ReportDto generateReport() {
            final var authorized = clients.authorize(req);
            final var bearerString = "Bearer %s".formatted(authorized.getAccessToken().getTokenValue());
            // TODO: set the bearerString as Authorization header to the request using your favorite client;
            
            return new ReportDto();
        }
    
    }
    

    Note that most REST clients include features to authorize all requests transparently (get the token from an "authorized client" and set the Authorization header without cluttering the service code). For instance, RestClient can be configured with requestInterceptor functions, and WebClient with filter functions.

    Using spring-addons-starter-rest

    To avoid writing the code for the REST client and its authorization, I publish a Spring Boot starter. It integrates with Spring's HttpServiceProxyFactory to add RestClient (or WebClient) auto-configuration for HTTP proxy and OAuth2 (or Basic) authorization.

    Sample remote service description (probably generated from an OpenAPI spec using a tool like the openapi-generator-maven-plugin):

    @HttpExchange(accept = MediaType.APPLICATION_JSON_VALUE)
    public interface ReportApi {
        @GetExchange(url = "/report")
        ReportDto generateReport();
    }
    

    spring-addons dependency:

    <dependency>
        <groupId>com.c4-soft.springaddons</groupId>
        <artifactId>spring-addons-starter-rest</artifactId>
        <version>7.8.8</version>
    </dependency>
    

    spring-addons configuration to use a token from an authorized client (an alternate strategy is forwarding the Bearer which authorized the incoming request, but this is possible only on a resource server):

    com:
      c4-soft:
        springaddons:
          rest:
            client:
              report-api:
                base-url: http://reporting-service
                authorization:
                  oauth2:
                    # Kept your registration ID, even if it is rather confusing
                    oauth2-registration-id: the-provider
    

    Having the remote service client generated:

    @Configuration
    public class RestConfiguration {
        @Bean
        ReportApi reportApi(SpringAddonsRestClientSupport restSupport) {
            return restSupport.service("report-api", ReportApi.class);
        }
    }
    

    Usage in a Spring component:

    @Controller
    @RequiredArgsConstructor
    public class SomeController {
        // Abracadabra! This is successfully auto-wired
        private final ReportApi reportApi;
    
        ...
        
    }
    

    Note that there is absolutely no code for ReportApi implementation or authorization, all is generated!

    In my projects, even the client interfaces (those decorated with @HttpExchange like the ReportApi above) are generated from the OpenAPI spec, itself generated by Swagger from the remote service @RestController sources (decorated with @RequestMapping variants).