logout.adoc 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. = OIDC Logout
  2. Once an end user is able to login to your application, it's important to consider how they will log out.
  3. Generally speaking, there are three use cases for you to consider:
  4. 1. I want to perform only a local logout
  5. 2. I want to log out both my application and the OIDC Provider, initiated by my application
  6. 3. I want to log out both my application and the OIDC Provider, initiated by the OIDC Provider
  7. [[configure-local-logout]]
  8. == Local Logout
  9. To perform a local logout, no special OIDC configuration is needed.
  10. Spring Security automatically stands up a local logout endpoint, which you can xref:servlet/authentication/logout.adoc[configure through the `logout()` DSL].
  11. [[configure-client-initiated-oidc-logout]]
  12. == OpenID Connect 1.0 Client-Initiated Logout
  13. OpenID Connect Session Management 1.0 allows the ability to log out the end user at the Provider by using the Client.
  14. One of the strategies available is https://openid.net/specs/openid-connect-rpinitiated-1_0.html[RP-Initiated Logout].
  15. 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].
  16. You can do so by configuring the `ClientRegistration` with the `issuer-uri`, as follows:
  17. [source,yaml]
  18. ----
  19. spring:
  20. security:
  21. oauth2:
  22. client:
  23. registration:
  24. okta:
  25. client-id: okta-client-id
  26. client-secret: okta-client-secret
  27. ...
  28. provider:
  29. okta:
  30. issuer-uri: https://dev-1234.oktapreview.com
  31. ----
  32. Also, you should configure `OidcClientInitiatedLogoutSuccessHandler`, which implements RP-Initiated Logout, as follows:
  33. [tabs]
  34. ======
  35. Java::
  36. +
  37. [source,java,role="primary"]
  38. ----
  39. @Configuration
  40. @EnableWebSecurity
  41. public class OAuth2LoginSecurityConfig {
  42. @Autowired
  43. private ClientRegistrationRepository clientRegistrationRepository;
  44. @Bean
  45. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  46. http
  47. .authorizeHttpRequests((authorize) -> authorize
  48. .anyRequest().authenticated()
  49. )
  50. .oauth2Login(withDefaults())
  51. .logout((logout) -> logout
  52. .logoutSuccessHandler(oidcLogoutSuccessHandler())
  53. );
  54. return http.build();
  55. }
  56. private LogoutSuccessHandler oidcLogoutSuccessHandler() {
  57. OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
  58. new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository);
  59. // Sets the location that the End-User's User Agent will be redirected to
  60. // after the logout has been performed at the Provider
  61. oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");
  62. return oidcLogoutSuccessHandler;
  63. }
  64. }
  65. ----
  66. Kotlin::
  67. +
  68. [source,kotlin,role="secondary"]
  69. ----
  70. @Configuration
  71. @EnableWebSecurity
  72. class OAuth2LoginSecurityConfig {
  73. @Autowired
  74. private lateinit var clientRegistrationRepository: ClientRegistrationRepository
  75. @Bean
  76. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  77. http {
  78. authorizeHttpRequests {
  79. authorize(anyRequest, authenticated)
  80. }
  81. oauth2Login { }
  82. logout {
  83. logoutSuccessHandler = oidcLogoutSuccessHandler()
  84. }
  85. }
  86. return http.build()
  87. }
  88. private fun oidcLogoutSuccessHandler(): LogoutSuccessHandler {
  89. val oidcLogoutSuccessHandler = OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository)
  90. // Sets the location that the End-User's User Agent will be redirected to
  91. // after the logout has been performed at the Provider
  92. oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}")
  93. return oidcLogoutSuccessHandler
  94. }
  95. }
  96. ----
  97. ======
  98. [NOTE]
  99. ====
  100. `OidcClientInitiatedLogoutSuccessHandler` supports the `+{baseUrl}+` placeholder.
  101. If used, the application's base URL, such as `https://app.example.org`, replaces it at request time.
  102. ====
  103. [NOTE]
  104. ====
  105. By default, `OidcClientInitiatedLogoutSuccessHandler` redirects to the logout URL using a standard HTTP redirect with the `GET` method.
  106. To perform the logout using a `POST` request, set the redirect strategy to `FormPostRedirectStrategy`, for example with `OidcClientInitiatedLogoutSuccessHandler.setRedirectStrategy(new FormPostRedirectStrategy())`.
  107. ====
  108. [[configure-provider-initiated-oidc-logout]]
  109. == OpenID Connect 1.0 Back-Channel Logout
  110. 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.
  111. This is referred to as https://openid.net/specs/openid-connect-backchannel-1_0.html[OIDC Back-Channel Logout].
  112. To enable this, you can stand up the Back-Channel Logout endpoint in the DSL like so:
  113. [tabs]
  114. ======
  115. Java::
  116. +
  117. [source,java,role="primary"]
  118. ----
  119. @Bean
  120. OidcBackChannelLogoutHandler oidcLogoutHandler() {
  121. return new OidcBackChannelLogoutHandler();
  122. }
  123. @Bean
  124. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  125. http
  126. .authorizeHttpRequests((authorize) -> authorize
  127. .anyRequest().authenticated()
  128. )
  129. .oauth2Login(withDefaults())
  130. .oidcLogout((logout) -> logout
  131. .backChannel(Customizer.withDefaults())
  132. );
  133. return http.build();
  134. }
  135. ----
  136. Kotlin::
  137. +
  138. [source,kotlin,role="secondary"]
  139. ----
  140. @Bean
  141. fun oidcLogoutHandler(): OidcBackChannelLogoutHandler {
  142. return OidcBackChannelLogoutHandler()
  143. }
  144. @Bean
  145. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  146. http {
  147. authorizeHttpRequests {
  148. authorize(anyRequest, authenticated)
  149. }
  150. oauth2Login { }
  151. oidcLogout {
  152. backChannel { }
  153. }
  154. }
  155. return http.build()
  156. }
  157. ----
  158. ======
  159. Then, you need a way listen to events published by Spring Security to remove old `OidcSessionInformation` entries, like so:
  160. [tabs]
  161. ======
  162. Java::
  163. +
  164. [source,java,role="primary"]
  165. ----
  166. @Bean
  167. public HttpSessionEventPublisher sessionEventPublisher() {
  168. return new HttpSessionEventPublisher();
  169. }
  170. ----
  171. Kotlin::
  172. +
  173. [source,kotlin,role="secondary"]
  174. ----
  175. @Bean
  176. open fun sessionEventPublisher(): HttpSessionEventPublisher {
  177. return HttpSessionEventPublisher()
  178. }
  179. ----
  180. ======
  181. This will make so that if `HttpSession#invalidate` is called, then the session is also removed from memory.
  182. And that's it!
  183. 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.
  184. [NOTE]
  185. `oidcLogout` requires that `oauth2Login` also be configured.
  186. [NOTE]
  187. `oidcLogout` requires that the session cookie be called `JSESSIONID` in order to correctly log out each session through a backchannel.
  188. === Back-Channel Logout Architecture
  189. Consider a `ClientRegistration` whose identifier is `registrationId`.
  190. The overall flow for a Back-Channel logout is like this:
  191. 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.
  192. 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.
  193. 3. Spring Security validates the token's signature and claims.
  194. 4. If the token contains a `sid` claim, then only the Client's session that correlates to that provider session is terminated.
  195. 5. Otherwise, if the token contains a `sub` claim, then all that Client's sessions for that End User are terminated.
  196. [NOTE]
  197. Remember that Spring Security's OIDC support is multi-tenant.
  198. This means that it will only terminate sessions whose Client matches the `aud` claim in the Logout Token.
  199. One notable part of this architecture's implementation is that it propagates the incoming back-channel request internally for each corresponding session.
  200. Initially, this may seem unnecessary.
  201. However, recall that the Servlet API does not give direct access to the `HttpSession` store.
  202. By making an internal logout call, the corresponding session can now be invalidated.
  203. Additionally, forging a logout call internally allows for each set of ``LogoutHandler``s to be run against that session and corresponding `SecurityContext`.
  204. === Customizing the Session Logout Endpoint
  205. With `OidcBackChannelLogoutHandler` published, the session logout endpoint is `+{baseUrl}+/logout/connect/back-channel/+{registrationId}+`.
  206. 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.
  207. In the event that you need to customize the endpoint, you can provide the URL as follows:
  208. [tabs]
  209. ======
  210. Java::
  211. +
  212. [source=java,role="primary"]
  213. ----
  214. http
  215. // ...
  216. .oidcLogout((oidc) -> oidc
  217. .backChannel((backChannel) -> backChannel
  218. .logoutUri("http://localhost:9000/logout/connect/back-channel/+{registrationId}+")
  219. )
  220. );
  221. ----
  222. Kotlin::
  223. +
  224. [source=kotlin,role="secondary"]
  225. ----
  226. http {
  227. oidcLogout {
  228. backChannel {
  229. logoutUri = "http://localhost:9000/logout/connect/back-channel/+{registrationId}+"
  230. }
  231. }
  232. }
  233. ----
  234. ======
  235. === Customizing the Session Logout Cookie Name
  236. By default, the session logout endpoint uses the `JSESSIONID` cookie to correlate the session to the corresponding `OidcSessionInformation`.
  237. However, the default cookie name in Spring Session is `SESSION`.
  238. You can configure Spring Session's cookie name in the DSL like so:
  239. [tabs]
  240. ======
  241. Java::
  242. +
  243. [source=java,role="primary"]
  244. ----
  245. @Bean
  246. OidcBackChannelLogoutHandler oidcLogoutHandler(OidcSessionRegistry oidcSessionRegistry) {
  247. OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(oidcSessionRegistry);
  248. logoutHandler.setSessionCookieName("SESSION");
  249. return logoutHandler;
  250. }
  251. ----
  252. Kotlin::
  253. +
  254. [source=kotlin,role="secondary"]
  255. ----
  256. @Bean
  257. open fun oidcLogoutHandler(val sessionRegistry: OidcSessionRegistry): OidcBackChannelLogoutHandler {
  258. val logoutHandler = OidcBackChannelLogoutHandler(sessionRegistry)
  259. logoutHandler.setSessionCookieName("SESSION")
  260. return logoutHandler
  261. }
  262. ----
  263. ======
  264. [[oidc-backchannel-logout-session-registry]]
  265. === Customizing the OIDC Provider Session Registry
  266. By default, Spring Security stores in-memory all links between the OIDC Provider session and the Client session.
  267. 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.
  268. You can achieve this by configuring a custom `OidcSessionRegistry`, like so:
  269. [tabs]
  270. ======
  271. Java::
  272. +
  273. [source,java,role="primary"]
  274. ----
  275. @Component
  276. public final class MySpringDataOidcSessionRegistry implements OidcSessionRegistry {
  277. private final OidcProviderSessionRepository sessions;
  278. // ...
  279. @Override
  280. public void saveSessionInformation(OidcSessionInformation info) {
  281. this.sessions.save(info);
  282. }
  283. @Override
  284. public OidcSessionInformation removeSessionInformation(String clientSessionId) {
  285. return this.sessions.removeByClientSessionId(clientSessionId);
  286. }
  287. @Override
  288. public Iterable<OidcSessionInformation> removeSessionInformation(OidcLogoutToken token) {
  289. return token.getSessionId() != null ?
  290. this.sessions.removeBySessionIdAndIssuerAndAudience(...) :
  291. this.sessions.removeBySubjectAndIssuerAndAudience(...);
  292. }
  293. }
  294. ----
  295. Kotlin::
  296. +
  297. [source,kotlin,role="secondary"]
  298. ----
  299. @Component
  300. class MySpringDataOidcSessionRegistry: OidcSessionRegistry {
  301. val sessions: OidcProviderSessionRepository
  302. // ...
  303. @Override
  304. fun saveSessionInformation(info: OidcSessionInformation) {
  305. this.sessions.save(info)
  306. }
  307. @Override
  308. fun removeSessionInformation(clientSessionId: String): OidcSessionInformation {
  309. return this.sessions.removeByClientSessionId(clientSessionId);
  310. }
  311. @Override
  312. fun removeSessionInformation(token: OidcLogoutToken): Iterable<OidcSessionInformation> {
  313. return token.getSessionId() != null ?
  314. this.sessions.removeBySessionIdAndIssuerAndAudience(...) :
  315. this.sessions.removeBySubjectAndIssuerAndAudience(...);
  316. }
  317. }
  318. ----
  319. ======