Spring Security 5 - Accesso OAuth2

1. Panoramica

Spring Security 5 introduce una nuova classe OAuth2LoginConfigurer che possiamo utilizzare per configurare un server di autorizzazione esterno.

In questo articolo, esploreremo alcune delle varie opzioni di configurazione disponibili per l' elemento oauth2Login () .

2. Dipendenze di Maven

In un progetto Spring Boot, tutto ciò di cui abbiamo bisogno è aggiungere lo starter spring-boot-starter-oauth2-client :

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

In un progetto non Boot, oltre alle dipendenze standard Spring e Spring Security, dovremo anche aggiungere esplicitamente le dipendenze spring-security-oauth2-client e 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. Installazione client

In un progetto Spring Boot, tutto ciò che dobbiamo fare è aggiungere alcune proprietà standard per ogni client che vogliamo configurare.

Impostiamo il nostro progetto per il login con i clienti registrati con Google e Facebook come provider di autenticazione.

3.1. Ottenere le credenziali del cliente

Per ottenere le credenziali del client per l'autenticazione Google OAuth2, vai alla Console API di Google - sezione "Credenziali".

Qui creeremo le credenziali di tipo "OAuth2 Client ID" per la nostra applicazione web. Ciò si traduce in Google che imposta un ID client e un segreto per noi.

Dobbiamo anche configurare un URI di reindirizzamento autorizzato nella console di Google, che è il percorso a cui gli utenti verranno reindirizzati dopo aver effettuato correttamente l'accesso con Google.

Per impostazione predefinita, Spring Boot configura questo URI di reindirizzamento come / login / oauth2 / code / {registrationId}. Pertanto, per Google aggiungeremo l'URI:

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

Per ottenere le credenziali del client per l'autenticazione con Facebook, dobbiamo registrare un'applicazione sul sito Web Facebook per sviluppatori e impostare l'URI corrispondente come "URI di reindirizzamento OAuth valido":

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

3.3. Configurazione della protezione

Successivamente, dobbiamo aggiungere le credenziali del client al file application.properties . Le proprietà Spring Security hanno il prefisso "spring.security.oauth2.client.registration" seguito dal nome del client, quindi dal nome della proprietà del 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'aggiunta di queste proprietà per almeno un client abiliterà la classe Oauth2ClientAutoConfiguration che imposta tutti i bean necessari.

La configurazione automatica della sicurezza web equivale a definire un semplice elemento oauth2Login () :

@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .oauth2Login(); } }

Qui, possiamo vedere l' elemento oauth2Login () è usato in modo simile agli elementi httpBasic () e formLogin () già noti .

Ora, quando proviamo ad accedere a un URL protetto, l'applicazione visualizzerà una pagina di accesso generata automaticamente con due client:

3.4. Altri clienti

Nota che oltre a Google e Facebook, il progetto Spring Security contiene anche configurazioni predefinite per GitHub e Okta. Queste configurazioni predefinite forniscono tutte le informazioni necessarie per l'autenticazione, che è ciò che ci consente di inserire solo le credenziali del cliente.

Se vogliamo utilizzare un diverso provider di autenticazione non configurato in Spring Security, dovremo definire la configurazione completa, con informazioni come l'URI di autorizzazione e l'URI del token. Ecco uno sguardo alle configurazioni predefinite in Spring Security per avere un'idea delle proprietà necessarie.

4. Installazione in un progetto non di avvio

4.1. Creazione di un bean ClientRegistrationRepository

Se non stiamo lavorando con un'applicazione Spring Boot, dobbiamo definire un bean ClientRegistrationRepository che contiene una rappresentazione interna delle informazioni sul client di proprietà del server di autorizzazione:

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

Qui stiamo creando un InMemoryClientRegistrationRepository con un elenco di oggetti ClientRegistration .

4.2. Creazione di oggetti ClientRegistration

Vediamo il metodo getRegistration () che costruisce questi oggetti:

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

Qui, stiamo leggendo le credenziali del client da un file application.properties simile , quindi utilizzando l' enumerazione CommonOauth2Provider già definita in Spring Security per il resto delle proprietà del client per i client Google e Facebook.

Ogni istanza di ClientRegistration corrisponde a un client.

4.3. Registrazione di ClientRegistrationRepository

Infine, dobbiamo creare un bean OAuth2AuthorizedClientService basato sul bean ClientRegistrationRepository e registrarli entrambi con l' elemento 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")); }

Aggiungendo la proprietà name come attributo Model , possiamo visualizzarla nella vista loginSuccess come messaggio di benvenuto per l'utente:

Oltre al nome, l'userAttributes mappa contiene anche proprietà quali e-mail, FAMILY_NAME, immagine, locale.

7. Conclusione

In questo articolo, abbiamo visto come possiamo utilizzare l' elemento oauth2Login () in Spring Security per autenticarci con diversi provider come Google e Facebook. Abbiamo anche esaminato alcuni scenari comuni di personalizzazione di questo processo.

Il codice sorgente completo degli esempi può essere trovato su GitHub.