famous-jackal-2829
06/25/2025, 6:07 AMbrief-egg-61061
06/25/2025, 6:07 AMbrief-egg-61061
06/25/2025, 6:08 AMhydra revoke token --client-id <client-id> --client-secret <client-secret> <token>
[Hydra CLI: revoke token](https://www.ory.sh/docs/hydra/cli/hydra-revoke-token) 2. Delete all access tokens for a specific client: hydra delete access-tokens <client-id>
[Hydra CLI: delete access-tokens](https://www.ory.sh/docs/hydra/cli/hydra-delete-access-tokens) ## Revoking User Consent You can revoke a user's consent for all applications they've granted access to: javascript import { Configuration, OAuth2Api } from "@ory/client" const ory = new OAuth2Api( new Configuration({ basePath: `https://${process.env.ORY_PROJECT_SLUG}.<http://projects.oryapis.com|projects.oryapis.com>`, accessToken: process.env.ORY_API_KEY, }), ) export async function revokeConsent() { const { data } = await ory.revokeOAuth2ConsentSessions({ subject: "some-user-id", all: true, }) }
[Revoke user consent](https://www.ory.sh/docs/oauth2-oidc/revoke-consent) ## Implementing OpenID Connect Logout For a more comprehensive logout that handles both front-channel and back-channel logout mechanisms: 1. Implement a custom logout endpoint using the Ory SDK: javascript import { Configuration, OAuth2Api } from "@ory/client" import { Request, Response } from "express" const ory = new OAuth2Api( new Configuration({ basePath: `https://${process.env.ORY_PROJECT_SLUG}.<http://projects.oryapis.com|projects.oryapis.com>`, accessToken: process.env.ORY_API_KEY, }), ) export async function handleLogout(request: Request, response: Response) { const challenge = request.query.logout_challenge.toString() const { data: logoutRequest } = await ory.getOAuth2LogoutRequest({ logoutChallenge: challenge.toString(), }) // Accept the logout request ory .acceptOAuth2LogoutRequest({ logoutChallenge: challenge, }) .then(({ data }) => response.redirect(data.redirect_to)) }
[Implementing the OIDC logout endpoint & UI](https://www.ory.sh/docs/hydra/guides/logout) ## One Active Session Per User If you want to ensure users only have one active session at a time, you can use the revoke_active_sessions
action: ory patch identity-config --project <project-id> --workspace <workspace-id> \ --add '/selfservice/flows/login/after/hooks/0/hook="revoke_active_sessions"'
[One active session per user](https://www.ory.sh/docs/actions/revoke-active-sessions) This action will automatically log out a user from all other active sessions when they log in on a new device. Choose the approach that best fits your security requirements and user experience goals.famous-jackal-2829
06/25/2025, 6:09 AMjson:"logout_url"
LogoutToken string json:"logout_token"
}
func NewOryHandler() *OryHandler {
return &OryHandler{}
}
func (oh *OryHandler) LoginForm(w http.ResponseWriter, r *http.Request) {
redirectURL := os.Getenv("APP_URL") + "/auth/external/openid-connect.corteza"
http.Redirect(w, r, redirectURL, http.StatusFound)
}
func (oh *OryHandler) LogoutProc(w http.ResponseWriter, r *http.Request) {
// Step 1: Perform Kratos logout to get the logout URL
logoutURL, err := url.Parse(kratosPublicURL)
if err != nil {
log.Printf("Invalid KRATOS_PUBLIC_URL: %v", err)
http.Error(w, "Logout configuration error", http.StatusInternalServerError)
return
}
logoutURL.Path = "/self-service/logout/browser"
if finalPostLogoutRedirectURL != "" {
q := logoutURL.Query()
q.Set("return_to", finalPostLogoutRedirectURL)
logoutURL.RawQuery = q.Encode()
}
req, err := http.NewRequest("GET", logoutURL.String(), nil)
if err != nil {
log.Printf("Failed to create logout request: %v", err)
http.Error(w, "Logout service error", http.StatusInternalServerError)
return
}
for _, c := range r.Cookies() {
req.AddCookie(c)
}
req.Header.Set("Accept", "application/json, text/html")
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Do(req)
if err != nil {
log.Printf("Error calling Kratos logout: %v", err)
http.Error(w, "Logout failed", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading Kratos logout response: %v", err)
http.Error(w, "Logout failed", http.StatusInternalServerError)
return
}
if resp.StatusCode != http.StatusOK {
log.Printf("Kratos logout returned status %d: %s", resp.StatusCode, string(body))
http.Error(w, "Logout failed", http.StatusInternalServerError)
return
}
var kratosResp KratosLogoutResponse
if err := json.Unmarshal(body, &kratosResp); err != nil {
log.Printf("Failed to parse Kratos logout response: %v", err)
http.Error(w, "Logout failed", http.StatusInternalServerError)
return
}
if kratosResp.LogoutURL == "" {
log.Printf("Kratos logout response missing logout_url: %s", string(body))
http.Error(w, "Logout failed", http.StatusInternalServerError)
return
}
// Step 2: Call Hydra's end-session endpoint to invalidate OAuth2/OIDC sessions
hydraLogoutURL, err := url.Parse(hydraPublicURL)
if err != nil {
log.Printf("Invalid HYDRA_PUBLIC_URL: %v", err)
http.Error(w, "Logout configuration error", http.StatusInternalServerError)
return
}
hydraLogoutURL.Path = "/oauth2/sessions/logout"
// Optionally, add a post_logout_redirect_uri for Hydra
if finalPostLogoutRedirectURL != "" {
q := hydraLogoutURL.Query()
q.Set("post_logout_redirect_uri", finalPostLogoutRedirectURL)
hydraLogoutURL.RawQuery = q.Encode()
}
hydraReq, err := http.NewRequest("GET", hydraLogoutURL.String(), nil)
if err != nil {
log.Printf("Failed to create Hydra logout request: %v", err)
http.Error(w, "Logout service error", http.StatusInternalServerError)
return
}
// Forward cookies to Hydra
for _, c := range r.Cookies() {
hydraReq.AddCookie(c)
}
hydraResp, err := client.Do(hydraReq)
if err != nil {
log.Printf("Error calling Hydra logout: %v", err)
http.Error(w, "Logout failed", http.StatusInternalServerError)
return
}
defer hydraResp.Body.Close()
if hydraResp.StatusCode != http.StatusOK && hydraResp.StatusCode != http.StatusFound {
hydraBody, _ := io.ReadAll(hydraResp.Body)
log.Printf("Hydra logout returned status %d: %s", hydraResp.StatusCode, string(hydraBody))
http.Error(w, "Logout failed", http.StatusInternalServerError)
return
}
// Step 3: Redirect to Kratos logout URL (which may trigger Hydra redirects)
log.Printf("Redirecting to Kratos logout URL: %s", kratosResp.LogoutURL)
http.Redirect(w, r, kratosResp.LogoutURL, http.StatusSeeOther)
}
func GetBasePath() string {
_, filename, _, _ := runtime.Caller(0)
return filepath.Dir(filename)
}brief-egg-61061
06/25/2025, 6:09 AMbrief-egg-61061
06/25/2025, 6:09 AMgo func (oh *OryHandler) LogoutProc(w http.ResponseWriter, r *http.Request) { // ... existing Kratos logout code ... // Step 2: Call Hydra's end-session endpoint to invalidate OAuth2/OIDC sessions hydraLogoutURL, err := url.Parse(hydraPublicURL) if err != nil { log.Printf("Invalid HYDRA_PUBLIC_URL: %v", err) http.Error(w, "Logout configuration error", http.StatusInternalServerError) return } // Use the OpenID Connect Front-channel logout endpoint // This will trigger both front-channel and back-channel logout hydraLogoutURL.Path = "/oauth2/sessions/logout" // Optionally, add a post_logout_redirect_uri for Hydra if finalPostLogoutRedirectURL != "" { q := hydraLogoutURL.Query() q.Set("post_logout_redirect_uri", finalPostLogoutRedirectURL) hydraLogoutURL.RawQuery = q.Encode() } // Forward all cookies to ensure proper session identification hydraReq, err := http.NewRequest("GET", hydraLogoutURL.String(), nil) if err != nil { log.Printf("Failed to create Hydra logout request: %v", err) http.Error(w, "Logout service error", http.StatusInternalServerError) return } // Forward cookies to Hydra for _, c := range r.Cookies() { hydraReq.AddCookie(c) } // Add ID token hint if available from the request // This helps Hydra identify all related sessions idToken := r.URL.Query().Get("id_token_hint") if idToken != "" { q := hydraReq.URL.Query() q.Set("id_token_hint", idToken) hydraReq.URL.RawQuery = q.Encode() } hydraResp, err := <http://client.Do|client.Do>(hydraReq) if err != nil { log.Printf("Error calling Hydra logout: %v", err) http.Error(w, "Logout failed", http.StatusInternalServerError) return } defer hydraResp.Body.Close() // ... rest of your code ... }
The key points in this implementation: 1. It uses the /oauth2/sessions/logout
endpoint which implements the OpenID Connect Front-channel logout specification [Ory Hydra API](https://raw.githubusercontent.com/ory/hydra/master/spec/api.json). 2. This endpoint handles both front-channel and back-channel logout, which means it will attempt to notify all connected clients about the logout event [Implementing the OIDC logout endpoint & UI](https://www.ory.sh/docs/hydra/guides/logout). 3. By forwarding all cookies and optionally including the ID token hint, you maximize the chance that Hydra can identify all related sessions. For a more comprehensive solution that ensures all sessions are terminated, you might want to consider adding an administrative endpoint that uses the Ory SDK to explicitly revoke all sessions. This would require: 1. Getting the user's ID from the session 2. Using the admin API to revoke all consent sessions for that user However, for most use cases, the OpenID Connect logout endpoint should be sufficient to handle logout across all connected clients.famous-jackal-2829
06/25/2025, 6:11 AMbrief-egg-61061
06/25/2025, 6:11 AMbrief-egg-61061
06/25/2025, 6:12 AMgo package oryhandler import ( "encoding/json" "io" "log" "net/http" "net/url" "os" "path/filepath" "runtime" ) // Configuration loaded from environment variables var ( kratosPublicURL = os.Getenv("KRATOS_PUBLIC_URL") finalPostLogoutRedirectURL = os.Getenv("POST_LOGOUT_REDIRECT_URL") // Add Hydra public URL hydraPublicURL = os.Getenv("HYDRA_PUBLIC_URL") // e.g., "<https://hydra.example.com>" ) type OryHandler struct{} type KratosLogoutResponse struct { LogoutURL string `json:"logout_url"` LogoutToken string `json:"logout_token"` } func NewOryHandler() *OryHandler { return &OryHandler{} } func (oh *OryHandler) LoginForm(w http.ResponseWriter, r *http.Request) { redirectURL := os.Getenv("APP_URL") + "/auth/external/openid-connect.corteza" http.Redirect(w, r, redirectURL, http.StatusFound) } func (oh *OryHandler) LogoutProc(w http.ResponseWriter, r *http.Request) { // Step 1: Perform Kratos logout to get the logout URL logoutURL, err := url.Parse(kratosPublicURL) if err != nil { log.Printf("Invalid KRATOS_PUBLIC_URL: %v", err) http.Error(w, "Logout configuration error", http.StatusInternalServerError) return } logoutURL.Path = "/self-service/logout/browser" if finalPostLogoutRedirectURL != "" { q := logoutURL.Query() q.Set("return_to", finalPostLogoutRedirectURL) logoutURL.RawQuery = q.Encode() } req, err := http.NewRequest("GET", logoutURL.String(), nil) if err != nil { log.Printf("Failed to create logout request: %v", err) http.Error(w, "Logout service error", http.StatusInternalServerError) return } for _, c := range r.Cookies() { req.AddCookie(c) } req.Header.Set("Accept", "application/json, text/html") client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } resp, err := <http://client.Do|client.Do>(req) if err != nil { log.Printf("Error calling Kratos logout: %v", err) http.Error(w, "Logout failed", http.StatusInternalServerError) return } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { log.Printf("Error reading Kratos logout response: %v", err) http.Error(w, "Logout failed", http.StatusInternalServerError) return } if resp.StatusCode != http.StatusOK { log.Printf("Kratos logout returned status %d: %s", resp.StatusCode, string(body)) http.Error(w, "Logout failed", http.StatusInternalServerError) return } var kratosResp KratosLogoutResponse if err := json.Unmarshal(body, &kratosResp); err != nil { log.Printf("Failed to parse Kratos logout response: %v", err) http.Error(w, "Logout failed", http.StatusInternalServerError) return } if kratosResp.LogoutURL == "" { log.Printf("Kratos logout response missing logout_url: %s", string(body)) http.Error(w, "Logout failed", http.StatusInternalServerError) return } // Step 2: Call Hydra's end-session endpoint to invalidate OAuth2/OIDC sessions hydraLogoutURL, err := url.Parse(hydraPublicURL) if err != nil { log.Printf("Invalid HYDRA_PUBLIC_URL: %v", err) http.Error(w, "Logout configuration error", http.StatusInternalServerError) return } // Use the OpenID Connect Front-channel logout endpoint // This will trigger both front-channel and back-channel logout hydraLogoutURL.Path = "/oauth2/sessions/logout" // Optionally, add a post_logout_redirect_uri for Hydra if finalPostLogoutRedirectURL != "" { q := hydraLogoutURL.Query() q.Set("post_logout_redirect_uri", finalPostLogoutRedirectURL) hydraLogoutURL.RawQuery = q.Encode() } // Forward all cookies to ensure proper session identification hydraReq, err := http.NewRequest("GET", hydraLogoutURL.String(), nil) if err != nil { log.Printf("Failed to create Hydra logout request: %v", err) http.Error(w, "Logout service error", http.StatusInternalServerError) return } // Forward cookies to Hydra for _, c := range r.Cookies() { hydraReq.AddCookie(c) } // Add ID token hint if available from the request // This helps Hydra identify all related sessions even when "Remember Me" is off idToken := r.URL.Query().Get("id_token_hint") if idToken != "" { q := hydraReq.URL.Query() q.Set("id_token_hint", idToken) hydraReq.URL.RawQuery = q.Encode() } // Add state parameter if needed for tracking the logout request state := r.URL.Query().Get("state") if state != "" { q := hydraReq.URL.Query() q.Set("state", state) hydraReq.URL.RawQuery = q.Encode() } hydraResp, err := <http://client.Do|client.Do>(hydraReq) if err != nil { log.Printf("Error calling Hydra logout: %v", err) http.Error(w, "Logout failed", http.StatusInternalServerError) return } defer hydraResp.Body.Close() if hydraResp.StatusCode != http.StatusOK && hydraResp.StatusCode != http.StatusFound { hydraBody, _ := io.ReadAll(hydraResp.Body) log.Printf("Hydra logout returned status %d: %s", hydraResp.StatusCode, string(hydraBody)) http.Error(w, "Logout failed", http.StatusInternalServerError) return } // Step 3: Redirect to Kratos logout URL (which may trigger Hydra redirects) log.Printf("Redirecting to Kratos logout URL: %s", kratosResp.LogoutURL) http.Redirect(w, r, kratosResp.LogoutURL, http.StatusSeeOther) } func GetBasePath() string { _, filename, _, _ := runtime.Caller(0) return filepath.Dir(filename) }
This implementation: 1. First performs a Kratos logout to get the logout URL 2. Then calls Hydra's OpenID Connect logout endpoint (/oauth2/sessions/logout
) which handles both front-channel and back-channel logout [OpenID Connect Front- and Back-channel Enabled Logout](https://raw.githubusercontent.com/ory/hydra/master/spec/api.json) 3. Forwards all cookies and includes the ID token hint if available, which helps Hydra identify all related sessions even when "Remember Me" is off 4. Adds a state parameter if needed for tracking the logout request 5. Finally redirects to the Kratos logout URL This implementation should ensure that users are properly logged out from all clients in Hydra, as it leverages the OpenID Connect Front-Channel and Back-Channel logout specifications.famous-jackal-2829
06/25/2025, 6:25 AMbrief-egg-61061
06/25/2025, 6:25 AMbrief-egg-61061
06/25/2025, 6:25 AMid_token_hint
, logout might not work properly when "Remember Me" is off. 2. Cookie Persistence: Hydra maintains a single session cookie that gets replaced with new logins, as discussed in [this GitHub discussion](https://github.com/ory/network/discussions/3823). 3. Login Challenge Issues: There might be issues with login challenges when previous OIDC flows for the same client are not completed, as noted in [this GitHub issue](https://github.com/ory/kratos/issues/4024). ## Solutions To properly log out from all clients and ensure a clean session for the new user, you need to: 1. Revoke OAuth2 Consent Sessions: Add code to explicitly revoke consent sessions for the user: go // Add this before redirecting to the Kratos logout URL // You'll need to extract the user ID from the session func revokeAllConsentSessions(userID string) error { adminURL, err := url.Parse(hydraAdminURL) // You'll need to add this env var if err != nil { return err } adminURL.Path = "/admin/oauth2/auth/sessions/consent" q := adminURL.Query() q.Set("subject", userID) q.Set("all", "true") adminURL.RawQuery = q.Encode() req, err := http.NewRequest("DELETE", adminURL.String(), nil) if err != nil { return err } // Add admin authorization if needed // req.Header.Set("Authorization", "Bearer " + adminToken) client := &http.Client{} resp, err := <http://client.Do|client.Do>(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("failed to revoke consent sessions: %d %s", resp.StatusCode, string(body)) } return nil }
2. Revoke Login Sessions: Also revoke the login sessions: go func revokeAllLoginSessions(userID string) error { adminURL, err := url.Parse(hydraAdminURL) if err != nil { return err } adminURL.Path = "/admin/oauth2/auth/sessions/login" q := adminURL.Query() q.Set("subject", userID) adminURL.RawQuery = q.Encode() req, err := http.NewRequest("DELETE", adminURL.String(), nil) if err != nil { return err } // Add admin authorization if needed client := &http.Client{} resp, err := <http://client.Do|client.Do>(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("failed to revoke login sessions: %d %s", resp.StatusCode, string(body)) } return nil }
3. Ensure Proper ID Token Hint: Make sure you're always sending the id_token_hint
parameter in the logout request as recommended in the [OpenID Connect logout documentation](https://www.ory.sh/docs/oauth2-oidc/oidc-logout#send-the-id-token-in-id_token_hint). 4. Session Cookie Settings: Check if your Kratos configuration has session.cookie.persistent
set correctly. According to [this GitHub issue](https://github.com/ory/network/issues/194), setting this to false
can cause problems with session management. 5. Clear Browser Cookies: As a fallback, you might want to add JavaScript to your frontend that clears relevant cookies when a user logs out. Remember that OAuth2/OIDC was not designed for global logout across all applications, as mentioned in the [Ory blog post](https://www.ory.sh/blog/oauth2-openid-connect-do-you-need-use-cases-examples). Each application maintains its own session, which is why implementing a comprehensive logout solution requires additional steps beyond the standard protocol. If these solutions don't resolve your issue, you might need to investigate how your specific OIDC clients (Dolibarr, Guacamole) handle sessions and whether they have their own session persistence mechanisms that need to be addressed.famous-jackal-2829
06/25/2025, 6:41 AMbrief-egg-61061
06/25/2025, 6:41 AMbrief-egg-61061
06/25/2025, 6:41 AMsubject
query parameter to identify the user, which might not be available in the logout request. To properly implement this, you need a reliable way to get the user ID. Here are some suggestions to improve your implementation: 1. Extract the user ID from the session token: Instead of relying on a query parameter, you should extract the user ID from the session token: go // Add this function to extract user ID from session func getUserIDFromSession(r *http.Request) (string, error) { // Get session cookie cookie, err := r.Cookie("ory_kratos_session") if err != nil { return "", err } // Call Kratos to get session info req, err := http.NewRequest("GET", kratosPublicURL+"/sessions/whoami", nil) if err != nil { return "", err } req.Header.Set("Cookie", fmt.Sprintf("ory_kratos_session=%s", cookie.Value)) client := &http.Client{} resp, err := <http://client.Do|client.Do>(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("failed to get session info: %d", resp.StatusCode) } var session struct { Identity struct { ID string `json:"id"` } `json:"identity"` } if err := json.NewDecoder(resp.Body).Decode(&session); err != nil { return "", err } return session.Identity.ID, nil }
2. Implement the revocation functions correctly: Make sure your revokeAllConsentSessions
and revokeAllLoginSessions
functions are properly implemented as shown in my previous response. 3. Use the Hydra Admin API: Ensure you're using the Hydra Admin API (not the public API) for revoking sessions: go func revokeAllConsentSessions(userID string) error { adminURL, err := url.Parse(os.Getenv("HYDRA_ADMIN_URL")) // Make sure this is set if err != nil { return err } adminURL.Path = "/admin/oauth2/auth/sessions/consent" q := adminURL.Query() q.Set("subject", userID) q.Set("all", "true") adminURL.RawQuery = q.Encode() req, err := http.NewRequest("DELETE", adminURL.String(), nil) if err != nil { return err } client := &http.Client{} resp, err := <http://client.Do|client.Do>(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("failed to revoke consent sessions: %d %s", resp.StatusCode, string(body)) } return nil }
The key improvements needed are: 1. Get the user ID from the session rather than expecting it as a query parameter 2. Use the Hydra Admin API (not the public API) for revoking sessions 3. Properly handle errors and responses According to the [Ory API Reference](https://raw.githubusercontent.com/ory/docs/master/docs/reference/api.json), the endpoints for revoking consent and login sessions are: - /admin/oauth2/auth/sessions/consent
for consent sessions - /admin/oauth2/auth/sessions/login
for login sessions Both require the subject
parameter to identify the user whose sessions should be revoked.famous-jackal-2829
06/25/2025, 6:45 AMbrief-egg-61061
06/25/2025, 6:45 AMbrief-egg-61061
06/25/2025, 6:45 AMgetUserIDFromSession()
but immediately returning with an error response. This might prevent the logout from completing if there's an issue getting the user ID. Let's refine this a bit: go // If Hydra logout was successful, try to revoke all consent and login sessions userID, err := getUserIDFromSession(r) if err != nil { // Log the error but continue with the logout process log.Printf("Failed to get user ID from session: %v", err) // Try to get user ID from query parameter as fallback userID = r.URL.Query().Get("subject") } // If we have a user ID, revoke all consent and login sessions if userID != "" { log.Printf("Revoking sessions for user ID: %s", userID) if err := revokeAllConsentSessions(userID); err != nil { log.Printf("Failed to revoke consent sessions for user %s: %v", userID, err) } else { log.Printf("Revoked all consent sessions for user %s", userID) } if err := revokeAllLoginSessions(userID); err != nil { log.Printf("Failed to revoke login sessions for user %s: %v", userID, err) } else { log.Printf("Revoked all login sessions for user %s", userID) } } else { log.Printf("No user ID available for session revocation") } // Always proceed with the Kratos logout URL redirect log.Printf("Redirecting to Kratos logout URL: %s", kratosResp.LogoutURL) http.Redirect(w, r, kratosResp.LogoutURL, http.StatusSeeOther)
This approach: 1. Tries to get the user ID from the session 2. If that fails, falls back to checking for a subject query parameter 3. Only attempts to revoke sessions if a user ID is available 4. Always proceeds with the Kratos logout redirect, even if session revocation fails This ensures that the logout process completes even if there are issues with the session revocation, which is important for user experience. Make sure your getUserIDFromSession()
function is properly implemented as described in my previous response, and that your revokeAllConsentSessions()
and revokeAllLoginSessions()
functions are using the Hydra Admin API correctly.famous-jackal-2829
06/25/2025, 10:39 AMbrief-egg-61061
06/25/2025, 10:39 AMbrief-egg-61061
06/25/2025, 10:40 AM/admin/oauth2/auth/sessions/consent
) will revoke all access and refresh tokens for the user, but it does not invalidate the user's authentication session (the login session/cookie). This means that unless the authentication session is also revoked, the user may still be considered logged in by Hydra or other OIDC clients after logout. The documentation explicitly states: > Revoking a user's consent automatically revokes all related access and refresh tokens. Don't use this method to invalidate user sessions. [Revoke user consent](https://www.ory.sh/docs/oauth2-oidc/revoke-consent) 2. Login Session Revocation The /admin/oauth2/auth/sessions/login
endpoint can be used to revoke authentication sessions by subject (user ID). However, the documentation notes: > If you send the subject in a query param, all authentication sessions that belong to that subject are revoked. No OpenID Connect Front- or Back-channel logout is performed in this case. [Hydra API Reference](https://raw.githubusercontent.com/ory/hydra/master/spec/api.json) This means that even after revoking login sessions, OIDC clients may not be notified via front- or back-channel logout, and their own sessions may persist. 3. OIDC Logout Endpoint Limitations The /oauth2/sessions/logout
endpoint in Hydra is designed to trigger front- and back-channel logout for OIDC clients. However, if there is no active authentication session (e.g., no valid session cookie), Hydra will immediately redirect to the post-logout redirect URI without triggering client logouts. This is documented here: > If no active authentication session is set at Ory OAuth2 & OpenID Connect, the browser will be redirected immediately to the system-wide configured post logout redirect URI. [Hydra Debug: Logout](https://www.ory.sh/docs/hydra/debug/logout) Even if you provide an id_token_hint
, if the session is not active or the session cookie does not match, Hydra may not perform the expected logout actions for all clients. This is a known issue and has been discussed in the community: [Hydra Issue #3132](https://github.com/ory/hydra/issues/3132) 4. Browser and Client-Side Sessions Some OIDC clients (like Dolibarr, Guacamole) may maintain their own sessions independently of Hydra. Logging out from Hydra does not guarantee that these applications will clear their own sessions unless they properly implement and respond to OIDC front- or back-channel logout notifications. --- Summary: Based on the official documentation and community discussions, there are limitations in Hydra's ability to fully log out a user from all OIDC clients in all scenarios, especially if the session is not active or if clients do not handle logout notifications correctly. The knowledge sources do not provide a guaranteed method to force logout from all clients in every case. This is a known challenge with OAuth2/OIDC and is not unique to Ory Hydra. If you need a more robust solution, you may need to coordinate logout flows at the application level for each OIDC client, or ensure that all clients properly implement and respond to OIDC logout mechanisms. If you have followed all the recommended steps and it still does not work, it is likely due to these protocol and implementation limitations, as described in the official documentation and issues.