Dynamically configure scope for client registrations with Spring Security 5 OAuth2.0
Asked Answered
S

2

6

I want to dynamically change scope for client registrations. I know how to set up registrations this way:

spring:
  security:
    oauth2:
      client:
        registration:
          custom:
            client-id: clientId
            client-secret: clientSecret
            authorization-grant-type: client_credentials
        provider:
          custom:
            token-uri: http://localhost:8081/oauth/token

How can I configure this programatically?

Strictly answered 31/8, 2020 at 23:34 Comment(0)
P
4

You need to provide custom ClientRegistrationRepository bean. It is described in docs.

@Configuration
public class OAuth2LoginConfig {

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
    }

    private ClientRegistration googleClientRegistration() {
        return ClientRegistration.withRegistrationId("google")
            .clientId("google-client-id")
            .clientSecret("google-client-secret")
            .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
            .scope("openid", "profile", "email", "address", "phone")
            .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
            .tokenUri("https://www.googleapis.com/oauth2/v4/token")
            .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
            .userNameAttributeName(IdTokenClaimNames.SUB)
            .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
            .clientName("Google")
            .build();
    }
}
Puck answered 1/9, 2020 at 9:9 Comment(2)
It looks like this gets defined at startup. Is there an easy way to make this dynamic? I need the scope to depend on the client request.Strictly
@Strictly did you found a solution to make ClientRegistration more dynamic and not during startup?Monandry
L
2

Disclaimer: This is a hacky way, but it works

(and if its stupid but it works, it ain't stupid)

All relevant parts will always use the ClientRegistrationRepository to find the ClientRegistration (and with that, the scopes).

So my hacky-way to solve this was to build a wrapper around the InMemoryClientRegistrationRepository. In my case, I wanted to allow any additional scope to be requested from the client side, so I wanted to add additional scopes from the query parameter scope.

Here's the example code for this solution:

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties properties) {
        final List<ClientRegistration> registrations = new ArrayList<>(OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties).values());
        // this is the ClientRegistrationRepository that would be used by default configuration
        final ClientRegistrationRepository parent = new InMemoryClientRegistrationRepository(registrations);

        // this lambda is our wrapper around the configuration based ClientRegistrationRepository
        return (registrationId) -> {
            final ClientRegistration clientRegistration = parent.findByRegistrationId(registrationId);
            if (clientRegistration == null) {
                return null;
            }

            final HttpServletRequest request = Optional.ofNullable(RequestContextHolder.getRequestAttributes())
                    .filter(ServletRequestAttributes.class::isInstance)
                    .map(ServletRequestAttributes.class::cast)
                    .map(ServletRequestAttributes::getRequest)
                    .orElse(null);

            final String query;
            if (request == null || (query = request.getQueryString()) == null) {
                return clientRegistration;
            }

            final List<String> scopeQueryParam = parseQuery(query).get(OAuth2ParameterNames.SCOPE);
            if (scopeQueryParam == null) {
                return clientRegistration;
            }

            final Set<String> scopes = scopeQueryParam.stream()
                    .flatMap((v) -> Arrays.stream(v.split(" ")))
                    .collect(Collectors.toSet());

            if (clientRegistration.getScopes().containsAll(scopes)) {
                return clientRegistration;
            }

            final Set<String> resultingScopes = new HashSet<>(scopes);
            resultingScopes.addAll(clientRegistration.getScopes());

            return ClientRegistration.withClientRegistration(clientRegistration)
                    .scope(resultingScopes)
                    .build();
        };
    }

    private static MultiValueMap<String, String> parseQuery(String query) {
        final MultiValueMap<String, String> result = new LinkedMultiValueMap<>();

        final String[] pairs = query.split("&");
        String[] pair;

        for (String _pair : pairs) {
            pair = _pair.split("=");

            if (pair.length >= 1) {
                final List<String> values = result.computeIfAbsent(URLDecoder.decode(pair[0], StandardCharsets.UTF_8), (k) -> new ArrayList<>());

                if (pair.length >= 2) {
                    values.add(URLDecoder.decode(pair[1], StandardCharsets.UTF_8));
                }
            }
        }

        return result;
    }
Lyallpur answered 5/10, 2021 at 18:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.