Sign users out of all applications
This guide explains how to sign a user out of every application of an organization with one request, and how your applications learn about the sign-out so that they can end their own sessions.
Learning outcomes
- Call the SSO logout endpoint for all sessions or for the current session only.
- Receive and verify sign-out notifications in your applications.
- Receive OpenID Connect (OIDC) back-channel logout tokens.
- Find the cause when a user stays signed in.
What you need
- Applications that sign users in with Casdoor. See Set up single sign-on.
- A way to authenticate to the Casdoor API as the user: the access token or the session cookie
- To receive notifications: a notification provider on the application
About single sign-out
Single sign-out ends the sessions of a user in Casdoor and tells your applications to do the same. Typical uses are:
- Security incidents: End all sessions of an account at once.
- Organization policy: Sign users out everywhere when they leave the organization or change roles.
- Compliance: Guarantee that a user is signed out of all systems.
- User request: Let users sign out of all applications with one action.
The logoutAll parameter selects one of two modes:
Full sign-out: logoutAll=true (default) | Current session only: logoutAll=false | |
|---|---|---|
| Sessions | Deletes all sessions of the user across all applications of the organization | Deletes the current session |
| Access tokens | Expires all access tokens issued to the user | Clears the token of the current session |
| Notification | Contains all session IDs and the hashes of all expired tokens | Contains the current session ID and the hashes of its tokens |
Use logoutAll=false when a user signs out on one device or browser and stays signed in on the others.
This endpoint is different from RP-initiated logout, the standard OIDC end-session endpoint that a single client calls for its own session.
Call the SSO logout endpoint
GET or POST /api/sso-logout?logoutAll=<true|false>
| Parameter | Required | Description |
|---|---|---|
logoutAll | No | true, 1, or omitted: sign out of all sessions. Any other value, such as false or 0: sign out of the current session only |
Authenticate the request as the user, with the access token in the Authorization header or with the session cookie. See Choose an authentication method.
Sign out of all sessions:
curl -X POST https://door.casdoor.com/api/sso-logout \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Or explicitly specify logoutAll=true
curl -X POST "https://door.casdoor.com/api/sso-logout?logoutAll=true" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Sign out of the current session only:
curl -X POST "https://door.casdoor.com/api/sso-logout?logoutAll=false" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Authenticate with the session cookie:
curl -X POST https://door.casdoor.com/api/sso-logout \
--cookie "casdoor_session_id=abc123def456"
A successful request returns:
{
"status": "ok",
"msg": "",
"data": ""
}
| Field | Description |
|---|---|
status | "ok" on success, "error" on failure |
msg | Error message on failure, otherwise empty |
data | Empty for this endpoint |
Call the endpoint from your application
In a browser, with fetch:
// Logout from all sessions
fetch('https://door.casdoor.com/api/sso-logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`
},
credentials: 'include'
})
.then(response => response.json())
.then(data => {
console.log('Logout successful:', data);
window.location.href = '/login';
})
.catch(error => {
console.error('Logout failed:', error);
});
// Logout from current session only
fetch('https://door.casdoor.com/api/sso-logout?logoutAll=false', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`
},
credentials: 'include'
})
.then(response => response.json())
.then(data => {
console.log('Logged out from current session:', data);
window.location.href = '/login';
});
With the Go SDK:
import "github.com/casdoor/casdoor-go-sdk/casdoorsdk"
func logout(w http.ResponseWriter, r *http.Request) {
// Get the access token from the session or request
token := getAccessTokenFromSession(r)
// Call the SSO logout endpoint
err := casdoorsdk.Logout(token)
if err != nil {
http.Error(w, "Logout failed", http.StatusInternalServerError)
return
}
// Clear local session
clearSession(w, r)
// Redirect to login page
http.Redirect(w, r, "/login", http.StatusFound)
}
With the JavaScript SDK:
import Sdk from "casdoor-js-sdk";
const CasdoorSDK = new Sdk({
serverUrl: "https://door.casdoor.com",
clientId: "YOUR_CLIENT_ID",
appName: "YOUR_APP_NAME",
organizationName: "YOUR_ORG_NAME",
});
async function handleLogout() {
try {
// Call the SSO logout endpoint
await CasdoorSDK.logout();
// Clear local state
localStorage.removeItem('casdoor_token');
sessionStorage.clear();
// Redirect to login page
window.location.href = '/login';
} catch (error) {
console.error('Logout failed:', error);
}
}
With the Python SDK:
from casdoor import CasdoorSDK
sdk = CasdoorSDK(
endpoint="https://door.casdoor.com",
client_id="YOUR_CLIENT_ID",
client_secret="YOUR_CLIENT_SECRET",
certificate="YOUR_CERT",
org_name="YOUR_ORG_NAME",
app_name="YOUR_APP_NAME",
)
def logout(access_token):
try:
# Call the SSO logout endpoint
result = sdk.logout(access_token)
if result['status'] == 'ok':
# Clear local session
clear_session()
return True
else:
print(f"Logout failed: {result['msg']}")
return False
except Exception as e:
print(f"Logout error: {e}")
return False
Whatever the result of the request, clear the local state of your application afterward: the stored tokens, the cached user, and local session cookies. A user who chose to sign out must appear signed out even if the request to Casdoor failed.
async function logout() {
try {
await callSSOLogout();
} catch (error) {
console.error('SSO logout failed, clearing local state anyway:', error);
} finally {
// Always clear local state
clearLocalAuthenticationState();
redirectToLogin();
}
}
Receive sign-out notifications
On sign-out, Casdoor sends a notification to each notification provider of the application that the user signed up with. Your applications use the notification to end their own sessions for that user.
Configure the notification provider
-
In the Casdoor admin console, add a notification provider, for example of type Custom HTTP, and add it to the application.
-
For a Custom HTTP provider, set the following fields:
Field Value Endpoint Endpoint of your application that receives the notification, for example https://app.example.com/api/logout-webhookMethod POSTParameter content. This is the name of the form field that carries the JSON payload
Read the notification
Casdoor sends a POST request with the following payload:
{
"owner": "org-name",
"name": "username",
"displayName": "John Doe",
"email": "user@example.com",
"phone": "+1234567890",
"id": "user-id",
"event": "sso-logout",
"sessionIds": ["session-123", "session-456"],
"accessTokenHashes": ["hash-abc", "hash-def"],
"nonce": "random-nonce-xyz",
"timestamp": 1699900000,
"signature": "hmac-sha256-signature"
}
| Field | Description |
|---|---|
sessionIds | IDs of the sessions that were ended |
accessTokenHashes | SHA-256 hashes of the access tokens that were expired. Compare them with the hashes of the tokens that your application holds to find the sessions to end |
nonce | Random value that protects against replay |
timestamp | Unix time at which Casdoor created the notification |
signature | HMAC-SHA256 signature, computed with the client secret of the application |
Verify the notification
Anyone who can reach your endpoint can send a request to it. Before you act on a notification:
-
Verify the signature. In Go:
// Example verification in Go
func verifyLogoutNotification(notification *SsoLogoutNotification, clientSecret string) bool {
data := fmt.Sprintf("%s|%s|%s|%d|%s|%s",
notification.Owner,
notification.Name,
notification.Nonce,
notification.Timestamp,
strings.Join(notification.SessionIds, ","),
strings.Join(notification.AccessTokenHashes, ","))
expectedSignature := hmacSHA256(clientSecret, data)
return notification.Signature == expectedSignature
}In JavaScript:
// Example verification in JavaScript
const crypto = require('crypto');
function verifyLogoutNotification(notification, clientSecret) {
const data = `${notification.owner}|${notification.name}|${notification.nonce}|${notification.timestamp}|${notification.sessionIds.join(',')}|${notification.accessTokenHashes.join(',')}`;
const expectedSignature = crypto
.createHmac('sha256', clientSecret)
.update(data)
.digest('hex');
return notification.signature === expectedSignature;
} -
Reject notifications that are too old, for example older than five minutes:
function isNotificationValid(notification) {
const maxAge = 5 * 60 * 1000; // 5 minutes in milliseconds
const now = Date.now();
const notificationTime = notification.timestamp * 1000; // Convert to milliseconds
if (now - notificationTime > maxAge) {
console.error('Notification is too old, possible replay attack');
return false;
}
return verifyLogoutNotification(notification, clientSecret);
} -
End the sessions in your application that match
sessionIdsoraccessTokenHashes.
The client secret signs the notifications. Keep it on the server, for example in an environment variable or a secret manager, and never ship it in client-side code.
Receive OIDC back-channel logout tokens
Casdoor supports OpenID Connect Back-Channel Logout 1.0. When a user signs out through RP-initiated logout, Casdoor sends a signed logout token to every application that has an active session for the user. Each application then ends its own session, server to server.
- In the Casdoor admin console, open the edit page of the application.
- Set Backchannel logout URL (
backchannelLogoutUri) to the endpoint of your application that receives the logout token.
Casdoor sends an HTTP POST request with a logout_token parameter to that URI. The token is a JSON Web Token (JWT) with the sub and sid claims and the http://schemas.openid.net/event/backchannel-logout event. Casdoor skips applications that have no back-channel logout URL.
Fehlerbehebung
The user stays signed in to some applications
- Check that all applications sign users in through Casdoor and belong to the same organization.
- Check that each application validates the token on every request, or acts on the sign-out notification. An application that relies only on its own local session doesn't notice the sign-out.
A token still works after sign-out
- Check that the request went to
/api/sso-logoutand was authenticated as the user. - Check that your applications don't cache the result of token validation.
After a full sign-out, the token records stay in the database with ExpiresIn set to 0. The token introspection endpoint and the refresh token endpoint reject them. A client that uses a refresh token after sign-out receives the error invalid_grant with the message "refresh token is invalid, expired or revoked".
The endpoint returns an error
| Status | Cause | What to do |
|---|---|---|
| HTTP 401 | The access token is invalid or expired | Clear the local state and send the user to the sign-in page |
| HTTP 403 | The user isn't allowed to sign out. This points to a configuration problem | Check the configuration of the application |
| HTTP 500 | Error on the server | Log the error and clear the local state anyway |
async function logout() {
try {
// Replace with your actual Casdoor server URL
const response = await fetch('https://door.casdoor.com/api/sso-logout', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) {
console.error(`Logout failed with status: ${response.status}`);
// Clear local state anyway
}
} catch (error) {
console.error('Logout error:', error);
} finally {
clearLocalAuthenticationState();
redirectToLogin();
}
}