OIDC 登出

一旦最終使用者能夠登入您的應用程式,考慮他們將如何登出是很重要的。

一般來說,您需要考慮三個使用案例

  1. 我只想執行本地登出

  2. 我想登出我的應用程式和 OIDC Provider,由我的應用程式啟動

  3. 我想登出我的應用程式和 OIDC Provider,由 OIDC Provider 啟動

本地登出

若要執行本地登出,不需要特殊的 OIDC 組態。Spring Security 會自動啟動本地登出端點,您可以透過 logout() DSL 進行組態

OpenID Connect 1.0 Client 啟動的登出

OpenID Connect Session Management 1.0 允許透過 Client 登出 Provider 上的最終使用者。其中一種可用的策略是 RP 啟動的登出

如果 OpenID Provider 同時支援 Session Management 和 Discovery,client 可以從 OpenID Provider 的 Discovery Metadata 取得 end_session_endpoint URL。您可以透過使用 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} 佔位符。如果使用,應用程式的基本 URL,例如 app.example.org,會在請求時取代它。

OpenID Connect 1.0 Back-Channel 登出

OpenID Connect Session Management 1.0 允許透過 Provider 對 Client 進行 API 呼叫,以登出 Client 上的最終使用者。這被稱為 OIDC Back-Channel 登出

若要啟用此功能,您可以在 DSL 中啟動 Back-Channel 登出端點,如下所示

  • 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()
}

就是這樣!

這將啟動端點 /logout/connect/back-channel/{registrationId},OIDC Provider 可以請求該端點以使最終使用者在您的應用程式中的指定 session 失效。

oidcLogout 需要也組態 oauth2Login
oidcLogout 需要 session Cookie 稱為 JSESSIONID,以便透過 backchannel 正確登出每個 session。

Back-Channel 登出架構

考量一個 identifier 為 registrationIdClientRegistration

Back-Channel 登出的整體流程如下

  1. 在登入時,Spring Security 會將 ID Token、CSRF Token 和 Provider Session ID (如果有的話) 與您的應用程式 session id 關聯在其 ReactiveOidcSessionRegistry 實作中。

  2. 然後在登出時,您的 OIDC Provider 會對 /logout/connect/back-channel/registrationId 進行 API 呼叫,其中包含一個 Logout Token,指示要登出的 sub (最終使用者) 或 sid (Provider Session ID)。

  3. Spring Security 會驗證 token 的簽章和宣告。

  4. 如果 token 包含 sid 宣告,則只會終止與該 provider session 相關聯的 Client session。

  5. 否則,如果 token 包含 sub 宣告,則會終止該最終使用者的所有 Client session。

請記住,Spring Security 的 OIDC 支援是多租戶的。這表示它只會終止 Client 與 Logout Token 中的 aud 宣告相符的 session。

自訂 OIDC Provider Session Registry

預設情況下,Spring Security 會在記憶體中儲存 OIDC Provider session 和 Client session 之間的所有連結。

在許多情況下,例如叢集應用程式,最好將其儲存在不同的位置,例如資料庫。

您可以透過組態自訂 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(...);
    }
}