Spring Security 5 - Connexion OAuth2

Spring Security 5 - Connexion à OAuth2

1. Vue d'ensemble

Spring Security 5 introduit une nouvelle classeOAuth2LoginConfigurer que nous pouvons utiliser pour configurer un serveur d'autorisation externe.

Dans cet article,we’ll explore some of the various configuration options available for the oauth2Login() element.

2. Dépendances Maven

En plus des dépendances standard de Spring et Spring Security, nous devrons également ajouter les dépendancesspring-security-oauth2-client etspring-security-oauth2-jose:


    org.springframework.security
    spring-security-oauth2-client


   org.springframework.security
   spring-security-oauth2-jose

Dans notre exemple, les dépendances sont gérées par le parent de démarrage Spring Boot, version2.x, qui correspond à la version5.x des artefacts Spring Security.

3. Configuration des clients

Dans un projet Spring Boot, il suffit d’ajouter quelques propriétés standard à chaque client à configurer.

Configurons notre projet de connexion avec les clients enregistrés avec Google et Facebook en tant que fournisseurs d'authentification.

3.1. Obtention des informations d'identification du client

Pour obtenir les informations d'identification du client pour l'authentification Google OAuth2, rendez-vous dans la sectionGoogle API Console "Informations d'identification".

Ici, nous allons créer des identifiants de type "OAuth2 Client ID" pour notre application Web. Cela nous amène Google à configurer un identifiant client et un secret.

Nous devons également configurer un URI de redirection autorisé dans la console Google, qui est le chemin vers lequel les utilisateurs seront redirigés après s'être connectés avec Google.

Par défaut, Spring Boot configure cet URI de redirection en tant que/login/oauth2/code/{registrationId}. Par conséquent, pour Google, nous ajouterons l'URI:

http://localhost:8081/login/oauth2/code/google

Pour obtenir les informations d'identification du client pour l'authentification avec Facebook, nous devons enregistrer une application sur le site Web deFacebook for Developers et configurer l'URI correspondant en tant qu '«URI de redirection OAuth valide»:

http://localhost:8081/login/oauth2/code/facebook

3.3. Configuration de sécurité

Ensuite, nous devons ajouter les informations d'identification du client dans le fichierapplication.properties. The Spring Security properties are prefixed with “spring.security.oauth2.client.registration” followed by the client name, then the name of the client property:

spring.security.oauth2.client.registration.google.client-id=
spring.security.oauth2.client.registration.google.client-secret=

spring.security.oauth2.client.registration.facebook.client-id=
spring.security.oauth2.client.registration.facebook.client-secret=

Adding these properties for at least one client will enable the Oauth2ClientAutoConfiguration class qui configure tous les beans nécessaires.

La configuration automatique de la sécurité Web équivaut à définir un simple élémentoauth2Login():

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
         .anyRequest().authenticated()
         .and()
         .oauth2Login();
    }
}

Ici, nous pouvons voir que l'élémentoauth2Login() est utilisé de manière similaire aux élémentshttpBasic() etformLogin() déjà connus.

Désormais, lorsque nous essayons d'accéder à une URL protégée, l'application affichera une page de connexion générée automatiquement avec deux clients:

image

3.4. Autres clients

Note that in addition to Google and Facebook, the Spring Security project also contains default configurations for GitHub and Okta. Ces configurations par défaut fournissent toutes les informations nécessaires à l'authentification, ce qui nous permet de n'entrer que les informations d'identification du client.

Si nous voulons utiliser un autre fournisseur d'authentification non configuré dans Spring Security, nous devons définir la configuration complète, avec des informations telles que l'URI d'autorisation et l'URI du jeton. Hereexamine les configurations par défaut de Spring Security pour avoir une idée des propriétés nécessaires.

4. Configuration dans un projet non initialisé

4.1. Créer un BeanClientRegistrationRepository

If we’re not working with a Spring Boot application, we’ll need to define a ClientRegistrationRepository bean qui contient une représentation interne des informations client appartenant au serveur d'autorisation:

@Configuration
@EnableWebSecurity
@PropertySource("classpath:application.properties")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private static List clients = Arrays.asList("google", "facebook");

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        List registrations = clients.stream()
          .map(c -> getRegistration(c))
          .filter(registration -> registration != null)
          .collect(Collectors.toList());

        return new InMemoryClientRegistrationRepository(registrations);
    }
}

Ici, nous créons unInMemoryClientRegistrationRepository avec une liste d’objetsClientRegistration.

4.2. Construction d'objetsClientRegistration

Voyons la méthodegetRegistration() qui construit ces objets:

private static String CLIENT_PROPERTY_KEY
  = "spring.security.oauth2.client.registration.";

@Autowired
private Environment env;

