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:
-
I want to perform only a local logout
-
I want to log out both my application and the OIDC Provider, initiated by my application
-
I want to log out both my application and the OIDC Provider, initiated by the OIDC Provider
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 configure through the logout()
DSL.
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 RP-Initiated Logout.
If the OpenID Provider supports both Session Management and Discovery, the client can obtain the end_session_endpoint
URL
from the OpenID Provider’s Discovery Metadata.
You can do so by configuring the ClientRegistration
with the issuer-uri
, as follows:
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 OidcClientInitiatedServerLogoutSuccessHandler
, which implements RP-Initiated Logout, as follows:
-
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
}
}
|
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 OIDC Back-Channel Logout.
To enable this, you can stand up the Back-Channel Logout endpoint in the DSL like so:
-
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()
}
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.
oidcLogout requires that oauth2Login also be configured.
|
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:
-
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
ReactiveOidcSessionRegistry
implementation. -
Then at logout time, your OIDC Provider makes an API call to
/logout/connect/back-channel/registrationId
including a Logout Token that indicates either thesub
(the End User) or thesid
(the Provider Session ID) to logout. -
Spring Security validates the token’s signature and claims.
-
If the token contains a
sid
claim, then only the Client’s session that correlates to that provider session is terminated. -
Otherwise, if the token contains a
sub
claim, then all that Client’s sessions for that End User are terminated.
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.
|
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 ReactiveOidcSessionRegistry
, like so:
-
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(...);
}
}