Nous allons étudier comment il est possible sous Spring Security d’avoir plusieurs types d’authentification (Basic Auth, OAuth 2.0) mais utilisant un même fournisseur d’accès pour valider l’utilisateur.

Postulat

Vous disposez d’une API Java s’appuyant sur la configuration d’un serveur de ressources de Spring Security et utilisant un ResourceServerConfigurerAdapter pour filtrer les appels entrant. Une configuration par défaut ressemble généralement à ça :

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private ResourceServerProperties resourceServerProperties;

    @Override
    public void configure(final ResourceServerSecurityConfigurer resources) {
        resources.resourceId(resourceServerProperties.getResourceId());
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
            .anyRequest().authenticated();
    }
}

Cette configuration demande à ce que les appels entrant dans votre API doivent disposer d’un token d’accès ou JWT dans leur entête pour ensuite être filtrés et émis vers votre fournisseur d’accès basé sur la protocole OAuth 2.0. Pour cela vous avez probablement injecté les dépendances suivantes dans votre pom.xml :

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.6.RELEASE</version>
</dependency>

Spring met alors en place un certains nombre de filtre (notamment OAuth2ClientAuthenticationProcessingFilter) qui s’appuient sur les fichiers d’environnement de votre API pour configurer le fournisseur d’accès. Pour un serveur de ressources, la configuration minimale est la suivante :

security:
  oauth2:
    client:
      clientId: <your-client-id>
      clientSecret: <your-client-secret>
    resource:
      id: <authentication-provider-id>
      token-info-uri: <authentication-provider-check-token-info-uri>
      user-info-uri: <authentication-provider-check-user-info-uri>

Maintenant, on souhaite avoir des chemins de l’API accessibles directement via un appel de type Basic Auth et portant les identifiants d’un utilisateur (un compte de service par exemple). Généralement, on retrouve souvent des exemples impliquant la mise en place de plusieurs fichiers de configuration mais utilisant des fournisseurs d’accès différent. Ce que l’on souhaite, c’est rediriger notre appel vers le fournisseur d’accès utilisant le protocole OAuth 2.0 pour valider l’utilisateur.

Interception et redirection

Nous devons mettre en place une configuration de sécurité qui doit dans un premier temps vérifier si l’API est appelée avec une entête contenant un token de type Basic et effectuer le cas échéant une redirection vers le fournisseur d’accès. Si l’utilisateur est authentifié, la configuration doit pouvoir rediriger l’appel vers la route initiale avec les informations d’identification tel que le token d’accès récupéré auprès du serveur d’authentification. Dans le cas contraire il faut laisser la configuration par défaut s’exécuter.

Comme vue précédemment, Spring permet la mise en place de configuration multiple. Il suffit de les marquer de l’annotation @Order() pour indiquer l’ordre d’exécution de chaque configuration. Notre configuration initiale ResourceServerSecurityConfigurer doit donc être exécuté en dernier.

La configuration d’interception et de redirection peut être de la forme suivante :

@Configuration
@EnableWebSecurity
@Order(2)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private ResourceOwnerPasswordResourceDetails resourceDetails = new ResourceOwnerPasswordResourceDetails();

    @Autowired
    OAuth2ClientContext oauth2ClientContext;

    @Autowired
    private Environment env;

    @Autowired
    ResourceServerProperties resourceServerProperties;
    private OAuth2ClientAuthenticationProcessingFilter oAuth2Filter;

    @Bean
    public ResourceOwnerPasswordResourceDetails resourceOwnerPasswordResourceDetails() {
    resourceDetails.setAccessTokenUri(env.getProperty("security.oauth2.client.access-token-uri"));
        resourceDetails.setClientId(resourceServerProperties.getClientId());
        resourceDetails.setClientSecret(resourceServerProperties.getClientSecret());

        return resourceDetails;
    }

    /**
     * Allowed usage of multi-threading in the same HTTP request context.
     * Avoid the IllegalStateException: No thread-bound request found
     * @return a RequestContextListener object.
     */
    @Bean
    public RequestContextListener requestContextListener() {
        return new RequestContextListener();
    }

    /**
     * Configure a user authentication through OAuth provider.
     *
     * @param defaultFilterProcessesUrl URL to matches by the filter
     * @return the OAuth2 filter
     */
    private OAuth2ClientAuthenticationProcessingFilter oAuth2UserRedirectFilter(String defaultFilterProcessesUrl) {
        // Creating the filter for the matching url
        OAuth2ClientAuthenticationProcessingFilter oAuth2Filter = new OAuth2ClientAuthenticationProcessingFilter(defaultFilterProcessesUrl);

        // Creating the rest template for getting connected with OAuth provider.
        // The configuration parameters will be inject while creating the bean.
        OAuth2RestTemplate oAuth2RestTemplate =
            new OAuth2RestTemplate(resourceOwnerPasswordResourceDetails(), oauth2ClientContext);
        oAuth2Filter.setRestTemplate(oAuth2RestTemplate);

        // setting the token service. It will help for getting the token and
        // user details from the OAuth Provider
        oAuth2Filter.setTokenServices(new UserInfoTokenServices(resourceServerProperties.getUserInfoUri(),
            resourceServerProperties.getClientId()));

        // Redirect to the referer URI with token and user details from the OAuth Provider
        // on successful authentication.
        oAuth2Filter.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                request.getRequestDispatcher(request.getRequestURI()).forward(request, response);
            }
        });

        return oAuth2Filter;
    }

    /**
     * Extract the basic auth credentials (username and password) and configure the resource
     * detail object with it.
     *
     * @return the filter
     */
    private OncePerRequestFilter extractHttpBasicCredentials() {
    OncePerRequestFilter extractFilter = new OncePerRequestFilter() {
            @Override
            protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
                BasicAuthenticationConverter basicAuthConverter = new BasicAuthenticationConverter();
                UsernamePasswordAuthenticationToken basicToken = basicAuthConverter.convert(httpServletRequest);
                resourceDetails.setUsername(basicToken.getPrincipal().toString());
                resourceDetails.setPassword(basicToken.getCredentials().toString());
                filterChain.doFilter(httpServletRequest, httpServletResponse);
            }
        };

        return extractFilter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        http
            .csrf().disable()
            .requestMatcher(request -> {
                String auth = request.getHeader(HttpHeaders.AUTHORIZATION);
                return (auth != null && auth.startsWith("Basic"));
            })
            .addFilterBefore(oAuth2UserRedirectFilter("/api/**"), BasicAuthenticationFilter.class)
            .addFilterBefore(extractHttpBasicCredentials(), OAuth2ClientAuthenticationProcessingFilter.class)
            .httpBasic().disable()
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        // @formatter:on
    }
}

