Personalizando solicitações de autorização e token com o cliente Spring Security 5.1

Personalizando solicitações de autorização e token com o cliente Spring Security 5.1

1. Visão geral

Às vezes, as APIs do OAuth2 podem divergir um pouco do padrão; nesse caso, precisamos fazer algumas personalizações nas solicitações do OAuth2 padrão.

Spring Security 5.1 fornece suporte para customizar a autorização OAuth2 e solicitações de token.

Neste tutorial, veremos como personalizar os parâmetros de solicitação e tratamento de resposta.

2. Solicitação de autorização personalizada

Primeiro, personalizaremos a solicitação de autorização OAuth2. Podemos modificar parâmetros padrão e adicionar parâmetros extras à solicitação de autorização, conforme necessário.

Para fazer isso,we need to implement our own OAuth2AuthorizationRequestResolver:

public class CustomAuthorizationRequestResolver
  implements OAuth2AuthorizationRequestResolver {

    private OAuth2AuthorizationRequestResolver defaultResolver;

    public CustomAuthorizationRequestResolver(
      ClientRegistrationRepository repo, String authorizationRequestBaseUri) {
        defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repo, authorizationRequestBaseUri);
    }

    // ...
}

Observe que usamosDefaultOAuth2AuthorizationRequestResolver para fornecer a funcionalidade básica.

Também substituiremos os métodosresolve() para adicionar nossa lógica de personalização:

public class CustomAuthorizationRequestResolver
  implements OAuth2AuthorizationRequestResolver {

    //...

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
        OAuth2AuthorizationRequest req = defaultResolver.resolve(request);
        if(req != null) {
            req = customizeAuthorizationRequest(req);
        }
        return req;
    }

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
        OAuth2AuthorizationRequest req = defaultResolver.resolve(request, clientRegistrationId);
        if(req != null) {
            req = customizeAuthorizationRequest(req);
        }
        return req;
    }

    private OAuth2AuthorizationRequest customizeAuthorizationRequest(
      OAuth2AuthorizationRequest req) {
        // ...
    }

}

Adicionaremos nossas personalizações mais tarde, usando nosso métodocustomizeAuthorizationRequest(), conforme discutiremos nas próximas seções.

Depois de implementar nossoOAuth2AuthorizationRequestResolver personalizado, precisamos adicioná-lo à nossa configuração de segurança:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.oauth2Login()
          .authorizationEndpoint()
          .authorizationRequestResolver(
            new CustomAuthorizationRequestResolver(
              clientRegistrationRepository(), "/oauth2/authorize-client"))
        //...
    }
}

Aqui, usamosoauth2Login().authorizationEndpoint().authorizationRequestResolver() para injetar nossoOAuth2AuthorizationRequestResolver. personalizado

3. Customizando Parâmetros Padrão de Solicitação de Autorização

Agora, vamos discutir a personalização real. Podemos modificarOAuth2AuthorizationRequest o quanto quisermos.

Para começar,we can modify a standard parameter for each authorization request.

Podemos, por exemplo, gerar nosso próprio parâmetro“state”:

private OAuth2AuthorizationRequest customizeAuthorizationRequest(
  OAuth2AuthorizationRequest req) {
    return OAuth2AuthorizationRequest
      .from(req).state("xyz").build();
}

4. Parâmetros extras de solicitação de autorização

We can also add extra parameters to our OAuth2AuthorizationRequest usando o métodoadditionalParameters() deOAuth2AuthorizationRequeste passando em umMap:

private OAuth2AuthorizationRequest customizeAuthorizationRequest(
  OAuth2AuthorizationRequest req) {
    Map extraParams = new HashMap();
    extraParams.putAll(req.getAdditionalParameters());
    extraParams.put("test", "extra");

    return OAuth2AuthorizationRequest
      .from(req)
      .additionalParameters(extraParams)
      .build();
}

Também temos que nos certificar de que incluímos osadditionalParameters antigos antes de adicionarmos os novos.

Vamos ver um exemplo mais prático personalizando a solicitação de autorização usada com o servidor de autorização Okta.

4.1. Pedido de autorização de Okta personalizado

Okta tem parâmetros opcionais extras para solicitação de autorização para fornecer ao usuário mais funcionalidade. Por exemplo,idp que indica o provedor de identidade.

O provedor de identidade é Okta por padrão, mas podemos personalizá-lo usando o parâmetroidp:

private OAuth2AuthorizationRequest customizeOktaReq(OAuth2AuthorizationRequest req) {
    Map extraParams = new HashMap();
    extraParams.putAll(req.getAdditionalParameters());
    extraParams.put("idp", "https://idprovider.com");
    return OAuth2AuthorizationRequest
      .from(req)
      .additionalParameters(extraParams)
      .build();
}

5. Pedido de token personalizado

Agora, veremos como personalizar a solicitação de token OAuth2.

Podemos personalizar a solicitação de token personalizandoOAuth2AccessTokenResponseClient.

A implementação padrão paraOAuth2AccessTokenResponseClient éDefaultAuthorizationCodeTokenResponseClient.

