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
SOAP
and anArtifact
binding to do this. This guide only refers toPOST
andRedirect
bindings.devise_saml_authenticatable
does not supportSOAP
andArtifact
bindings.
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
Logout
within the app - This triggers a DELETE
/users/sign_out
(at least fordevise_saml_authenticatable
, others might differ) - The server generates a SAML
LogoutRequest
and responds with a redirect the theSingleLogoutService
HTTP-Redirect Binding (found in the IDP metadata). TheLogoutRequest
is 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
LogoutResponse
and redirects the user back the app. TheLogoutResponse
is contained in the redirect URL params as a URL, base64 encoded
xml document.
The URL für this redirect needs to be configured at the IDP. For example in keycloak it is theLogout Service Redirect Binding URL
found in theAdvanced
tab 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_authenticatable
actually 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 URL
with theLogoutResponse
.
Keycloak does this whenFront channel logout
is 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 send a POST with a SAML
LogoutRequest
to the appserver to a, at the IDP, configured URL (For keycloak theLogout Service POST Binding URL
) - The appserver parses the response and destroys the users session (There obviously is no user browser involved in this)
How to enable
- You need to configure
config.saml_session_index_key = :saml_session
within 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_authenticatable
this 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.