private ClientRegistration getRegistration(String client) {
    String clientId = env.getProperty(
      CLIENT_PROPERTY_KEY + client + ".client-id");

    if (clientId == null) {
        return null;
    }

    String clientSecret = env.getProperty(
      CLIENT_PROPERTY_KEY + client + ".client-secret");

    if (client.equals("google")) {
        return CommonOAuth2Provider.GOOGLE.getBuilder(client)
          .clientId(clientId).clientSecret(clientSecret).build();
    }
    if (client.equals("facebook")) {
        return CommonOAuth2Provider.FACEBOOK.getBuilder(client)
          .clientId(clientId).clientSecret(clientSecret).build();
    }
    return null;
}

Ici, nous lisons les informations d'identification du client à partir d'un fichierapplication.properties similaire, puis nous utilisons l'énumérationCommonOauth2Provider déjà définie dans Spring Security pour le reste des propriétés du client pour les clients Google et Facebook.

Chaque instanceClientRegistration correspond à un client.

4.3. Enregistrement desClientRegistrationRepository

Enfin, nous devons créer un beanOAuth2AuthorizedClientService basé sur le beanClientRegistrationRepository et enregistrer les deux avec l'élémentoauth2Login():

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().anyRequest().authenticated()
      .and()
      .oauth2Login()
      .clientRegistrationRepository(clientRegistrationRepository())
      .authorizedClientService(authorizedClientService());
}

@Bean
public OAuth2AuthorizedClientService authorizedClientService() {

    return new InMemoryOAuth2AuthorizedClientService(
      clientRegistrationRepository());
}

Comme démontré ici,we can use the clientRegistrationRepository() method of oauth2Login() to register a custom registration repository.

Nous devrons également définir une page de connexion personnalisée, car elle ne sera plus générée automatiquement. Nous verrons plus d'informations à ce sujet dans la section suivante.

Poursuivons la personnalisation de notre processus de connexion.

5. Personnalisation deoauth2Login()

Il existe plusieurs éléments que le processus OAuth 2 utilise et que nous pouvons personnaliser à l'aide des méthodesoauth2Login().

Notez que tous ces éléments ont des configurations par défaut dans Spring Boot et qu'une configuration explicite n'est pas requise.

Voyons comment nous pouvons les personnaliser dans notre configuration.

5.1. Page de connexion personnalisée

Même si Spring Boot génère une page de connexion par défaut pour nous, nous souhaitons généralement définir notre propre page personnalisée.

Commençons par configurer une nouvelle URL de connexion pour l'élémentoauth2Login() en utilisant le MéthodeloginPage():

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
      .antMatchers("/oauth_login")
      .permitAll()
      .anyRequest()
      .authenticated()
      .and()
      .oauth2Login()
      .loginPage("/oauth_login");
}

Ici, nous avons configuré notre URL de connexion sur/oauth_login.

Ensuite, définissons unLoginController avec une méthode qui correspond à cette URL:

@Controller
public class LoginController {

    private static String authorizationRequestBaseUri
      = "oauth2/authorization";
    Map oauth2AuthenticationUrls
      = new HashMap<>();

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    @GetMapping("/oauth_login")
    public String getLoginPage(Model model) {
        // ...

        return "oauth_login";
    }
}

This method has to send a map of the clients available and their authorization endpoints to the view, que nous obtiendrons à partir du beanClientRegistrationRepository:

public String getLoginPage(Model model) {
    Iterable clientRegistrations = null;
    ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository)
      .as(Iterable.class);
    if (type != ResolvableType.NONE &&
      ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) {
        clientRegistrations = (Iterable) clientRegistrationRepository;
    }

    clientRegistrations.forEach(registration ->
      oauth2AuthenticationUrls.put(registration.getClientName(),
      authorizationRequestBaseUri + "/" + registration.getRegistrationId()));
    model.addAttribute("urls", oauth2AuthenticationUrls);

    return "oauth_login";
}

Enfin, nous devons définir notre pageoauth_login.html:

Login with:

Client

Il s'agit d'une simple page HTML qui affiche des liens pour s'authentifier auprès de chaque client.

Après avoir ajouté du style, nous pouvons avoir une page de connexion beaucoup plus jolie:

image

5.2. Comportement de réussite et d'échec de l'authentification personnalisée

Nous pouvons contrôler le comportement post-authentification en utilisant différentes méthodes:

  • defaultSuccessUrl() etfailureUrl() - pour rediriger l'utilisateur vers une URL donnée

  • successHandler() etfailureHandler() - pour exécuter une logique personnalisée après le processus d'authentification

Voyons comment nous pouvons définir des URL personnalisées pour rediriger l'utilisateur vers:

.oauth2Login()
  .defaultSuccessUrl("/loginSuccess")
  .failureUrl("/loginFailure");

Si l'utilisateur a visité une page sécurisée avant de s'authentifier, il sera redirigé vers cette page après s'être connecté; sinon, ils seront redirigés vers/loginSuccess.

Si nous voulons que l'utilisateur soit toujours envoyé à l'URL de/loginSuccess, qu'il soit sur une page sécurisée auparavant ou non, nous pouvons utiliser la méthodedefaultSuccessUrl(“/loginSuccess”, true).

