kernel
kernel

Reputation: 783

What is the difference between spring-boot-starter-oauth2-client and spring-boot-starter-oauth2-resource-server

Spring Boot version: 3.2.2

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

What exactly is the difference between these two dependencies? I don't fully understand which one I should use in which situation. In my project, I perform Token transactions with Keycloak. Even though I used AuthenticationManagerResolver when my previous Spring Boot version was 2.7.4, I needed to define the following parameters. When I upgrade to version 3.2.2, I can verify tokens using Keycloak, even though I do not use these parameters:

spring.security.oauth2.client.registration.keycloak.provider=keycloak
spring.security.oauth2.client.registration.keycloak.client-id=payment-client
spring.security.oauth2.client.registration.keycloak.client-secret=apK0d8j5mmn32Do9Es
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.scope=openid,read,write
spring.security.oauth2.client.registration.keycloak.redirect-uri=http://keycloak.payment/login/oauth2/code/keycloak

spring.security.oauth2.client.provider.keycloak.authorization-uri=http://keycloak.payment/realms/payment-realm/protocol/openid-connect/auth
spring.security.oauth2.client.provider.keycloak.token-uri=http://keycloak.payment/realms/payment-realm/protocol/openid-connect/token
spring.security.oauth2.client.provider.keycloak.user-info-uri=http://keycloak.payment/realms/payment-realm/protocol/openid-connect/userinfo
spring.security.oauth2.client.provider.keycloak.jwk-set-uri=http://keycloak.payment/realms/payment-realm/protocol/openid-connect/certs
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username
spring.security.oauth2.client.provider.keycloak.user-info-authentication-method=header

My currently Security class

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final AuthenticationEntryPoint tokenAuthenticationEntryPoint;
    private JwtAuthenticationManagerResolver authenticationManagerResolver;

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        http.cors(AbstractHttpConfigurer::disable);
        http.oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer.authenticationManagerResolver(authenticationManagerResolver).authenticationEntryPoint(tokenAuthenticationEntryPoint));
        http.authorizeHttpRequests((authorize) -> authorize.requestMatchers(
                        "/swagger-ui/**").permitAll().anyRequest().authenticated())
                .sessionManagement(manager -> manager.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }
}

My currently AuthenticationManagerResolve class

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.stereotype.Component;

import java.util.Collections;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> {

    @Override
    public AuthenticationManager resolve(HttpServletRequest request) {
        JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider(JwtDecoders.fromIssuerLocation("http://keycloak.payment/payment-realm"));
        return new ProviderManager(Collections.singletonList(authenticationProvider));
    }
}

Even though I never specified the parameters I mentioned above, security is now verifying tokens properly. In what situation should I use these parameters? Currently I'm just using the "spring-boot-starter-oauth2-resource-server" dependency.

When is spring-boot-starter-oauth2-client dependency and above parameters needed.

Note: I use Keycloak only for token transactions. I use my own project for role and authority processing.

Upvotes: 0

Views: 1987

Answers (1)

ch4mp
ch4mp

Reputation: 12835

Short answer

As any Boot starter, both provide with some auto-configuration (use a few application properties to save a lot of Java Configuration code). And as per their names, spring-boot-starter-oauth2-client helps with OAuth2 client auto-configuration and spring-boot-starter-oauth2-resource-server OAuth2 resource server ones.

Something more developed

OAuth2 is a 4 actors thing:

  • resource owner (end-user, batch program, ...)
  • authorization server: authenticates resource owners and issues tokens
  • resource server: expects requests to be authorized with Bearer access token. It validates this tokens (either using a public JWT signing key or by introspecting the access token on the authorization server) and takes access control decisions. It does care who issued the token (can it trust the issuer?), to what (which client?), on behalf of who (the resource owner) and what for (scope and audience). But it really doesn't care how this token was acquired (which OAuth2 flow).
  • client: acquires tokens from the authorization server:
    • access token to authorize requests to resource server on behalf of the resource owner
    • ID token to get some info about the resource owner (if the authorization server complies with OpenID and if the openid scope is granted)
    • refresh token to get new access tokens when older ones expire (if offline_access scope is granted)

