Spring Security und OpenID Connect

Spring Security und OpenID Connect

1. Überblick

In diesem kurzen Tutorial konzentrieren wir uns auf das Einrichten von OpenID Connect mit einer Spring Security OAuth2-Implementierung.

OpenID Connect ist eine einfache Identitätsschicht, die auf dem OAuth 2.0-Protokoll aufbaut.

Insbesondere erfahren Sie, wie Sie Benutzer mitOpenID Connect implementation fromGoogle authentifizieren.

2. Maven-Konfiguration

Zunächst müssen wir unserer Spring Boot-Anwendung die folgenden Abhängigkeiten hinzufügen:


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


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

3. Das ID-Token

Bevor wir uns mit den Implementierungsdetails befassen, werfen wir einen kurzen Blick darauf, wie OpenID funktioniert und wie wir damit interagieren.

An dieser Stelle ist es natürlich wichtig, bereits ein Verständnis für OAuth2 zu haben, da OpenID auf OAuth aufbaut.

Um die Identitätsfunktionalität nutzen zu können, verwenden wir zunächst einen neuen OAuth2-Bereich namensopenid. This will result in an extra field in our Access Token – “id_token“.

id_token ist ein JWT (JSON Web Token), das Identitätsinformationen über den Benutzer enthält, die vom Identitätsanbieter (in unserem Fall Google) signiert wurden.

Schließlich sind sowohlserver(Authorization Code)- als auchimplicit-Flüsse die am häufigsten verwendeten Methoden, umid_token zu erhalten. In unserem Beispiel werden wirserver flow verwenden.

3. OAuth2-Client-Konfiguration

Als Nächstes konfigurieren wir unseren OAuth2-Client wie folgt:

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

Und hier istapplication.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

Beachten Sie, dass:

  • Sie müssen zuerst OAuth 2.0-Anmeldeinformationen für Ihre Google-Webanwendung vonGoogle Developers Console erhalten.

  • Wir haben den Bereichopenid verwendet, umid_token zu erhalten.

  • Wir haben auch einen zusätzlichen Bereichemail verwendet, um Benutzer-E-Mails in die Identitätsinformationen vonid_tokenaufzunehmen.

  • Der Weiterleitungs-URIhttp://localhost:8081/google-login entspricht dem in unserer Google-Webanwendung verwendeten.

4. Benutzerdefinierter OpenID Connect-Filter

Jetzt müssen wir unsere eigenen benutzerdefiniertenOpenIdConnectFilter erstellen, um die Authentifizierung ausid_token zu extrahieren - wie folgt:

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

Und hier sind unsere einfachenOpenIdConnectUserDetails:

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

Beachten Sie, dass:

  • FedersicherheitJwtHelper zum Dekodieren vonid_token.

  • id_token enthält immer das Feld „sub”“, das eine eindeutige Kennung für den Benutzer darstellt.

  • id_token enthält auch das Feld "email", da wir unserer Anfrage den Gültigkeitsbereich vonemailhinzugefügt haben.

4.1. Überprüfen des ID-Tokens

Im obigen Beispiel haben wir diedecodeAndVerify()-Methode vonJwtHelper verwendet, um Informationen aus denid_token, zu extrahieren, aber auch um sie zu validieren.

Der erste Schritt hierfür besteht darin, zu überprüfen, ob es mit einem der im DokumentGoogle Discoveryangegebenen Zertifikate signiert wurde.

Diese ändern sich ungefähr einmal pro Tag, daher verwenden wir eine Dienstprogrammbibliothek namensjwks-rsa, um sie zu lesen:


    com.auth0
    jwks-rsa
    0.3.0

Fügen wir derapplication.properties-Datei die URL hinzu, die die Zertifikate enthält:

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

Jetzt können wir diese Eigenschaft lesen und das ObjektRSAVerifiererstellen:

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

Schließlich überprüfen wir auch die Ansprüche im decodierten ID-Token:

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

DieverifyClaims()-Methode überprüft, ob das ID-Token von Google ausgestellt wurde und nicht abgelaufen ist.

Weitere Informationen hierzu finden Sie inGoogle documentation.

5. Sicherheitskonfiguration

Lassen Sie uns als Nächstes unsere Sicherheitskonfiguration diskutieren:

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

Beachten Sie, dass:

  • Wir haben unsere benutzerdefiniertenOpenIdConnectFilter nachOAuth2ClientContextFilter hinzugefügt

  • Wir haben eine einfache Sicherheitskonfiguration verwendet, um Benutzer zu "/google-login" umzuleiten, damit sie von Google authentifiziert werden

6. Benutzersteuerung

Im Folgenden finden Sie einen einfachen Controller zum Testen unserer App:

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

Beispielantwort (nach Weiterleitung an Google zur Genehmigung der App-Berechtigungen):

Welcome, [email protected]

7. Beispiel für einen OpenID Connect-Prozess

Schauen wir uns zum Schluss ein Beispiel für einen OpenID Connect-Authentifizierungsprozess an.

Zuerst senden wir einAuthentication 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

Die Antwort (after user approval) ist eine Weiterleitung an:

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

Als nächstes werden wir diecode gegen ein Zugriffstoken undid_token austauschen:

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

Hier ist eine Beispielantwort:

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

Zum Schluss sehen die Informationen zu den tatsächlichenid_tokeno aus:

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

So können Sie sofort sehen, wie nützlich die Benutzerinformationen im Token für die Bereitstellung von Identitätsinformationen für unsere eigene Anwendung sind.

8. Fazit

In dieser kurzen Einführung haben wir gelernt, wie Benutzer mithilfe der OpenID Connect-Implementierung von Google authentifiziert werden.

Und wie immer finden Sie den Quellcodeover on GitHub.