この記事は新しいSpringSecurityOAuth2.0スタックに更新されていることに注意してください。 ただし、レガシースタックを使用したチュートリアルは引き続き利用できます。

1. 概要

In this tutorial, we’ll focus on setting up OpenID Connect (OIDC) with Spring Security.

この仕様のさまざまな側面を紹介し、次にSpringSecurityがOAuth2.0クライアントに実装するために提供するサポートを確認します。

2. クイックOpenIDConnectの概要

OpenID Connect は、OAuth2.0プロトコルの上に構築されたIDレイヤーです。

So, it’s really important to know OAuth 2.0 before diving into OIDC, especially the Authorization Code flow.

The OIDC specification suite is extensive. It includes core features and several other optional capabilities, presented in different groups. Here are the main ones:

  • Core – authentication and use of Claims to communicate End User information
  • Discovery – stipulate how a client can dynamically determine information about OpenID Providers
  • Dynamic Registration – dictate how a client can register with a provider
  • Session Management – define how to manage OIDC sessions

On top of this, the documents distinguish the OAuth 2.0 Authentication Servers that offer support for this spec, referring to them as OpenID Providers (OPs) and the OAuth 2.0 Clients that use OIDC as Relying Parties (RPs). We’ll be using this terminology in this article.

It’s also worth noting that a client can request the use of this extension by adding the openid scope in its Authorization Request.

Finally, for this tutorial, it’s useful to know that the OPs emit End User information as a JWT called an ID Token.

Now we’re ready to dive deeper into the OIDC world.

3. プロジェクトの設定

Before focusing on the actual development, we’ll have to register an OAuth 2.0 Client with our OpenID Provider.

この場合、OpenIDプロバイダーとしてGoogleを使用します。 これらの手順に従って、クライアントアプリケーションをプラットフォームに登録できます。 openidスコープがデフォルトで存在することに注意してください。

The Redirect URI we set up in this process is an endpoint in our service: http://localhost:8081/login/oauth2/code/google.

We should obtain a Client ID and a Client Secret from this process.

3.1. Maven構成

まず、これらの依存関係をプロジェクトのpomファイルに追加します。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
    <version>2.2.6.RELEASE</version>
</dependency>

The starter artifact aggregates all Spring Security Client-related dependencies, including

  • OAuth2.0ログインおよびクライアント機能のspring-security-oauth2-client依存関係
  • JWTサポート用のJOSEライブラリ

いつものように、 Maven Central検索エンジンを使用して、このアーティファクトの最新バージョンを見つけることができます。

4. SpringBootを使用した基本構成

First, we’ll start by configuring our application to use the client registration we just created with Google.

Using Spring Boot makes this very easy since all we have to do is define two application properties:

spring:
  security:
    oauth2:
      client:
        registration: 
          google: 
            client-id: <client-id>
            client-secret: <secret>

アプリケーションを起動して、エンドポイントにアクセスしてみましょう。 OAuth2.0クライアントのGoogleログインページにリダイレクトされることがわかります。

とてもシンプルに見えますが、ここでは内部でかなり多くのことが起こっています。 次に、Springセキュリティがこれをどのように実現するかを探ります。

以前は、WebClientとOAuth2サポートの投稿で、SpringSecurityがOAuth2.0認証サーバーとクライアントを処理する方法の内部を分析しました。

There we saw that we have to provide additional data, apart from the Client ID and the Client Secret, to configure a ClientRegistration instance successfully.

それで、これはどのように機能していますか?

Google is a well-known provider, and therefore the framework offers some predefined properties to make things easier.

CommonOAuth2Provider列挙型でこれらの構成を確認できます。

For Google, the enumerated type defines properties such as

  • 使用されるデフォルトのスコープ
  • 承認エンドポイント
  • トークンエンドポイント
  • UserInfoエンドポイント。これもOIDCコア仕様の一部です。

4.1. ユーザー情報へのアクセス

