Spring Security : Rediriger des requêtes d’accès de type Basic Auth vers un fournisseur utilisant le protocole OAuth 2.0
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êteAuthorization
de typeBasic
, sinon c’est la configuration par défaut qui prend le pas.extractHttpBasicCredentials()
: ce filtre s’occupe d’extraire du token leusername
et lepassword
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 classeOAuth2ClientAuthenticationProcessingFilter
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
- Documentation de l’auto-configuration OAuth 2 sous Spring Boot : https://docs.spring.io/spring-security-oauth2-boot/docs/current/reference/html5/
- Utilisation d’authentification multiple avec Spring Security : https://www.baeldung.com/spring-security-multiple-auth-providers
- Comment rediriger un utilisateur sur son URL d’origine : https://www.baeldung.com/spring-security-redirect-login
- java.lang.IllegalStateException Protecting REST API with OAuth2: Error creating bean with name ‘scopedTarget.oauth2ClientContext’: Scope ‘session’ is not active