There are two ways a logout in SAML can happen: Service Provider (SP) initiated and Identity Provider (IDP) initiated logout. I'll explain how to implement both flows with devise_saml_authenticatable.
Note
SAML also supports a
SOAPand anArtifactbinding to do this. This guide only refers toPOSTandRedirectbindings.devise_saml_authenticatabledoes not supportSOAPandArtifactbindings.
SP initiated logout (using the Redirect Binding)
When the user clicks on Logout within the app, the app can trigger a logout for the user in the IDP.
This way the user does not get immediately reauthenticated when he visits the app again, but gets redirected to the IDP login mask.
This might be necessary, if the user has multiple accounts within the IDP he needs to switch between, which is not really practical without a SP initiated logout.
Flow:
- The user clicks
Logoutwithin the app - This triggers a DELETE
/users/sign_out(at least fordevise_saml_authenticatable, others might differ) - The server generates a SAML
LogoutRequestand responds with a redirect the theSingleLogoutServiceHTTP-Redirect Binding (found in the IDP metadata). TheLogoutRequestis contained in the redirect URL params as an URL, base64 encoded XML document. - The Browser follows the redirect to the IDP
- The IDP checks the request, logs out the user from itself
- The IDP generates a SAML
LogoutResponseand redirects the user back the app. TheLogoutResponseis contained in the redirect URL params as a URL, base64 encoded
xml document.
The URL for this redirect needs to be configured at the IDP. For example in keycloak it is theLogout Service Redirect Binding URLfound in theAdvancedtab of the client. - The app server could now parse the
LogoutResponse, if necessary. It doesn't contain a lot, mainly some general information about the IDP and a status, if the logout at the IDP was successful. The session for the app is already destroyed upon DELETE/users/sign_out, so there isn't really anything we need to do there.
Note
There is another way this can be done: The IDP can be configured to perform a backchannel logout when receiving a SP initiated logout. This changes the flow from 6. onwards: Instead of redirecting the user back with a LogoutResponse, the IDP performs a IDP initiated logout like described below.
devise_saml_authenticatableactually does nothing in that case, as the session within the app already has been destroyed before sending the Request to IDP. Then the user gets redirected to theLogout Service POST Binding URLwith theLogoutResponse.
Keycloak does this whenFront channel logoutis disabled.
Testing in development using a local keycloak server
You can test this using a local keycloak server.
You need to configure:
-
Client->Settings->Valid post logout redirect URIs:* -
Client->Advanced->Logout Service Redirect Binding URL: The url to redirect to after a logout
Troubleshooting when using devise_saml_authenticatable
devise_saml_authenticatable does a SP initiated logout by default, if not otherwise configured. But there are some steps I had to take in order to make it work.
Don´t overwrite #after_sign_out_path_for in the SamlSessionsController
In this method the LogoutRequest gets generated and returned alongside the SingleLogoutService URL. If this does not happen, obviously no logout at the IDP will occur.
But the normal logout within the app does happen, so it might seam confusing why the IDP logout is not working.
Calling current_user within the after logout route results in an exception
This might show up as a exception Issuer of the Response not found or multiple. from within RubySaml.
devise_saml_authenticatable (at least up to version 1.9.1) has a bug there:
If you call current_user when a LogoutResponse is present as an URL param it fails to parse it and throws an error. devise_saml_authenticatable tries to parse the LogoutResponse as a normal SAML Response and fails because a LogoutResponse is just different.
As there should never be a current user present for this route, I solved this by overwriting #current_user within the SamlSessionsController and just returning nil for those routes:
def current_user
super unless params[:action].in? %w[after_logout idp_sign_out]
end
Unsafe redirect when trying to log out
Since Rails 7 you need to pass allow_other_host: true to redirect_to to allow a redirect to other hosts than the configured app hosts.
Since we need to redirect to the IDP on logout and devise does not do that we get a
Unsafe redirect to "https://some-idp.example.com/realms/dev/protocol/saml?SAMLRequest=fZJBb9QwEIX%2FSm45zcZx4j...", pass allow_other_host: true to redirect anyway.
upon trying to logout.
See https://github.com/apokalipto/devise_saml_authenticatable/issues/237 Show archive.org snapshot and https://github.com/heartcombo/devise/pull/5462 Show archive.org snapshot . This might get fixed in the future, so check if this patch is still necessary.
I fixed this by patching #respond_to_on_destroy within the SamlSessionsController and adding the allow_other_host: true
# Patched to allow to for redirect to the idp. Can be removed once https://github.com/heartcombo/devise/pull/5462 is merged and released.
def respond_to_on_destroy
raise 'Check if this patch is still needed.' unless Devise::VERSION == '4.9.3'
respond_to do |format|
format.all { head :no_content }
format.any(*navigational_formats) { redirect_to after_sign_out_path_for(resource_name), allow_other_host: true, status: Devise.responder.redirect_status }
end
end
IDP initiated logout
SAML also supports a logout initiated from the IDP side. This will destroy the user session within the app when someone clicks a button within the IDP or the user uses SLO within another SP connected to the IDP, depending on the configuration at the IDP.
A little documentation for devise_saml_authenticatable:
https://github.com/apokalipto/devise_saml_authenticatable?tab=readme-ov-file#logout-request
Show archive.org snapshot
Flow
- The IDP sends a POST with a SAML
LogoutRequestto the appserver to a URL, configured at the IDP (For keycloak theLogout Service POST Binding URL) - The appserver parses the response and destroys the user's session (There obviously is no user browser involved in this)
How to enable
- You need to configure
config.saml_session_index_key = :saml_sessionwithin the devise config, so the appserver can find the user from theLogoutRequest - The route for this is
/users/saml/idp_sign_out(for devise_saml_authenticatable) and is enabled per default - This just worked for me, so no troubleshooting here
Testing in development using a local keycloak server
You can test this using a local keycloak server.
You need to configure:
-
Client->Advanced->Logout Service POST Binding URL: The url to receive the logout request, e.g. fordevise_saml_authenticatablethis would behttp://localhost:3000/users/saml/idp_sign_out -
Client->Settings->Front channel logout(all the way at the bottom): Turn this off
Now login using the IDP and you should see a session for the user within Sessions in keycloak. Click the three dots for the session you want to terminate and choose Sign out. The appserver should receive a POST to the configured URL and destroy the user´s session. When reloading the tab with the app, you should see the IDP login mask again.