Etudions le déroulement de cette configuration :

  • requestMatcher() : cette configuration ne s’applique que si la requête contient une entête Authorization de type Basic, sinon c’est la configuration par défaut qui prend le pas.
  • extractHttpBasicCredentials() : ce filtre s’occupe d’extraire du token le username et le password de l’utilisateur puis d’alimenter un objet ResourceOwnerPasswordResourceDetails qui sert à paramétrer le prochain filtre.
  • oAuth2UserRedirectFilter("<you-api-route-to-filter>") : ce filtre qui s’appui sur la classe OAuth2ClientAuthenticationProcessingFilter s’occupe de gérer toute la partie d’authentification auprès du fournisseur d’accès OAuth 2.0. Il ajoute également un certains nombres d’attributs à la requête initiale dont le token d’accès éventuellement récupéré. En cas de succès d’authentification, la chaîne de filtre est rompue et la requête est alors transmise (forward) à la route initiale :
    request.getRequestDispatcher(request.getRequestURI()).forward(request, response);
  • Toute autre requête sera rejetée si elle n’est pas authentifiée.

Le bean RequestContextListener est là uniquement dans le cas où les routes de vos API effectuent des traitements asynchrones. A cause du forward, le contexte HTTP ne supporte pas l’utilisation de thread hors contexte et vous risquez d’enclencher une exception du type java.lang.IllegalStateException, sauf si votre contexte dispose d’un listener adéquate.

N’oubliez pas d’ajouter un ordonnancement à la configuration initiale de manière à l’exécuter après notre WebSecurityConfigurerAdapter :

@Configuration
@EnableResourceServer
@Order(3)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
  …

AccessTokenUri

Vous l’avez sans doute remarqué mais l’un des paramètres utilisé dans la classe ResourceOwnerPasswordResourceDetails n’est pas desservi par resourceServerProperties, il s’agit du paramètre AccessTokenUri.

Dans notre exemple, vous devez l’ajouter manuellement dans le fichier d’environnement application.yml de cette manière :

security:
   oauth2:
     client:
       clientId: <your-client-id>
       clientSecret: <your-client-secret>
       access-token-uri: <authentication-provider-get-access-token-uri>

Puis vous pouvez utiliser votre paramètre customisé en injectant une instance de la classe Environment

@Autowired
private Environment env;

et en appelant le paramètre de cette façon :

env.getProperty("security.oauth2.client.access-token-uri")

Il est également possible de passer par une propriété privée et de l’annoter comme ci-dessous :

@Value("${security.oauth2.client.access-token-uri}")
private String accessTokenUri;

Cette URL corresponds à la route permettant l’acquisition d’un token d’accès via le flux d’obtention par mot de passe auprès d’un fournisseur d’accès. Dans Spring Security, la valeur par défaut est /oauth/token.

Conclusion

Dans cet article nous avons vu :

  • comment ordonnancer plusieurs fichiers de configuration à l’aide de l’annotation @Order().
  • comment paramétrer une configuration de sécurité permettant d’extraire les informations de connexion d’un token de type Basic pour les utiliser dans un flux d’authentification OAuth 2.0.
  • comment rediriger la requête après une authentification réussi vers l’URI d’origine.
  • comment ajouter et utiliser un paramètre customisé depuis un fichier d’environnement.

Ressources


0 commentaire

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

Spring Security : Rediriger des requêtes d’accès de type Basic…

par Cyrille P. temps de lecture : 6 min
0