When to use spring-boot-starter-oauth2-resource-server

(and the spring.security.oauth2.resourceserver.* properties)

In apps exposing a REST API to OAuth2 clients, meaning to something capable of acquiring an access token and sending it as Bearer authorization header.

Such clients can be:

  • REST clients with a GUI like Postman
  • programmatic REST clients like RestClient, WebClient, RestTemplate, @FeignClient, ... (the last two being in "maintenance" mode should probably be avoided)
  • single-page or mobile applications configured as "public" client and using a client-side OAuth2 lib like angular-auth-oidc-client, but this is now discouraged
  • OAuth2 backend for frontend like Spring Cloud Gateway configured with oauth2Login and the TokenRelay filter

Note that a browser can't be an OAuth2 client without specific tooling (like a dedicated lib running in a Javascript application or a BFF), and won't be able to get protected resources from a resource server.

When to use spring-boot-starter-oauth2-client

(and the spring.security.oauth2.client.* properties)

When needing to call an OAuth2 resource server using one of the programmatic REST clients listed above or something like the Spring Cloud Gateway TokenRelay filter.

As a consequence, it is frequent to have client & resource server configuration in apps part of a micro-service system: when a resource server delegates some of the processing to an other one, it is a client for this other service.

A much less frequent case is a Spring app with publicly exposing a REST API to random OAuth2 clients and also a server-side rendered GUI (Thymeleaf or whatever) with oauth2Login and MVC controller calling the API with a programmatic REST client.

You can use it just to use oauth2Login, even if you don't use tokens afterwards to call a resource server, but then you should maybe question your architecture: why acquiring tokens if you don't use it?

About security conf

Requests from browsers to Spring backends configured with oauth2Login are authorized with session cookies (not with Bearer access tokens), which requires protection against CSRF attacks. It is usually expected that such a backend answers with 302 redirect to login when a user tries to access a protected URL but does not have an authorized session.

Requests from OAuth2 clients to oauth2ResourceServer are authorized with Bearer tokens. Sessions are usually disabled, which makes it insensible to CSRF attacks. Last, a resource servers can't reasonably answer something else than 401 Unauthorized when a token is invalid or issued by an untrusted authorization server, or when the token misses in requests to an endpoint that isn't permitAll. Resource server don't care how tokens are acquired and don't even know the flow the resource owner can use (authorization-code for users, client-credentials for batches, ...) or, in multi-tenant scenarios, the authorization server it should redirect to.

Because the security configuration requirements are not quite compatible (session VS stateless, CSRF protection enabled VS disabled, 302 VS 401) oauth2Login and oauth2ResourceServer should not be mixed in the same Security(Web)FilterChain. If you need the two in the same app, expose two different filter-chain beans. This is something I do frequently on OAuth2 BFFs: a filter-chain with oauth2Login for routes configured with the TokenRelay filter and another filter-chain with oauth2ResourceServer for other routes and, publicly accessible assets and REST resources (Spring Boot Actuator).

P.S.

I maintain an additional starter which pushes OAuth2 auto-configuration way further than official ones and makes quite a few frequent features configurable with just properties. To name a few:

  • change OAuth2 redirect URIs to point to a reverse-proxy instead of the internal OAuth2 client
  • give SPAs the control of where the user is redirected after login / logout
  • expose CSRF token in a cookie accessible to Javascript code running in a browser
  • adapt to not exactly standard RP-Initiated Logout (Auth0 and Amazon Cognito for instance)
  • add optional parameters to the authorization request (Auth0 audience or whatever)
  • change the HTTP status of OAuth2 redirections so that SPAs can choose how to follow to Location header
  • register two distinct SecurityFilterChain beans with respectively oauth2Login() (with session-based security and CSRF protection) and - oauth2ResourceServer() (stateless, with token-based security) to secure different groups of resources
  • define which endpoints are accessible to anonymous
  • on resource servers, accept tokens issued by more than just one  OpenID Provider
  • add an audience validator to JWT decoder(s)
  • map authorities from any claim(s) (and add prefix or force upper / lower case)

Upvotes: 2

Related Questions