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 開始ログアウトを実装する OidcClientInitiatedServerLogoutSuccessHandler を次のように構成する必要があります。

  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {

	@Autowired
	private ReactiveClientRegistrationRepository clientRegistrationRepository;

	@Bean
	public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception {
		http
			.authorizeExchange((authorize) -> authorize
				.anyExchange().authenticated()
			)
			.oauth2Login(withDefaults())
			.logout((logout) -> logout
				.logoutSuccessHandler(oidcLogoutSuccessHandler())
			);
		return http.build();
	}

	private ServerLogoutSuccessHandler oidcLogoutSuccessHandler() {
		OidcClientInitiatedServerLogoutSuccessHandler oidcLogoutSuccessHandler =
				new OidcClientInitiatedServerLogoutSuccessHandler(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
@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {
    @Autowired
    private lateinit var clientRegistrationRepository: ReactiveClientRegistrationRepository

    @Bean
    open fun filterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http {
            authorizeExchange {
                authorize(anyExchange, authenticated)
            }
            oauth2Login { }
            logout {
                logoutSuccessHandler = oidcLogoutSuccessHandler()
            }
        }
        return http.build()
    }

    private fun oidcLogoutSuccessHandler(): ServerLogoutSuccessHandler {
        val oidcLogoutSuccessHandler = OidcClientInitiatedServerLogoutSuccessHandler(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
    }
}

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

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

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

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

  • Java

  • Kotlin

@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception {
    http
        .authorizeExchange((authorize) -> authorize
            .anyExchange().authenticated()
        )
        .oauth2Login(withDefaults())
        .oidcLogout((logout) -> logout
            .backChannel(Customizer.withDefaults())
        );
    return http.build();
}
@Bean
open fun filterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    http {
        authorizeExchange {
            authorize(anyExchange, authenticated)
        }
        oauth2Login { }
        oidcLogout {
            backChannel { }
        }
    }
    return http.build()
}

以上です!

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

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

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

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

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

  1. ログイン時に、Spring Security は、ID トークン、CSRF トークン、プロバイダーセッション ID (存在する場合) を、ReactiveOidcSessionRegistry 実装内のアプリケーションのセッション 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 プロバイダーセッションとクライアントセッション間のすべてのリンクをメモリ内に保存します。

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

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

  • Java

  • Kotlin

@Component
public final class MySpringDataOidcSessionRegistry implements ReactiveOidcSessionRegistry {
    private final OidcProviderSessionRepository sessions;

    // ...

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

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

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

    // ...

    @Override
    fun saveSessionInformation(info: OidcSessionInformation): Mono<Void> {
        return this.sessions.save(info)
    }

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

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