I have mentioned that reactive spring client cannot be integrated using the cas-client library before. Because cas-client doesn’t support reactive, so i have explained the way to integrate using OpenID Connect. (https://dgempiuc.medium.com/spring-cloud-gateway-and-apereo-cas-integration-fbb6d5c8440c)
{
"@class" : "org.apereo.cas.support.oauth.services.OAuthRegisteredService",
...
"description": "Spring Cloud Gateway client",
"logoutUrl" : "http://gateway-url/oidc/logout",
...
}
How CAS logout works?
When we want to logout from applications, we send this request directly to the CAS, not to the application. In this way, CAS both destroys the TGT and sends a logout request to the applications logged in with this TGT. CAS supports 2 types of logouts: Single Logout and Single Sign-out.
In the Single Logout process, a logout request is sent to all applications that have logged in using the TGT that comes with the logout request, while in the Single Sign-out process, a logout request is sent only to the application from which the logout request came. The default logout type in CAS is Single Logout.
From CAS documentation
Remember that the callback submitted to each CAS-protected application is simply a notification; nothing more. It is the responsibility of the application to intercept that notification and properly destroy the user authentication session, either manually, via a specific endpoint or more commonly via a CAS client library that supports SLO.
Login Process in cas-client
- In order for applications to talk to CAS, they need to add the cas-client library as a dependency, which indicates that they are a CAS client.
- When the application receives a request for the first time, it is directed to the CAS.
- After logging in to CAS, it redirects to the application with ST.
- The application that receives the ST sends a request to the CAS again to verify this ST within itself.
- When ST’s verification takes place in CAS, session is provided to the requesting user.
During the final phase, the filter provided by the cas-client library comes into play and adds the ST — Session pair to the map in the HashMapBackedSessionMappingStorage class. (key=ST, value=session)
Logout Process in cas-client
When the application notifies CAS that it wants to logout, CAS sends the following request to the application (by DefaultSingleLogoutMessageCreator class). Check CAS logout specification https://apereo.github.io/cas/6.2.x/protocol/CAS-Protocol-Specification.html#head_appdx_c) for SAML logout request.
<samlp:LogoutRequest+xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol:+ID="LR-3-fu50-Fr1LK127asqb1wgvxil"+Version="2.0"+IssueInstant="2020-09-18T12:53:56Z"><saml:NameID+xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">dg</saml:NameID><samlp:SessionIndex>ST-5-Q39Lj5mj8f6PddA1Ezhi4Xk6uD4-denizg-l</samlp:SessionIndex></samlp:LogoutRequest>
When the application receives a logout request, ST is removed from the request with the filter provided by the cas-client, the ST corresponding session is found from the map in the HashMapBackedSessionMappingStorage used during login, and that session is invalidated. The sample application log is as follows;
DEBUG --- HashMapBackedSessionMappingStorage : Attempting to remove Session=[28E77A7C5A42E54BB8E314B10A0115D0]DEBUG --- HashMapBackedSessionMappingStorage : Found mapping for session. Session Removed.DEBUG --- SingleSignOutHandler : Invalidating session [28E77A7C5A42E54BB8E314B10A0115D0] for token [ST-5-Q39Lj5mj8f6PddA1Ezhi4Xk6uD4-denizg-l]
Spring Cloud Gateway Logout Problem
But, here we run into the problem of logout. Since we can’t use the cas-client library and ST does not reach the application in OpenID Connect communication, we can’t do ST-session mapping. Therefore, we can’t know and invalidate the session corresponding to the ST in the request during logout.
So, what should we do?
- During the logout, a logout request is sent to the application by the CAS, containing the username information corresponding to that ST, not the ST. (We need to wrote our custom SingleLogoutServiceMessageHandler class.)
- The application that receives the logout request takes the user name from the request and the user’s access token is deleted. In this way, when the TokenRelay filter runs on requests to be downstreamed from the gateway, the access token cannot be found.
Let’s put this into code and change the CAS side first.
@Slf4j
public class DefaultSingleLogoutServiceMessageHandler extends BaseSingleLogoutServiceMessageHandler {
...
@Override
public boolean sendSingleLogoutMessage(SingleLogoutRequest request, SingleLogoutMessage lm) {
val logoutService = request.getService();
...
val registeredService = request.getRegisteredService();
if(checkServiceIsAuthenticatedByOIDC(registeredService)) {
val url = request.getLogoutUrl().toString();
val msg = new LogoutUser(getUsernameInRequest(request));
result = sendMessageToOIDCLogoutEndpoint(url, msg);
} else {
val msg = getLogoutHttpMessageToSend(request, lm);
result = super.sendMessageToEndpoint(msg, request, lm);
}
...
}
private boolean checkServiceIsAuthenticatedByOIDC(RegisteredService registeredService) {
return registeredService instanceof OAuthRegisteredService;
}
private boolean sendMessageToOIDCLogoutEndpoint(String logoutUrl, LogoutUser user) {
try {
...
restTemplate.postForEntity(logoutUrl, ..);
log.debug("OIDC logout request is sent to [{}]", user);
return true;
} catch (final Exception e) {
log.error("Unable to send logout message", e);
return false;
}
}
}
With this class, if it is a service of type OAuthRegisteredService, a custom request is sent to it. If it is not OAuth client, the default behavior is not broken and a SAML logout request is sent.
Let’s change Spring Cloud Gateway side.
@RestController
@RequiredArgsConstructor
@Slf4j
public class LogoutController { private final SessionService sessionService;
@PostMapping("/oidc/logout")
public Mono<Void> logout(@RequestBody LogoutUser logoutUser) {
String username = logoutUser.getUsername();
return invalidateSession(username);
};
private Mono<Void> invalidateSession(String username) {
Mono<Void> result = sessionService.destroy(username);
log.info("Session for {} is terminated", username);
return result;
}
}