最新の安定バージョンについては、Spring Security 6.3.1 を使用してください!

OIDC ログアウト

エンドユーザーがアプリケーションにログインできるようになったら、ログアウトする方法を検討することが重要です。

一般に、考慮すべき使用例は 3 つあります。

  1. ローカルログアウトのみを行いたい

  2. アプリケーションと、アプリケーションによって開始された OIDC プロバイダーの両方をログアウトしたいです

  3. アプリケーションと、OIDC プロバイダーによって開始された OIDC プロバイダーの両方をログアウトしたいです

ローカルログアウト

ローカルログアウトを実行するには、特別な OIDC 構成は必要ありません。Spring Security は、ローカルログアウトエンドポイントを自動的に起動します。これは、logout() DSL を通じて構成できます。

OpenID Connect 1.0 クライアント開始のログアウト

OpenID Connect セッション管理 1.0 を使用すると、クライアントを使用してプロバイダーのエンドユーザーをログアウトできます。利用可能な戦略の 1 つは RP からのログアウト (英語) です。

OpenID プロバイダーがセッション管理とディスカバリ (英語) の両方をサポートしている場合、クライアントは OpenID プロバイダーのディスカバリメタデータ (英語) から end_session_endpointURL を取得できます。これを行うには、次のように issuer-uri を使用して ClientRegistration を構成します。

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: okta-client-id
            client-secret: okta-client-secret
            ...
        provider:
          okta:
            issuer-uri: https://dev-1234.oktapreview.com

また、RP 開始ログアウトを実装する OidcClientInitiatedLogoutSuccessHandler を次のように構成する必要があります。

  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {

	@Autowired
	private ClientRegistrationRepository clientRegistrationRepository;

	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests(authorize -> authorize
				.anyRequest().authenticated()
			)
			.oauth2Login(withDefaults())
			.logout(logout -> logout
				.logoutSuccessHandler(oidcLogoutSuccessHandler())
			);
		return http.build();
	}

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

		// Sets the location that the End-User's User Agent will be redirected to
		// after the logout has been performed at the Provider
		oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");

		return oidcLogoutSuccessHandler;
	}
}
@Configuration
@EnableWebSecurity
class OAuth2LoginSecurityConfig {
    @Autowired
    private lateinit var clientRegistrationRepository: ClientRegistrationRepository

    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            oauth2Login { }
            logout {
                logoutSuccessHandler = oidcLogoutSuccessHandler()
            }
        }
        return http.build()
    }

    private fun oidcLogoutSuccessHandler(): LogoutSuccessHandler {
        val oidcLogoutSuccessHandler = OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository)

        // Sets the location that the End-User's User Agent will be redirected to
        // after the logout has been performed at the Provider
        oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}")
        return oidcLogoutSuccessHandler
    }
}

OidcClientInitiatedLogoutSuccessHandler は、{baseUrl} プレースホルダーをサポートします。使用する場合、app.example.org (英語) などのアプリケーションのベース URL は、リクエスト時に置き換えられます。

OpenID Connect 1.0 バックチャネルログアウト

OpenID Connect セッション管理 1.0 を使用すると、プロバイダーにクライアントへの API 呼び出しを行わせることで、クライアントでエンドユーザーをログアウトできます。これは OIDC バックチャネルログアウト (英語) と呼ばれます。

これを有効にするには、次のように DSL でバックチャネルログアウトエンドポイントを立ち上げます。

  • Java

  • Kotlin

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .anyRequest().authenticated()
        )
        .oauth2Login(withDefaults())
        .oidcLogout((logout) -> logout
            .backChannel(Customizer.withDefaults())
        );
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        authorizeRequests {
            authorize(anyRequest, authenticated)
        }
        oauth2Login { }
        oidcLogout {
            backChannel { }
        }
    }
    return http.build()
}

次に、次のように、Spring Security によって公開されたイベントをリッスンして古い OidcSessionInformation エントリを削除する方法が必要です。

  • Java

  • Kotlin