Podemos personalizar a própria solicitação de token fornecendo umRequestEntityConverter personalizado e podemos até personalizar o tratamento da resposta de token personalizandoDefaultAuthorizationCodeTokenResponseClientRestOperations:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.tokenEndpoint()
          .accessTokenResponseClient(accessTokenResponseClient())
            //...
    }

    @Bean
    public OAuth2AccessTokenResponseClient accessTokenResponseClient(){
        DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
          new DefaultAuthorizationCodeTokenResponseClient();
        accessTokenResponseClient.setRequestEntityConverter(new CustomRequestEntityConverter());

        OAuth2AccessTokenResponseHttpMessageConverter tokenResponseHttpMessageConverter =
          new OAuth2AccessTokenResponseHttpMessageConverter();
        tokenResponseHttpMessageConverter.setTokenResponseConverter(new CustomTokenResponseConverter());
        RestTemplate restTemplate = new RestTemplate(Arrays.asList(
          new FormHttpMessageConverter(), tokenResponseHttpMessageConverter));
        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());

        accessTokenResponseClient.setRestOperations(restTemplate);
        return accessTokenResponseClient;
    }
}

Podemos injetar nosso próprioOAuth2AccessTokenResponseClient usingtokenEndpoint().accessTokenResponseClient().

Para personalizar os parâmetros de solicitação de token, implementaremosCustomRequestEntityConverter. Da mesma forma, para personalizar o tratamento da resposta do token, implementaremosCustomTokenResponseConverter.

DiscutiremosCustomRequestEntityConverter eCustomTokenResponseConverter nas seções a seguir.

6. Parâmetros extras de solicitação de token

Agora, veremos como adicionar parâmetros extras à nossa solicitação de token criando umConverter personalizado:

public class CustomRequestEntityConverter implements
  Converter> {

    private OAuth2AuthorizationCodeGrantRequestEntityConverter defaultConverter;

    public CustomRequestEntityConverter() {
        defaultConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
    }

    @Override
    public RequestEntity convert(OAuth2AuthorizationCodeGrantRequest req) {
        RequestEntity entity = defaultConverter.convert(req);
        MultiValueMap params = (MultiValueMap) entity.getBody();
        params.add("test2", "extra2");
        return new RequestEntity<>(params, entity.getHeaders(),
          entity.getMethod(), entity.getUrl());
    }

}

NossoConverter transformaOAuth2AuthorizationCodeGrantRequest em aRequestEntity. 

Usamos o conversor padrãoOAuth2AuthorizationCodeGrantRequestEntityConverter para fornecer a funcionalidade básica e adicionamos parâmetros extras ao corpo deRequestEntity.

7. Manuseio de resposta de token personalizado

Agora, vamos personalizar o tratamento da resposta do token.

Podemos usar o conversor de resposta de token padrãoOAuth2AccessTokenResponseHttpMessageConverter como ponto de partida.

ImplementaremosCustomTokenResponseConverter para lidar com o parâmetro“scope” de maneira diferente:

public class CustomTokenResponseConverter implements
  Converter, OAuth2AccessTokenResponse> {
    private static final Set TOKEN_RESPONSE_PARAMETER_NAMES = Stream.of(
        OAuth2ParameterNames.ACCESS_TOKEN,
        OAuth2ParameterNames.TOKEN_TYPE,
        OAuth2ParameterNames.EXPIRES_IN,
        OAuth2ParameterNames.REFRESH_TOKEN,
        OAuth2ParameterNames.SCOPE).collect(Collectors.toSet());

    @Override
    public OAuth2AccessTokenResponse convert(Map tokenResponseParameters) {
        String accessToken = tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN);

        Set scopes = Collections.emptySet();
        if (tokenResponseParameters.containsKey(OAuth2ParameterNames.SCOPE)) {
            String scope = tokenResponseParameters.get(OAuth2ParameterNames.SCOPE);
            scopes = Arrays.stream(StringUtils.delimitedListToStringArray(scope, ","))
                .collect(Collectors.toSet());
        }

        //...
        return OAuth2AccessTokenResponse.withToken(accessToken)
          .tokenType(accessTokenType)
          .expiresIn(expiresIn)
          .scopes(scopes)
          .refreshToken(refreshToken)
          .additionalParameters(additionalParameters)
          .build();
    }

}

O conversor de resposta de token transformaMap emOAuth2AccessTokenResponse.

Neste exemplo, analisamos o parâmetro“scope” como delimitado por vírgulas em vez deString. delimitado por espaço

Vejamos outro exemplo prático, personalizando a resposta do token usando o LinkedIn como um servidor de autorização.

7.1. Tratamento de resposta de token do LinkedIn

Finalmente, vamos ver como lidar com a resposta de tokenLinkedIn. Contém apenasaccess_tokeneexpires_in,, mas também precisamos detoken_type.

Podemos simplesmente implementar nosso próprio conversor de resposta de token e definirtoken_type manualmente:

public class LinkedinTokenResponseConverter
  implements Converter, OAuth2AccessTokenResponse> {

    @Override
    public OAuth2AccessTokenResponse convert(Map tokenResponseParameters) {
        String accessToken = tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN);
        long expiresIn = Long.valueOf(tokenResponseParameters.get(OAuth2ParameterNames.EXPIRES_IN));

        OAuth2AccessToken.TokenType accessTokenType = OAuth2AccessToken.TokenType.BEARER;

        return OAuth2AccessTokenResponse.withToken(accessToken)
          .tokenType(accessTokenType)
          .expiresIn(expiresIn)
          .build();
    }
}

8. Conclusão

Neste artigo, aprendemos como personalizar solicitações de autorização e token do OAuth2 adicionando ou modificando parâmetros de solicitação.

O código-fonte completo dos exemplos está disponívelover on GitHub.