Spring Securityは、OIDCプロバイダーに登録されているユーザープリンシパル OidcUserentityの便利な表現を提供します。

基本的なOAuth2AuthenticatedPrincipalメソッドとは別に、このエンティティはいくつかの便利な機能を提供します。

  • Retrieve the ID Token value and the Claims it contains
  • Obtain the Claims provided by the UserInfo endpoint
  • Generate an aggregate of the two sets

コントローラでこのエンティティに簡単にアクセスできます。

@GetMapping("/oidc-principal")
public OidcUser getOidcUserPrincipal(
  @AuthenticationPrincipal OidcUser principal) {
    return principal;
}

Or we can use the SecurityContextHolder in a bean:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.getPrincipal() instanceof OidcUser) {
    OidcUser principal = ((OidcUser) authentication.getPrincipal());
    
    // ...
}

If we inspect the principal, we’ll see a lot of useful information here, such as the user’s name, email, profile picture and locale.

さらに、Springは、プロバイダーから受け取ったスコープに基づいて、「SCOPE _」というプレフィックスが付いた権限をプリンシパルに追加することに注意してください。たとえば、openidスコープは次のようになります。 SCOPE_openid付与された権限。

These authorities can be used to restrict access to certain resources:

@EnableWebSecurity
public class MappedAuthorities extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
          .authorizeRequests(authorizeRequests -> authorizeRequests
            .mvcMatchers("/my-endpoint")
              .hasAuthority("SCOPE_openid")
            .anyRequest().authenticated()
          );
    }
}

5. OIDCの動作

So far, we’ve learned how we can easily implement an OIDC Login solution using Spring Security.

We’ve seen the benefit it carries by delegating the user identification process to an OpenID Provider, which in turn supplies detailed useful information, even in a scalable manner.

But the truth is that we didn’t have to deal with any OIDC-specific aspect so far. これは、Springがほとんどの作業を行っていることを意味します。

So, let’s look at what’s going on behind the scenes to understand better how this specification is put into action and be able to get the most out of it.

5.1. ログインプロセス

これを明確に確認するために、 RestTemplate ログを有効にして、サービスが実行しているリクエストを確認しましょう。

logging:
  level:
    org.springframework.web.client.RestTemplate: DEBUG

ここでセキュリティで保護されたエンドポイントを呼び出すと、サービスが通常のOAuth2.0認証コードフローを実行していることがわかります。 That’s because, as we said, this specification is built on top of OAuth 2.0.

There are some differences.

First, depending on the provider we’re using and the scopes we’ve configured, we might see that the service is making a call to the UserInfo endpoint we mentioned at the beginning.

つまり、承認応答がプロファイル電子メールアドレス、または電話スコープの少なくとも1つを取得すると、フレームワークはUserInfoを呼び出します。追加情報を取得するためのエンドポイント。

Even though everything would indicate that Google should retrieve the profile and the email scope — since we’re using them in the Authorization Request — the OP retrieves their custom counterparts instead, https://www.googleapis.com/auth/userinfo.email and https://www.googleapis.com/auth/userinfo.profile, so Spring doesn’t call the endpoint.

これは、取得するすべての情報がIDトークンの一部であることを意味します。

独自のOidcUserServiceインスタンスを作成して提供することにより、この動作に適応できます。

@Configuration
public class OAuth2LoginSecurityConfig
  extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        Set<String> googleScopes = new HashSet<>();
        googleScopes.add(
          "https://www.googleapis.com/auth/userinfo.email");
        googleScopes.add(
          "https://www.googleapis.com/auth/userinfo.profile");

        OidcUserService googleUserService = new OidcUserService();
        googleUserService.setAccessibleScopes(googleScopes);

        http
          .authorizeRequests(authorizeRequests -> authorizeRequests
            .anyRequest().authenticated())
          .oauth2Login(oauthLogin -> oauthLogin
            .userInfoEndpoint()
              .oidcUserService(googleUserService));
    }
}

