Fix tests. Remove tests which test rails functionality
This commit is contained in:
316
docs/backchannel-logout.md
Normal file
316
docs/backchannel-logout.md
Normal file
@@ -0,0 +1,316 @@
|
||||
# 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<br>• Revokes all access tokens<br>• Revokes all refresh tokens<br>• **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<br>• Revokes all access tokens<br>• Revokes all refresh tokens<br>• **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)
|
||||
Reference in New Issue
Block a user