Spring Security et OpenID Connect

Spring Security et OpenID Connect

1. Vue d'ensemble

Dans ce rapide didacticiel, nous allons nous concentrer sur la configuration d'OpenID Connect avec une implémentation Spring Security OAuth2.

OpenID Connect est une couche d'identité simple construite au-dessus du protocole OAuth 2.0.

Et, plus précisément, nous allons apprendre à authentifier les utilisateurs à l'aide desOpenID Connect implementation fromGoogle.

2. Configuration Maven

Premièrement, nous devons ajouter les dépendances suivantes à notre application Spring Boot:


    org.springframework.boot
    spring-boot-starter-security


    org.springframework.security.oauth
    spring-security-oauth2

3. Le jeton d'identité

Avant de plonger dans les détails de la mise en œuvre, examinons rapidement comment OpenID fonctionne et comment nous allons interagir avec lui.

À ce stade, il est bien sûr important de déjà comprendre OAuth2, car OpenID est construit sur OAuth.

Tout d'abord, pour utiliser la fonctionnalité d'identité, nous utiliserons une nouvelle étendue OAuth2 appeléeopenid. This will result in an extra field in our Access Token – “id_token“.

Leid_token est un JWT (JSON Web Token) qui contient des informations d'identité sur l'utilisateur, signées par le fournisseur d'identité (dans notre cas Google).

Enfin, les fluxserver(Authorization Code) etimplicit sont les moyens les plus couramment utilisés pour obtenirid_token, dans notre exemple, nous utiliseronsserver flow.

3. Configuration du client OAuth2

Ensuite, configurons notre client OAuth2 - comme suit:

@Configuration
@EnableOAuth2Client
public class GoogleOpenIdConnectConfig {
    @Value("${google.clientId}")
    private String clientId;

    @Value("${google.clientSecret}")
    private String clientSecret;

    @Value("${google.accessTokenUri}")
    private String accessTokenUri;

    @Value("${google.userAuthorizationUri}")
    private String userAuthorizationUri;

    @Value("${google.redirectUri}")
    private String redirectUri;

    @Bean
    public OAuth2ProtectedResourceDetails googleOpenId() {
        AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
        details.setClientId(clientId);
        details.setClientSecret(clientSecret);
        details.setAccessTokenUri(accessTokenUri);
        details.setUserAuthorizationUri(userAuthorizationUri);
        details.setScope(Arrays.asList("openid", "email"));
        details.setPreEstablishedRedirectUri(redirectUri);
        details.setUseCurrentUri(false);
        return details;
    }

    @Bean
    public OAuth2RestTemplate googleOpenIdTemplate(OAuth2ClientContext clientContext) {
        return new OAuth2RestTemplate(googleOpenId(), clientContext);
    }
}

Et voiciapplication.properties:

google.clientId=
google.clientSecret=
google.accessTokenUri=https://www.googleapis.com/oauth2/v3/token
google.userAuthorizationUri=https://accounts.google.com/o/oauth2/auth
google.redirectUri=http://localhost:8081/google-login

Notez que:

  • Vous devez d'abord obtenir les informations d'identification OAuth 2.0 pour votre application Web Google auprès deGoogle Developers Console.

  • Nous avons utilisé la portéeopenid pour obtenirid_token.

  • nous avons également utilisé une portée supplémentaireemail pour inclure l'adresse e-mail de l'utilisateur dans les informations d'identité deid_token.

  • L'URI de redirectionhttp://localhost:8081/google-login est le même que celui utilisé dans notre application Web Google.

4. Filtre OpenID Connect personnalisé

Maintenant, nous devons créer nos propresOpenIdConnectFilter personnalisés pour extraire l'authentification deid_token - comme suit:

public class OpenIdConnectFilter extends AbstractAuthenticationProcessingFilter {

    public OpenIdConnectFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
        setAuthenticationManager(new NoopAuthenticationManager());
    }
    @Override
    public Authentication attemptAuthentication(
      HttpServletRequest request, HttpServletResponse response)
      throws AuthenticationException, IOException, ServletException {
        OAuth2AccessToken accessToken;
        try {
            accessToken = restTemplate.getAccessToken();
        } catch (OAuth2Exception e) {
            throw new BadCredentialsException("Could not obtain access token", e);
        }
        try {
            String idToken = accessToken.getAdditionalInformation().get("id_token").toString();
            String kid = JwtHelper.headers(idToken).get("kid");
            Jwt tokenDecoded = JwtHelper.decodeAndVerify(idToken, verifier(kid));
            Map authInfo = new ObjectMapper()
              .readValue(tokenDecoded.getClaims(), Map.class);
            verifyClaims(authInfo);
            OpenIdConnectUserDetails user = new OpenIdConnectUserDetails(authInfo, accessToken);
            return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
        } catch (InvalidTokenException e) {
            throw new BadCredentialsException("Could not obtain user details from token", e);
        }
    }
}

Et voici nos simplesOpenIdConnectUserDetails:

public class OpenIdConnectUserDetails implements UserDetails {
    private String userId;
    private String username;
    private OAuth2AccessToken token;

    public OpenIdConnectUserDetails(Map userInfo, OAuth2AccessToken token) {
        this.userId = userInfo.get("sub");
        this.username = userInfo.get("email");
        this.token = token;
    }
}