観察する2番目の違いは、JWKSetURIの呼び出しです。 JWSおよびJWKの投稿で説明したように、これはJWT形式のIDトークン署名を検証するために使用されます。

次に、IDトークンを詳細に分析します。

5.2. IDトークン

当然、OIDC仕様は、さまざまなシナリオをカバーし、それに適応します。 この場合、認証コードフローを使用しており、プロトコルは、アクセストークンとIDトークンの両方がトークンエンドポイント応答の一部として取得されることを示しています。

前に述べたように、 OidcUser エンティティには、IDトークンに含まれるクレームと、jwt.ioを使用して検査できる実際のJWT形式のトークンが含まれています。

これに加えて、Springは、仕様で定義されている標準のクレームをクリーンな方法で取得するための多くの便利なゲッターを提供しています。

IDトークンにはいくつかの必須のクレームが含まれていることがわかります。

  • The issuer identifier formatted as a URL (e.g., “https://accounts.google.com“)
  • A subject id, which is a reference of the End User contained by the issuer
  • The expiration time for the token
  • Time at which the token was issued
  • The audience, which will contain the OAuth 2.0 Client ID we’ve configured

It also contains many OIDC Standard Claims such as the ones we mentioned before (name, locale, picture, email).

As these are standard, we can expect many providers to retrieve at least some of these fields and therefore facilitate the development of simpler solutions.

5.3. クレームと範囲

ご想像のとおり、OPによって取得されるクレームは、構成したスコープ(またはSpring Security)に対応しています。

OIDCは、OIDCによって定義されたクレームを要求するために使用できるいくつかのスコープを定義します。

  • profile, which can be used to request default profile Claims (e.g., name, preferred_usernamepicture, etc.)
  • email emailおよびemail_verifiedクレームにアクセスする
  • 住所
  • phone, to request the phone_number and phone_number_verified Claims

Springはまだサポートしていませんが、仕様では、承認リクエストで指定することにより、単一のクレームをリクエストできます。

6. OIDCディスカバリーの春のサポート

はじめに説明したように、OIDCには、その主要な目的とは別に、さまざまな機能が含まれています。

このセクションで分析する機能と以下の機能は、OIDCではオプションです。 So, it’s important to understand that there might be OPs that don’t support them.

この仕様では、RPがOPを検出し、OPとの対話に必要な情報を取得するための検出メカニズムを定義しています。

一言で言えば、OPは標準メタデータのJSONドキュメントを提供します。 情報は、発行者の場所の既知のエンドポイント/。well-known/openid-configurationによって提供される必要があります。

Springは、 ClientRegistration を1つの単純なプロパティ、つまり発行者の場所で構成できるようにすることで、このメリットを享受しています。

しかし、これを明確に確認するために、例に飛び込んでみましょう。

カスタムClientRegistrationインスタンスを定義します。

spring:
  security:
    oauth2:
      client:
        registration: 
          custom-google: 
            client-id: <client-id>
            client-secret: <secret>
        provider:
          custom-google:
            issuer-uri: https://accounts.google.com

これで、アプリケーションを再起動し、ログをチェックして、アプリケーションが起動プロセスでopenid-configurationエンドポイントを呼び出していることを確認できます。

このエンドポイントを参照して、Googleが提供する情報を確認することもできます。

https://accounts.google.com/.well-known/openid-configuration

たとえば、サービスが使用する必要のある承認、トークン、UserInfoエンドポイント、およびサポートされているスコープを確認できます。

It’s especially relevant to note here that if the Discovery endpoint is not available when the service launches, our app won’t be able to complete the startup process successfully.

7. OpenIDConnectセッション管理

This specification complements the Core functionality by defining the following:

  • Different ways to monitor the End User’s login status at the OP on an ongoing basis so that the RP can log out an End User who has logged out of the OpenID Provider
  • The possibility of registering RP logout URIs with the OP as part of the Client registration, in order to be notified when the End User logs out of the OP
  • A mechanism to notify the OP that the End User has logged out of the site and might want to log out of the OP as well

当然、すべてのOPがこれらの項目のすべてをサポートしているわけではなく、これらのソリューションの一部は、ユーザーエージェントを介したフロントエンド実装でのみ実装できます。

このチュートリアルでは、リストの最後の項目であるRPによって開始されるログアウトに対してSpringによって提供される機能に焦点を当てます。

この時点で、アプリケーションにログインすると、通常はすべてのエンドポイントにアクセスできます。

If we log out (calling the /logout endpoint) and we make a request to a secured resource afterward, we’ll see that we can get the response without having to log in again.

However, this is actually not true. If we inspect the Network tab in the browser debug console, we’ll see that when we hit the secured endpoint the second time, we get redirected to the OP Authorization Endpoint. And since we’re still logged in there, the flow is completed transparently, ending up in the secured endpoint almost instantly.

もちろん、これは場合によっては望ましい動作ではない可能性があります。 これに対処するために、このOIDCメカニズムを実装する方法を見てみましょう。

7.1. OpenIDプロバイダーの構成

この場合、OpenIDプロバイダーとしてOktaインスタンスを構成して使用します。 We won’t go into details on how to create the instance, but we can follow the steps of this guide, keeping in mind that Spring Security’s default callback endpoint will be /login/oauth2/code/okta.

このアプリケーションでは、プロパティを使用してクライアント登録データを定義できます。

spring:
  security:
    oauth2:
      client:
        registration: 
          okta: 
            client-id: <client-id>
            client-secret: <secret>
        provider:
          okta:
            issuer-uri: https://dev-123.okta.com

OIDCは、OPログアウトエンドポイントをend_session_endpoint要素として検出ドキュメントで指定できることを示します。

7.2. LogoutSuccessHandler構成

次に、カスタマイズされた LogoutSuccessHandler インスタンスを提供して、HttpSecurityログアウトロジックを構成する必要があります。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
      .authorizeRequests(authorizeRequests -> authorizeRequests
        .mvcMatchers("/home").permitAll()
        .anyRequest().authenticated())
      .oauth2Login(oauthLogin -> oauthLogin.permitAll())
      .logout(logout -> logout
        .logoutSuccessHandler(oidcLogoutSuccessHandler()));
}