Pour utiliser un gestionnaire personnalisé, il faudrait créer une classe qui implémente les interfacesAuthenticationSuccessHandler ouAuthenticationFailureHandler, remplacer les méthodes héritées, puis définir les beans en utilisant les méthodessuccessHandler() et failureHandler () .

5.3. Point de terminaison d'autorisation personnalisée

Le noeud final d'autorisation est le noeud final utilisé par Spring Security pour déclencher une demande d'autorisation auprès du serveur externe.

Tout d'abord,let’s set new properties for the authorization endpoint:

.oauth2Login()
  .authorizationEndpoint()
  .baseUri("/oauth2/authorize-client")
  .authorizationRequestRepository(authorizationRequestRepository());

Ici, nous avons modifié lebaseUri en/oauth2/authorize-client au lieu du/oauth2/authorization. par défaut. Nous définissons également explicitement un beanauthorizationRequestRepository() que nous devons définir:

@Bean
public AuthorizationRequestRepository
  authorizationRequestRepository() {

    return new HttpSessionOAuth2AuthorizationRequestRepository();
}

Dans notre exemple, nous avons utilisé l'implémentation fournie par Spring pour notre bean, mais nous pourrions également en fournir une personnalisée.

5.4. Point de terminaison de jeton personnalisé

Le jetonendpoint traite les jetons d'accès.

Let’s explicitly configure the tokenEndpoint() avec l'implémentation du client de réponse par défaut:

.oauth2Login()
  .tokenEndpoint()
  .accessTokenResponseClient(accessTokenResponseClient());

Et voici le bean client de réponse:

@Bean
public OAuth2AccessTokenResponseClient
  accessTokenResponseClient() {

    return new NimbusAuthorizationCodeTokenResponseClient();
}

Cette configuration est identique à celle par défaut et utilise l'implémentation Spring qui repose sur l'échange d'un code d'autorisation avec le fournisseur.

Bien entendu, nous pourrions également remplacer un client de réponse personnalisé.

5.5. Point de terminaison de redirection personnalisé

Il s'agit du noeud final vers lequel rediriger après l'authentification auprès du fournisseur externe.

Voyons comment nous pouvons modifier lesbaseUri pour le point de terminaison de redirection:

.oauth2Login()
  .redirectionEndpoint()
  .baseUri("/oauth2/redirect")

L'URI par défaut estlogin/oauth2/code.

Notez que si nous le changeons, nous devons également mettre à jour la propriétéredirectUriTemplate de chaqueClientRegistration et ajouter le nouvel URI en tant qu'URI de redirection autorisé pour chaque client.

5.6. Point de terminaison des informations utilisateur personnalisées

Le point de terminaison info utilisateur est l'emplacement que nous pouvons utiliser pour obtenir des informations sur l'utilisateur.

We can customize this endpoint using the userInfoEndpoint() method. Pour cela, nous pouvons utiliser des méthodes telles queuserService() etcustomUserType() pour modifier la façon dont les informations utilisateur sont récupérées.

6. Accès aux informations utilisateur

Une tâche courante que nous souhaitons peut-être réaliser consiste à rechercher des informations sur l'utilisateur connecté. Pour cela,we can make a request to the user information endpoint.

Tout d'abord, nous devrons obtenir le client correspondant au jeton utilisateur actuel:

@Autowired
private OAuth2AuthorizedClientService authorizedClientService;

@GetMapping("/loginSuccess")
public String getLoginInfo(Model model, OAuth2AuthenticationToken authentication) {
    OAuth2AuthorizedClient client = authorizedClientService
      .loadAuthorizedClient(
        authentication.getAuthorizedClientRegistrationId(),
          authentication.getName());
    //...
    return "loginSuccess";
}

Ensuite, nous enverrons une demande au point de terminaison des informations utilisateur du client et récupérerons lesuserAttributes Map:

String userInfoEndpointUri = client.getClientRegistration()
  .getProviderDetails().getUserInfoEndpoint().getUri();

if (!StringUtils.isEmpty(userInfoEndpointUri)) {
    RestTemplate restTemplate = new RestTemplate();
    HttpHeaders headers = new HttpHeaders();
    headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + client.getAccessToken()
      .getTokenValue());
    HttpEntity entity = new HttpEntity("", headers);
    ResponseEntity response = restTemplate
      .exchange(userInfoEndpointUri, HttpMethod.GET, entity, Map.class);
    Map userAttributes = response.getBody();
    model.addAttribute("name", userAttributes.get("name"));
}

En ajoutant la propriéténame en tant qu'attributModel, nous pouvons l'afficher dans la vueloginSuccess en tant que message de bienvenue à l'utilisateur:

image

Outre lesname,, lesuserAttributes Map contiennent également des propriétés telles queemail, family_name,picture, locale.

7. Conclusion

Dans cet article, nous avons vu comment nous pouvons utiliser l'élémentoauth2Login() dans Spring Security pour nous authentifier auprès de différents fournisseurs tels que Google et Facebook. Nous avons également passé en revue certains scénarios courants de personnalisation de ce processus.

Le code source complet des exemples peut être trouvéover on GitHub.