Notez que:

  • Spring SecurityJwtHelper pour décoderid_token.

  • id_token contient toujours le champ «sub”» qui est un identifiant unique pour l'utilisateur.

  • id_token contiendra également le champ «email» car nous avons ajouté la portéeemail à notre requête.

4.1. Vérification du jeton d'identification

Dans l'exemple ci-dessus, nous avons utilisé la méthodedecodeAndVerify() deJwtHelper pour extraire les informations desid_token, mais aussi pour les valider.

La première étape consiste à vérifier qu'il a été signé avec l'un des certificats spécifiés dans le documentGoogle Discovery.

Celles-ci changent environ une fois par jour, nous allons donc utiliser une bibliothèque utilitaire appeléejwks-rsa pour les lire:


    com.auth0
    jwks-rsa
    0.3.0

Ajoutons l'URL contenant les certificats au fichierapplication.properties:

google.jwkUrl=https://www.googleapis.com/oauth2/v2/certs

Nous pouvons maintenant lire cette propriété et construire l'objetRSAVerifier:

@Value("${google.jwkUrl}")
private String jwkUrl;

private RsaVerifier verifier(String kid) throws Exception {
    JwkProvider provider = new UrlJwkProvider(new URL(jwkUrl));
    Jwk jwk = provider.get(kid);
    return new RsaVerifier((RSAPublicKey) jwk.getPublicKey());
}

Enfin, nous vérifierons également les revendications dans le jeton d'identification décodé:

public void verifyClaims(Map claims) {
    int exp = (int) claims.get("exp");
    Date expireDate = new Date(exp * 1000L);
    Date now = new Date();
    if (expireDate.before(now) || !claims.get("iss").equals(issuer) ||
      !claims.get("aud").equals(clientId)) {
        throw new RuntimeException("Invalid claims");
    }
}

La méthodeverifyClaims() vérifie que le jeton d'identification a été émis par Google et qu'il n'est pas expiré.

Vous pouvez trouver plus d'informations à ce sujet dans lesGoogle documentation.

5. Configuration de sécurité

Ensuite, parlons de notre configuration de sécurité:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private OAuth2RestTemplate restTemplate;

    @Bean
    public OpenIdConnectFilter openIdConnectFilter() {
        OpenIdConnectFilter filter = new OpenIdConnectFilter("/google-login");
        filter.setRestTemplate(restTemplate);
        return filter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        .addFilterAfter(new OAuth2ClientContextFilter(),
          AbstractPreAuthenticatedProcessingFilter.class)
        .addFilterAfter(OpenIdConnectFilter(),
          OAuth2ClientContextFilter.class)
        .httpBasic()
        .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/google-login"))
        .and()
        .authorizeRequests()
        .anyRequest().authenticated();
    }
}

Notez que:

  • Nous avons ajouté nosOpenIdConnectFilter personnalisés aprèsOAuth2ClientContextFilter

  • Nous avons utilisé une configuration de sécurité simple pour rediriger les utilisateurs vers «/google-login» afin de s'authentifier par Google

6. Contrôleur utilisateur

Ensuite, voici un contrôleur simple pour tester notre application:

@Controller
public class HomeController {
    @RequestMapping("/")
    @ResponseBody
    public String home() {
        String username = SecurityContextHolder.getContext().getAuthentication().getName();
        return "Welcome, " + username;
    }
}

Exemple de réponse (après redirection vers Google pour approuver les autorisations d'application):

Welcome, [email protected]

7. Exemple de processus OpenID Connect

Enfin, examinons un exemple de processus d'authentification OpenID Connect.

Tout d'abord, nous allons envoyer unAuthentication Request:

https://accounts.google.com/o/oauth2/auth?
    client_id=sampleClientID
    response_type=code&
    scope=openid%20email&
    redirect_uri=http://localhost:8081/google-login&
    state=abc

La réponse (after user approval) est une redirection vers:

http://localhost:8081/google-login?state=abc&code=xyz

Ensuite, nous allons échanger lescode contre un jeton d'accès et desid_token:

POST https://www.googleapis.com/oauth2/v3/token
    code=xyz&
    client_id= sampleClientID&
    client_secret= sampleClientSecret&
    redirect_uri=http://localhost:8081/google-login&
    grant_type=authorization_code

Voici un exemple de réponse:

{
    "access_token": "SampleAccessToken",
    "id_token": "SampleIdToken",
    "token_type": "bearer",
    "expires_in": 3600,
    "refresh_token": "SampleRefreshToken"
}

Enfin, voici à quoi ressemblent les informations desid_token réels:

{
    "iss":"accounts.google.com",
    "at_hash":"AccessTokenHash",
    "sub":"12345678",
    "email_verified":true,
    "email":"[email protected]",
     ...
}

Ainsi, vous pouvez voir immédiatement à quel point les informations utilisateur contenues dans le jeton sont utiles pour fournir des informations d'identité à notre propre application.

8. Conclusion

Dans ce didacticiel d'introduction rapide, nous avons appris à authentifier les utilisateurs à l'aide de la mise en œuvre OpenID Connect de Google.

Et, comme toujours, vous pouvez trouver le code sourceover on GitHub.