@Bean
public HttpSessionEventPublisher sessionEventPublisher() {
    return new HttpSessionEventPublisher();
}
@Bean
open fun sessionEventPublisher(): HttpSessionEventPublisher {
    return HttpSessionEventPublisher()
}

これにより、HttpSession#invalidate が呼び出されると、セッションもメモリから削除されます。

以上です!

これにより、OIDC プロバイダーがアプリケーション内のエンドユーザーの特定のセッションを無効にするようにリクエストできるエンドポイント /logout/connect/back-channel/{registrationId} が起動します。

oidcLogout を使用するには、oauth2Login も構成する必要があります。
oidcLogout では、バックチャネル経由で各セッションを正しくログアウトするために、セッション Cookie を JSESSIONID と呼ぶ必要があります。

バックチャネルログアウトアーキテクチャ

識別子が registrationId である ClientRegistration について考えてみましょう。

バックチャネルログアウトの全体的なフローは次のようになります。

  1. ログイン時に、Spring Security は、ID トークン、CSRF トークン、プロバイダーセッション ID (存在する場合) を、OidcSessionStrategy 実装内のアプリケーションのセッション ID に関連付けます。

  2. その後、ログアウト時に、OIDC プロバイダーは、sub (エンドユーザー) または sid (プロバイダーセッション ID) がログアウトすることを示すログアウトトークンを含む /logout/connect/back-channel/registrationId への API 呼び出しを行います。

  3. Spring Security は、トークンの署名とクレームを検証します。

  4. トークンに sid クレームが含まれている場合、そのプロバイダーセッションに関連するクライアントのセッションのみが終了します。

  5. それ以外の場合、トークンに sub クレームが含まれている場合は、そのエンドユーザーに対するすべてのクライアントのセッションが終了します。

Spring Security の OIDC サポートはマルチテナントであることに注意してください。これは、クライアントがログアウトトークンの aud クレームに一致するセッションのみを終了することを意味します。

OIDC プロバイダーのセッション戦略のカスタマイズ

デフォルトでは、Spring Security は、OIDC プロバイダーセッションとクライアントセッション間のすべてのリンクをメモリ内に保存します。

クラスター化されたアプリケーションなど、これをデータベースなどの別の場所に保存した方がよい状況は数多くあります。

これは、次のようにカスタム OidcSessionStrategy を構成することで実現できます。

  • Java

  • Kotlin

@Component
public final class MySpringDataOidcSessionStrategy implements OidcSessionStrategy {
    private final OidcProviderSessionRepository sessions;

    // ...

    @Override
    public void saveSessionInformation(OidcSessionInformation info) {
        this.sessions.save(info);
    }

    @Override
    public OidcSessionInformation(String clientSessionId) {
       return this.sessions.removeByClientSessionId(clientSessionId);
    }

    @Override
    public Iterable<OidcSessionInformation> removeSessionInformation(OidcLogoutToken token) {
        return token.getSessionId() != null ?
            this.sessions.removeBySessionIdAndIssuerAndAudience(...) :
            this.sessions.removeBySubjectAndIssuerAndAudience(...);
    }
}
@Component
class MySpringDataOidcSessionStrategy: OidcSessionStrategy {
    val sessions: OidcProviderSessionRepository

    // ...

    @Override
    fun saveSessionInformation(info: OidcSessionInformation) {
        this.sessions.save(info)
    }

    @Override
    fun removeSessionInformation(clientSessionId: String): OidcSessionInformation {
       return this.sessions.removeByClientSessionId(clientSessionId);
    }

    @Override
    fun removeSessionInformation(token: OidcLogoutToken): Iterable<OidcSessionInformation> {
        return token.getSessionId() != null ?
            this.sessions.removeBySessionIdAndIssuerAndAudience(...) :
            this.sessions.removeBySubjectAndIssuerAndAudience(...);
    }
}