123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391 |
- = OIDC Logout
- Once an end user is able to login to your application, it's important to consider how they will log out.
- Generally speaking, there are three use cases for you to consider:
- 1. I want to perform only a local logout
- 2. I want to log out both my application and the OIDC Provider, initiated by my application
- 3. I want to log out both my application and the OIDC Provider, initiated by the OIDC Provider
- [[configure-local-logout]]
- == Local Logout
- To perform a local logout, no special OIDC configuration is needed.
- Spring Security automatically stands up a local logout endpoint, which you can xref:servlet/authentication/logout.adoc[configure through the `logout()` DSL].
- [[configure-client-initiated-oidc-logout]]
- == OpenID Connect 1.0 Client-Initiated Logout
- OpenID Connect Session Management 1.0 allows the ability to log out the end user at the Provider by using the Client.
- One of the strategies available is https://openid.net/specs/openid-connect-rpinitiated-1_0.html[RP-Initiated Logout].
- If the OpenID Provider supports both Session Management and https://openid.net/specs/openid-connect-discovery-1_0.html[Discovery], the client can obtain the `end_session_endpoint` `URL` from the OpenID Provider's https://openid.net/specs/openid-connect-session-1_0.html#OPMetadata[Discovery Metadata].
- You can do so by configuring the `ClientRegistration` with the `issuer-uri`, as follows:
- [source,yaml]
- ----
- 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
- ----
- Also, you should configure `OidcClientInitiatedLogoutSuccessHandler`, which implements RP-Initiated Logout, as follows:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @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;
- }
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @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
- }
- }
- ----
- ======
- [NOTE]
- ====
- `OidcClientInitiatedLogoutSuccessHandler` supports the `+{baseUrl}+` placeholder.
- If used, the application's base URL, such as `https://app.example.org`, replaces it at request time.
- ====
- [NOTE]
- ====
- By default, `OidcClientInitiatedLogoutSuccessHandler` redirects to the logout URL using a standard HTTP redirect with the `GET` method.
- To perform the logout using a `POST` request, set the redirect strategy to `FormPostRedirectStrategy`, for example with `OidcClientInitiatedLogoutSuccessHandler.setRedirectStrategy(new FormPostRedirectStrategy())`.
- ====
- [[configure-provider-initiated-oidc-logout]]
- == OpenID Connect 1.0 Back-Channel Logout
- OpenID Connect Session Management 1.0 allows the ability to log out the end user at the Client by having the Provider make an API call to the Client.
- This is referred to as https://openid.net/specs/openid-connect-backchannel-1_0.html[OIDC Back-Channel Logout].
- To enable this, you can stand up the Back-Channel Logout endpoint in the DSL like so:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Bean
- OidcBackChannelLogoutHandler oidcLogoutHandler() {
- return new OidcBackChannelLogoutHandler();
- }
- @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();
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Bean
- fun oidcLogoutHandler(): OidcBackChannelLogoutHandler {
- return OidcBackChannelLogoutHandler()
- }
- @Bean
- open fun filterChain(http: HttpSecurity): SecurityFilterChain {
- http {
- authorizeHttpRequests {
- authorize(anyRequest, authenticated)
- }
- oauth2Login { }
- oidcLogout {
- backChannel { }
- }
- }
- return http.build()
- }
- ----
- ======
- Then, you need a way listen to events published by Spring Security to remove old `OidcSessionInformation` entries, like so:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Bean
- public HttpSessionEventPublisher sessionEventPublisher() {
- return new HttpSessionEventPublisher();
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Bean
- open fun sessionEventPublisher(): HttpSessionEventPublisher {
- return HttpSessionEventPublisher()
- }
- ----
- ======
- This will make so that if `HttpSession#invalidate` is called, then the session is also removed from memory.
- And that's it!
- This will stand up the endpoint `+/logout/connect/back-channel/{registrationId}+` which the OIDC Provider can request to invalidate a given session of an end user in your application.
- [NOTE]
- `oidcLogout` requires that `oauth2Login` also be configured.
- [NOTE]
- `oidcLogout` requires that the session cookie be called `JSESSIONID` in order to correctly log out each session through a backchannel.
- === Back-Channel Logout Architecture
- Consider a `ClientRegistration` whose identifier is `registrationId`.
- The overall flow for a Back-Channel logout is like this:
- 1. At login time, Spring Security correlates the ID Token, CSRF Token, and Provider Session ID (if any) to your application's session id in its `OidcSessionRegistry` implementation.
- 2. Then at logout time, your OIDC Provider makes an API call to `/logout/connect/back-channel/registrationId` including a Logout Token that indicates either the `sub` (the End User) or the `sid` (the Provider Session ID) to logout.
- 3. Spring Security validates the token's signature and claims.
- 4. If the token contains a `sid` claim, then only the Client's session that correlates to that provider session is terminated.
- 5. Otherwise, if the token contains a `sub` claim, then all that Client's sessions for that End User are terminated.
- [NOTE]
- Remember that Spring Security's OIDC support is multi-tenant.
- This means that it will only terminate sessions whose Client matches the `aud` claim in the Logout Token.
- One notable part of this architecture's implementation is that it propagates the incoming back-channel request internally for each corresponding session.
- Initially, this may seem unnecessary.
- However, recall that the Servlet API does not give direct access to the `HttpSession` store.
- By making an internal logout call, the corresponding session can now be invalidated.
- Additionally, forging a logout call internally allows for each set of ``LogoutHandler``s to be run against that session and corresponding `SecurityContext`.
- === Customizing the Session Logout Endpoint
- With `OidcBackChannelLogoutHandler` published, the session logout endpoint is `+{baseUrl}+/logout/connect/back-channel/+{registrationId}+`.
- If `OidcBackChannelLogoutHandler` is not wired, then the URL is `+{baseUrl}+/logout/connect/back-channel/+{registrationId}+`, which is not recommended since it requires passing a CSRF token, which can be challenging depending on the kind of repository your application uses.
- In the event that you need to customize the endpoint, you can provide the URL as follows:
- [tabs]
- ======
- Java::
- +
- [source=java,role="primary"]
- ----
- http
- // ...
- .oidcLogout((oidc) -> oidc
- .backChannel((backChannel) -> backChannel
- .logoutUri("http://localhost:9000/logout/connect/back-channel/+{registrationId}+")
- )
- );
- ----
- Kotlin::
- +
- [source=kotlin,role="secondary"]
- ----
- http {
- oidcLogout {
- backChannel {
- logoutUri = "http://localhost:9000/logout/connect/back-channel/+{registrationId}+"
- }
- }
- }
- ----
- ======
- === Customizing the Session Logout Cookie Name
- By default, the session logout endpoint uses the `JSESSIONID` cookie to correlate the session to the corresponding `OidcSessionInformation`.
- However, the default cookie name in Spring Session is `SESSION`.
- You can configure Spring Session's cookie name in the DSL like so:
- [tabs]
- ======
- Java::
- +
- [source=java,role="primary"]
- ----
- @Bean
- OidcBackChannelLogoutHandler oidcLogoutHandler(OidcSessionRegistry oidcSessionRegistry) {
- OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(oidcSessionRegistry);
- logoutHandler.setSessionCookieName("SESSION");
- return logoutHandler;
- }
- ----
- Kotlin::
- +
- [source=kotlin,role="secondary"]
- ----
- @Bean
- open fun oidcLogoutHandler(val sessionRegistry: OidcSessionRegistry): OidcBackChannelLogoutHandler {
- val logoutHandler = OidcBackChannelLogoutHandler(sessionRegistry)
- logoutHandler.setSessionCookieName("SESSION")
- return logoutHandler
- }
- ----
- ======
- [[oidc-backchannel-logout-session-registry]]
- === Customizing the OIDC Provider Session Registry
- By default, Spring Security stores in-memory all links between the OIDC Provider session and the Client session.
- There are a number of circumstances, like a clustered application, where it would be nice to store this instead in a separate location, like a database.
- You can achieve this by configuring a custom `OidcSessionRegistry`, like so:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Component
- public final class MySpringDataOidcSessionRegistry implements OidcSessionRegistry {
- private final OidcProviderSessionRepository sessions;
- // ...
- @Override
- public void saveSessionInformation(OidcSessionInformation info) {
- this.sessions.save(info);
- }
- @Override
- public OidcSessionInformation removeSessionInformation(String clientSessionId) {
- return this.sessions.removeByClientSessionId(clientSessionId);
- }
- @Override
- public Iterable<OidcSessionInformation> removeSessionInformation(OidcLogoutToken token) {
- return token.getSessionId() != null ?
- this.sessions.removeBySessionIdAndIssuerAndAudience(...) :
- this.sessions.removeBySubjectAndIssuerAndAudience(...);
- }
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Component
- class MySpringDataOidcSessionRegistry: OidcSessionRegistry {
- 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(...);
- }
- }
- ----
- ======
|