# OpenID Connect Backchannel Logout ## Overview Backchannel logout is an OpenID Connect feature that enables Clinch to notify applications when a user logs out, ensuring sessions are terminated across all connected applications immediately. ## How It Works When a user logs out from Clinch (or any connected application), Clinch sends server-to-server HTTP POST requests to all applications that have configured a backchannel logout endpoint. This happens automatically in the background. ### Logout Triggers Backchannel logout notifications are sent when: 1. **User clicks "Sign Out" in Clinch** - All connected OIDC applications are notified, then the Clinch session is terminated 2. **User logs out via OIDC `/logout` endpoint** (RP-Initiated Logout) - All connected applications are notified, then the Clinch session is terminated 3. **User clicks "Logout" on an app (Dashboard)** - Backchannel logout is sent to that app, all access/refresh tokens are revoked, but OAuth consent is preserved (user can sign back in without re-authorizing) 4. **User clicks "Revoke Access" for a specific app (Active Sessions page)** - Backchannel logout is sent to that app to terminate its session, all access/refresh tokens are revoked, then the OAuth consent is permanently destroyed (user must re-authorize the app to use it again) 5. **User clicks "Revoke All App Access"** - All connected applications receive backchannel logout notifications, all tokens are revoked, then all OAuth consents are permanently destroyed ### The Logout Flow ``` User logs out → Clinch finds all connected apps ↓ For each app with backchannel_logout_uri: ↓ Generate signed JWT logout token ↓ HTTP POST to app's logout endpoint ↓ App validates JWT and terminates session ↓ Clinch revokes access and refresh tokens ``` ### Logout vs Revoke Access Clinch provides two distinct actions for managing application access: | Action | Location | What Happens | When to Use | |--------|----------|--------------|-------------| | **Logout** | Dashboard | • Sends backchannel logout to app
• Revokes all access tokens
• Revokes all refresh tokens
• **Keeps OAuth consent intact** | You want to end your session with an app but still trust it. Next login will skip the authorization screen. | | **Revoke Access** | Active Sessions page | • Sends backchannel logout to app
• Revokes all access tokens
• Revokes all refresh tokens
• **Destroys OAuth consent** | You want to completely de-authorize an app. Next login will require you to re-authorize the app. | **Key Difference**: "Logout" preserves the authorization relationship while terminating the active session. "Revoke Access" completely removes the app's authorization to access your account. **Example Use Cases**: - **Logout**: "I left my Jellyfin session open at a friend's house. I want to kill that session but I still use Jellyfin." - **Revoke Access**: "I no longer trust this app and want to remove its authorization completely." **Technical Details**: - Both actions revoke access tokens (opaque, database-backed, validated on each use) - Both actions revoke refresh tokens (prevents obtaining new access tokens) - ID tokens remain valid until expiry (stateless JWTs), but apps should honor backchannel logout - Backchannel logout ensures the app clears its local session immediately ## Configuring Applications ### In Clinch Admin UI 1. Navigate to **Admin → Applications** 2. Edit or create an OIDC application 3. In the "Backchannel Logout URI" field, enter the application's logout endpoint - Example: `https://kavita.local/oidc/backchannel-logout` - Must be HTTPS in production - Leave blank if the application doesn't support backchannel logout ### Checking Support The OIDC discovery endpoint advertises backchannel logout support: ```bash curl https://clinch.local/.well-known/openid-configuration | jq ``` Look for: ```json { "backchannel_logout_supported": true, "backchannel_logout_session_supported": true } ``` ## Implementing a Backchannel Logout Endpoint (for RPs) If you're developing an application that integrates with Clinch, here's how to implement backchannel logout support: ### 1. Create the Endpoint The endpoint must: - Accept HTTP POST requests - Parse the `logout_token` parameter from the form body - Validate the JWT signature - Terminate the user's session - Return 200 OK quickly (within 5 seconds) ### 2. Example Implementation (Ruby/Rails) ```ruby # config/routes.rb post '/oidc/backchannel-logout', to: 'oidc_backchannel_logout#logout' # app/controllers/oidc_backchannel_logout_controller.rb class OidcBackchannelLogoutController < ApplicationController skip_before_action :verify_authenticity_token # Server-to-server call skip_before_action :authenticate_user! # No user session yet def logout logout_token = params[:logout_token] unless logout_token.present? head :bad_request return end begin # Decode and verify the JWT # Get Clinch's public key from JWKS endpoint jwks = fetch_clinch_jwks decoded = JWT.decode( logout_token, nil, # Will be verified using JWKS true, { algorithms: ['RS256'], jwks: jwks, verify_aud: true, aud: YOUR_CLIENT_ID, verify_iss: true, iss: 'https://clinch.local' # Your Clinch URL } ) claims = decoded.first # Validate required claims unless claims['events']&.key?('http://schemas.openid.net/event/backchannel-logout') head :bad_request return end # Get session ID from the token sid = claims['sid'] sub = claims['sub'] # Terminate sessions if sid.present? # Terminate specific session by SID (recommended) Session.where(oidc_sid: sid).destroy_all elsif sub.present? # Terminate all sessions for this user user = User.find_by(oidc_sub: sub) user&.sessions&.destroy_all end Rails.logger.info "Backchannel logout: Terminated session for sid=#{sid}, sub=#{sub}" head :ok rescue JWT::DecodeError => e Rails.logger.error "Backchannel logout: Invalid JWT - #{e.message}" head :bad_request rescue => e Rails.logger.error "Backchannel logout: Error - #{e.class}: #{e.message}" head :internal_server_error end end private def fetch_clinch_jwks # Cache this in production! response = HTTParty.get('https://clinch.local/.well-known/jwks.json') JSON.parse(response.body, symbolize_names: true) end end ``` ### 3. Required JWT Claims Validation The logout token will contain: | Claim | Description | Required | |-------|-------------|----------| | `iss` | Issuer (Clinch URL) | Yes | | `aud` | Your application's client_id | Yes | | `iat` | Issued at timestamp | Yes | | `jti` | Unique token ID | Yes | | `sub` | Pairwise subject identifier (user's SID) | Yes | | `sid` | Session ID (same as sub) | Yes | | `events` | Must contain `http://schemas.openid.net/event/backchannel-logout` | Yes | | `nonce` | Must NOT be present (spec requirement) | No | ### 4. Session Tracking Requirements To support backchannel logout, your application must: 1. **Store the `sid` claim from ID tokens**: ```ruby # When user logs in via OIDC id_token = decode_id_token(params[:id_token]) session[:oidc_sid] = id_token['sid'] # Store this! ``` 2. **Associate sessions with SID**: ```ruby # Create session with SID tracking Session.create!( user: current_user, oidc_sid: id_token['sid'], ... ) ``` 3. **Terminate sessions by SID**: ```ruby # When backchannel logout is received Session.where(oidc_sid: sid).destroy_all ``` ### 5. Testing Your Endpoint Test with curl: ```bash # Get a valid logout token (you'll need to capture this from Clinch logs) LOGOUT_TOKEN="eyJhbGc..." curl -X POST https://your-app.local/oidc/backchannel-logout \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "logout_token=$LOGOUT_TOKEN" ``` Expected response: `200 OK` (empty body) ## Monitoring and Troubleshooting ### Checking Logs Clinch logs all backchannel logout attempts: ```bash # In development tail -f log/development.log | grep BackchannelLogout # Example log output: # BackchannelLogout: Successfully sent logout notification to Kavita (https://kavita.local/oidc/backchannel-logout) # BackchannelLogout: Application Jellyfin doesn't support backchannel logout # BackchannelLogout: Timeout sending logout to HomeAssistant (https://ha.local/logout): Connection timeout ``` ### Common Issues **1. HTTP Timeout** - Symptom: `Timeout sending logout to...` in logs - Solution: Ensure the RP's backchannel logout endpoint responds within 5 seconds - Note: Clinch will retry 3 times with exponential backoff **2. HTTP Errors (Non-200 Status)** - Symptom: `Application X returned HTTP 400/500...` in logs - Solution: Check the RP's logs for JWT validation errors - Common causes: - Wrong JWKS (public key mismatch) - Incorrect `aud` (client_id) validation - Missing required claims validation **3. Network Unreachable** - Symptom: `Failed to send logout to...` with connection errors - Solution: Ensure the RP's logout endpoint is accessible from Clinch server - Check: Firewalls, DNS, SSL certificates **4. Sessions Not Terminating** - Symptom: User still logged into RP after logging out of Clinch - Solution: Verify the RP is storing and checking `sid` correctly - Debug: Add logging to the RP's backchannel logout handler ### Verification Checklist For RPs (Application Developers): - [ ] Endpoint accepts POST requests - [ ] Endpoint validates JWT signature using Clinch's JWKS - [ ] Endpoint validates all required claims - [ ] Endpoint terminates sessions by SID - [ ] Endpoint returns 200 OK quickly (< 5 seconds) - [ ] Sessions store the `sid` claim from ID tokens - [ ] Backchannel logout URI is configured in Clinch admin For Administrators: - [ ] Application has `backchannel_logout_uri` configured - [ ] URI uses HTTPS (in production) - [ ] URI is reachable from Clinch server - [ ] Check logs for successful logout notifications ## Security Considerations 1. **JWT Signature Verification**: Always verify the logout token signature using Clinch's public key 2. **Audience Validation**: Ensure the `aud` claim matches your client_id 3. **Issuer Validation**: Ensure the `iss` claim matches your Clinch URL 4. **No Authentication Required**: The endpoint should not require user authentication (it's server-to-server) 5. **HTTPS Only**: Always use HTTPS in production (Clinch enforces this) 6. **Fire-and-Forget**: RPs should log failures but not block on errors ## Comparison with Other Logout Methods | Method | Communication | When Sessions Terminate | Reliability | |--------|--------------|------------------------|-------------| | **Backchannel Logout** | Server-to-server POST | Immediately | High (retries on failure) | | **Front-Channel Logout** | Browser iframes | When browser loads iframes | Low (blocked by privacy settings) | | **RP-Initiated Logout** | User redirects to Clinch | Only affects Clinch session | N/A (just triggers other methods) | | **Token Expiry** | None | When access token expires | Guaranteed but delayed | ## References - [OpenID Connect Back-Channel Logout 1.0](https://openid.net/specs/openid-connect-backchannel-1_0.html) - [RFC 7009: OAuth 2.0 Token Revocation](https://tools.ietf.org/html/rfc7009) - [Clinch OIDC Discovery](/.well-known/openid-configuration)