Personnalisation des demandes d’autorisation et de jetons avec le client Spring Security 5.1

Personnalisation des demandes d'autorisation et de jetons avec le client Spring Security 5.1

1. Vue d'ensemble

Parfois, les API OAuth2 peuvent diverger un peu du standard, auquel cas nous devons procéder à certaines personnalisations des requêtes OAuth2 standard.

Spring Security 5.1 prend en charge la personnalisation des demandes d'autorisation et de jeton OAuth2.

Dans ce didacticiel, nous verrons comment personnaliser les paramètres de requête et la gestion des réponses.

2. Demande d'autorisation personnalisée

Tout d'abord, nous allons personnaliser la demande d'autorisation OAuth2. Nous pouvons modifier les paramètres standard et ajouter des paramètres supplémentaires à la demande d'autorisation selon les besoins.

Pour ce faire,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);
    }

    // ...
}

Notez que nous avons utilisé lesDefaultOAuth2AuthorizationRequestResolver pour fournir les fonctionnalités de base.

Nous remplacerons également les méthodesresolve() pour ajouter notre logique de personnalisation:

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) {
        // ...
    }

}

Nous ajouterons nos personnalisations plus tard en utilisant la méthode de notre méthodecustomizeAuthorizationRequest(), comme nous le verrons dans les sections suivantes.

Après avoir implémenté nosOAuth2AuthorizationRequestResolver personnalisés, nous devons l'ajouter à notre configuration de sécurité:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

Ici, nous avons utiliséoauth2Login().authorizationEndpoint().authorizationRequestResolver() pour injecter nosOAuth2AuthorizationRequestResolver. personnalisés

3. Personnalisation des paramètres standard de demande d'autorisation

Maintenant, parlons de la personnalisation réelle. Nous pouvons modifierOAuth2AuthorizationRequest autant que nous le voulons.

Pour commencer,we can modify a standard parameter for each authorization request.

Nous pouvons, par exemple, générer notre propre paramètre“state”:

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

4. Paramètres supplémentaires de la demande d'autorisation

We can also add extra parameters to our OAuth2AuthorizationRequest en utilisant la méthodeadditionalParameters() desOAuth2AuthorizationRequest et en passant unMap:

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

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

Nous devons également nous assurer que nous incluons les anciensadditionalParameters avant d'ajouter nos nouveaux.

Voyons un exemple plus pratique en personnalisant la demande d’autorisation utilisée avec le serveur d’autorisation Okta.

4.1. Demande d'autorisation Okta personnalisée

Okta a des paramètres optionnels supplémentaires pour la demande d'autorisation afin de fournir à l'utilisateur plus de fonctionnalités. Par exemple,idp qui indique le fournisseur d'identité.

Le fournisseur d'identité est Okta par défaut, mais nous pouvons le personnaliser à l'aide du paramètreidp:

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. Demande de jeton personnalisé

Nous allons maintenant voir comment personnaliser la demande de jeton OAuth2.

Nous pouvons personnaliser la demande de jeton en personnalisantOAuth2AccessTokenResponseClient.

L'implémentation par défaut pourOAuth2AccessTokenResponseClient estDefaultAuthorizationCodeTokenResponseClient.

Nous pouvons personnaliser la demande de jeton elle-même en fournissant unRequestEntityConverter personnalisé et nous pouvons même personnaliser la gestion de la réponse de jeton en personnalisantDefaultAuthorizationCodeTokenResponseClientRestOperations:

@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;
    }
}

Nous pouvons injecter nos propresOAuth2AccessTokenResponseClient usingtokenEndpoint().accessTokenResponseClient().

Pour personnaliser les paramètres de demande de jeton, nous implémenteronsCustomRequestEntityConverter. De même, pour personnaliser la réponse de jeton de gestion, nous implémenteronsCustomTokenResponseConverter.

Nous aborderons à la foisCustomRequestEntityConverter etCustomTokenResponseConverter dans les sections suivantes.

6. Paramètres supplémentaires de demande de jeton

Maintenant, nous allons voir comment ajouter des paramètres supplémentaires à notre demande de jeton en créant unConverter personnalisé:

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());
    }

}

NotreConverter transformeOAuth2AuthorizationCodeGrantRequest enRequestEntity. 

Nous avons utilisé le convertisseur par défautOAuth2AuthorizationCodeGrantRequestEntityConverter pour fournir la fonctionnalité de base et ajouté des paramètres supplémentaires au corps deRequestEntity.

7. Gestion de la réponse de jeton personnalisé

Nous allons maintenant personnaliser la gestion de la réponse du jeton.

Nous pouvons utiliser le convertisseur de réponse de jeton par défautOAuth2AccessTokenResponseHttpMessageConverter comme point de départ.

Nous allons implémenterCustomTokenResponseConverter pour gérer le paramètre“scope” différemment:

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();
    }

}

Le convertisseur de réponse de jeton transformeMap enOAuth2AccessTokenResponse.

Dans cet exemple, nous avons analysé le paramètre“scope” comme délimité par des virgules au lieu deString. délimité par des espaces

Passons en revue un autre exemple pratique en personnalisant la réponse du jeton en utilisant LinkedIn comme serveur d'autorisation.

7.1. Gestion des réponses aux jetons LinkedIn

Enfin, voyons comment gérer la réponse du jetonLinkedIn. Cela ne contient queaccess_token etexpires_in, mais nous avons également besoin detoken_type.

Nous pouvons simplement implémenter notre propre convertisseur de réponse de jeton et définirtoken_type manuellement:

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. Conclusion

Dans cet article, nous avons appris à personnaliser les demandes d'autorisation et de jeton OAuth2 en ajoutant ou en modifiant les paramètres de demande.

Le code source complet des exemples est disponibleover on GitHub.