Spring Security 5 - Connexion OAuth2

1. Vue d'ensemble

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

Dans cet article, nous explorerons certaines des différentes options de configuration disponibles pour l' élément oauth2Login () .

2. Dépendances de Maven

Dans un projet Spring Boot, tout ce dont nous avons besoin est d'ajouter le starter spring-boot-starter-oauth2-client :

 org.springframework.boot spring-boot-starter-oauth2-client 2.3.3.RELEASE 

Dans un projet non-Boot, en plus des dépendances standard de Spring et Spring Security, nous devrons également ajouter explicitement les dépendances spring-security-oauth2-client et spring-security-oauth2-jose :

 org.springframework.security spring-security-oauth2-client 5.3.4.RELEASE   org.springframework.security spring-security-oauth2-jose 5.3.4.RELEASE 

3. Configuration des clients

Dans un projet Spring Boot, tout ce que nous avons à faire est d'ajouter quelques propriétés standard pour chaque client que nous voulons configurer.

Configurons notre projet de connexion avec des 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, accédez à la console API Google - section «Informations d'identification».

Ici, nous allons créer des informations d'identification de type «OAuth2 Client ID» pour notre application Web. Cela entraîne la configuration par Google d'un identifiant client et d'un secret pour nous.

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 avoir réussi à se connecter avec Google.

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

//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 de Facebook pour les développeurs et configurer l'URI correspondant en tant qu '«URI de redirection OAuth valide»:

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

3.3. Configuration de la sécurité

Ensuite, nous devons ajouter les informations d'identification du client au fichier application.properties . Les propriétés Spring Security sont préfixées par «spring.security.oauth2.client.registration» suivi du nom du client, puis du nom de la propriété du client:

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=

L'ajout de ces propriétés pour au moins un client activera la classe Oauth2ClientAutoConfiguration qui configure tous les beans nécessaires.

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

@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ément oauth2Login () est utilisé d'une manière similaire aux éléments déjà connus httpBasic () et formLogin () .

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:

3.4. Autres clients

Notez qu'en plus de Google et Facebook, le projet Spring Security contient également des configurations par défaut pour GitHub et Okta. Ces configurations par défaut fournissent toutes les informations nécessaires pour 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. Voici un aperçu des configurations par défaut dans Spring Security pour avoir une idée des propriétés nécessaires.

4. Installation dans un projet non initialisé

4.1. Création d' un ClientRegistrationRepository Bean

Si nous ne travaillons pas avec une application Spring Boot, nous devrons définir un bean ClientRegistrationRepository 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 un InMemoryClientRegistrationRepository avec une liste d' objets ClientRegistration .

4.2. Bâtiment ClientRegistration objets

Voyons la méthode getRegistration () 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 fichier application.properties similaire , puis nous utilisons l' énumération CommonOauth2Provider déjà définie dans Spring Security pour le reste des propriétés du client pour les clients Google et Facebook.

Chaque instance ClientRegistration correspond à un client.

4.3. Enregistrement du ClientRegistrationRepository

Enfin, nous devons créer un bean OAuth2AuthorizedClientService basé sur le bean ClientRegistrationRepository et enregistrer les deux avec l' élément oauth2Login () :

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

As evidenced here, we can use the clientRegistrationRepository() method of oauth2Login() to register a custom registration repository.

We'll also have to define a custom login page, as it won't be automatically generated anymore. We'll see more information on this in the next section.

Let's continue with further customization of our login process.

5. Customizing oauth2Login()

There are several elements that the OAuth 2 process uses and that we can customize using oauth2Login() methods.

Note that all these elements have default configurations in Spring Boot and explicit configuration isn't required.

Let's see how we can customize these in our configuration.

5.1. Custom Login Page

Even though Spring Boot generates a default login page for us, we'll usually want to define our own customized page.

Let's start with configuring a new login URL for the oauth2Login() element by using theloginPage() method:

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

Here, we've set up our login URL to be /oauth_login.

Next, let's define a LoginController with a method that maps to this 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, which we'll obtain from the ClientRegistrationRepository bean:

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

Finally, we need to define our oauth_login.html page:

Login with:

Client

This is a simple HTML page that displays links to authenticate with each client.

After adding some styling to it, we can change the look of the login page:

5.2. Custom Authentication Success and Failure Behavior

We can control the post-authentication behavior by using different methods:

  • defaultSuccessUrl() and failureUrl() – to redirect the user to a given URL
  • successHandler() and failureHandler() – to execute custom logic following the authentication process

Let's see how we can set custom URL's to redirect the user to:

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

If the user visited a secured page before authenticating, they will be redirected to that page after logging in; otherwise, they will be redirected to /loginSuccess.

If we want the user to always be sent to the /loginSuccess URL regardless if they were on a secured page before or not, we can use the method defaultSuccessUrl(“/loginSuccess”, true).

To use a custom handler, we would have to create a class that implements the AuthenticationSuccessHandler or AuthenticationFailureHandler interfaces, override the inherited methods, then set the beans using the successHandler() and failureHandler() methods.

5.3. Custom Authorization Endpoint

The authorization endpoint is the endpoint that Spring Security uses to trigger an authorization request to the external server.

First, let's set new properties for the authorization endpoint:

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

Here, we've modified the baseUri to /oauth2/authorize-client instead of the default /oauth2/authorization. We're also explicitly setting an authorizationRequestRepository() bean that we have to define:

@Bean public AuthorizationRequestRepository authorizationRequestRepository() { return new HttpSessionOAuth2AuthorizationRequestRepository(); }

In our example, we've used the Spring-provided implementation for our bean, but we could also provide a custom one.

5.4. Custom Token Endpoint

The token endpoint processes access tokens.

Let's explicitly configure the tokenEndpoint()with the default response client implementation:

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

And here's the response client bean:

@Bean public OAuth2AccessTokenResponseClient accessTokenResponseClient() { return new NimbusAuthorizationCodeTokenResponseClient(); }

This configuration is the same as the default one and is using the Spring implementation which is based on exchanging an authorization code with the provider.

Of course, we could also substitute a custom response client.

5.5. Custom Redirection Endpoint

This is the endpoint to redirect to after authentication with the external provider.

Let's see how we can change the baseUri for the redirection endpoint:

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

The default URI is login/oauth2/code.

Note that if we change it, we also have to update the redirectUriTemplate property of each ClientRegistration and add the new URI as an authorized redirect URI for each client.

5.6. Custom User Information Endpoint

The user info endpoint is the location we can leverage to obtain user information.

We can customize this endpoint using the userInfoEndpoint() method. For this, we can use methods such as userService() and customUserType() to modify the way user information is retrieved.

6. Accessing User Information

A common task we may want to achieve is finding information about the logged-in user. For this, we can make a request to the user information endpoint.

First, we'll have to get the client corresponding to the current user token:

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

Next, we'll send a request to the client's user info endpoint and retrieve the userAttributes 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 comme attribut Model , nous pouvons l'afficher dans la vue loginSuccess en tant que message de bienvenue à l'utilisateur:

Outre le nom, la carte userAttributes contient également des propriétés telles que le courrier électronique, le nom de famille, l' image, les paramètres régionaux.

7. Conclusion

Dans cet article, nous avons vu comment nous pouvons utiliser l' élément oauth2Login () 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 est disponible à l'adresse over sur GitHub.