OIDC 退出

一旦終端使用者能夠登入到您的應用程式,考慮他們將如何退出就非常重要。

一般來說,有三種用例需要您考慮

  1. 我只想執行本地退出

  2. 我想同時退出我的應用程式和 OIDC Provider,由我的應用程式發起

  3. 我想同時退出我的應用程式和 OIDC Provider,由 OIDC Provider 發起

本地退出

要執行本地退出,不需要特殊的 OIDC 配置。Spring Security 會自動啟動一個本地退出端點,您可以透過 logout() DSL 進行配置

OpenID Connect 1.0 客戶端發起退出

OpenID Connect 會話管理 1.0 允許客戶端在 Provider 端退出終端使用者。其中一種可用的策略是 RP-發起退出(RP-Initiated Logout)

如果 OpenID Provider 同時支援會話管理和 發現(Discovery),客戶端可以從 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 後端通道退出

OpenID Connect 會話管理 1.0 允許 Provider 透過向 Client 發起 API 呼叫來退出 Client 端的終端使用者。這被稱為 OIDC 後端通道退出(OIDC Back-Channel Logout)

要啟用此功能,您可以在 DSL 中配置後端通道退出端點,如下所示

  • Java

  • Kotlin

@Bean
OidcBackChannelServerLogoutHandler oidcLogoutHandler() {
	return new OidcBackChannelServerLogoutHandler();
}

@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
fun oidcLogoutHandler(): OidcBackChannelLogoutHandler {
    return OidcBackChannelLogoutHandler()
}

@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 可以請求此端點來使您應用程式中給定終端使用者的會話失效。

oidcLogout 要求同時也配置 oauth2Login
oidcLogout 要求會話 cookie 被命名為 JSESSIONID,以便通過後端通道正確地退出每個會話。

後端通道退出架構

考慮一個識別符號為 registrationIdClientRegistration

後端通道退出的總體流程如下

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

  2. 然後在退出時,您的 OIDC Provider 會向 /logout/connect/back-channel/registrationId 發起 API 呼叫,其中包含一個 Logout Token,該令牌指示要退出的 sub(終端使用者)或 sid(Provider 會話 ID)。

  3. Spring Security 會驗證令牌的簽名和宣告(claims)。

  4. 如果令牌包含 sid 宣告,則只有與該 provider 會話相關的 Client 會話會被終止。

  5. 否則,如果令牌包含 sub 宣告,則該終端使用者的所有 Client 會話都會被終止。

請記住,Spring Security 的 OIDC 支援是多租戶的。這意味著它只會終止 Logout Token 中 aud 宣告與之匹配的 Client 的會話。

自定義會話退出端點

當釋出 OidcBackChannelServerLogoutHandler 後,會話退出端點是 {baseUrl}/logout/connect/back-channel/{registrationId}

如果 OidcBackChannelServerLogoutHandler 未配置,則 URL 為 {baseUrl}/logout/connect/back-channel/{registrationId},但不推薦這樣做,因為它需要傳遞一個 CSRF token,根據您應用程式使用的倉庫型別,這可能會很有挑戰性。

如果您需要自定義端點,可以按如下方式提供 URL

  • Java

  • Kotlin

http
    // ...
    .oidcLogout((oidc) -> oidc
        .backChannel((backChannel) -> backChannel
            .logoutUri("https://:9000/logout/connect/back-channel/+{registrationId}+")
        )
    );
http {
    oidcLogout {
        backChannel {
            logoutUri = "https://:9000/logout/connect/back-channel/+{registrationId}+"
        }
    }
}

預設情況下,會話退出端點使用 JSESSIONID cookie 來關聯會話與相應的 OidcSessionInformation

然而,Spring Session 中的預設 cookie 名稱是 SESSION

您可以在 DSL 中像這樣配置 Spring Session 的 cookie 名稱

  • Java

  • Kotlin

@Bean
OidcBackChannelServerLogoutHandler oidcLogoutHandler(ReactiveOidcSessionRegistry sessionRegistry) {
    OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(sessionRegistry);
    logoutHandler.setSessionCookieName("SESSION");
    return logoutHandler;
}
@Bean
open fun oidcLogoutHandler(val sessionRegistry: ReactiveOidcSessionRegistry): OidcBackChannelServerLogoutHandler {
    val logoutHandler = OidcBackChannelServerLogoutHandler(sessionRegistry)
    logoutHandler.setSessionCookieName("SESSION")
    return logoutHandler
}

自定義 OIDC Provider 會話登錄檔

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

在許多情況下,比如叢集應用程式,將這些資訊儲存在單獨的位置(例如資料庫)中會更好。

您可以透過配置自定義的 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(...);
    }
}