Now let’s see how we can create a LogoutSuccessHandler for this purpose using a special class provided by Spring Security, the OidcClientInitiatedLogoutSuccessHandler:

@Autowired
private ClientRegistrationRepository clientRegistrationRepository;

private LogoutSuccessHandler oidcLogoutSuccessHandler() {
    OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
      new OidcClientInitiatedLogoutSuccessHandler(
        this.clientRegistrationRepository);

    oidcLogoutSuccessHandler.setPostLogoutRedirectUri(
      URI.create("http://localhost:8081/home"));

    return oidcLogoutSuccessHandler;
}

したがって、OPクライアント構成パネルでこのURIを有効なログアウトリダイレクトURIとして設定する必要があります。

Clearly, the OP logout configuration is contained in the client registration setup since all we’re using to configure the handler is the ClientRegistrationRepository bean present in the context.

では、今何が起こるのでしょうか?

After we log in to our application, we can send a request to the /logout endpoint provided by Spring Security.

ブラウザのデバッグコンソールでネットワークログを確認すると、構成したリダイレクトURIに最終的にアクセスする前に、OPログアウトエンドポイントにリダイレクトされたことがわかります。

次回、認証が必要なアプリケーションのエンドポイントにアクセスするときは、アクセス許可を取得するために、強制的にOPプラットフォームに再度ログインする必要があります。

8. 結論

To summarize, in this article, we learned a lot about the solutions offered by OpenID Connect and how we can implement some of them using Spring Security.

As always, all the complete examples can be found over on GitHub.