OIDC Back-Channel Logout — Server-to-Server Session Termination
In this tutorial, you will learn about OIDC Back. We cover key concepts, practical examples, and best practices to help you master this topic.
Back-channel logout in Openid Connect sends a direct HTTP POST request from the provider to each application's logout endpoint when a session ends, terminating sessions server-to-server without requiring browser iframes.
What You'll Learn
- How back-channel logout differs from front-channel logout
- How to validate the logout token sent by the provider
- When to use back-channel vs front-channel logout
Why It Matters
Front-channel logout requires a browser to load iframes. Mobile apps, background services, and server-side applications cannot participate. Back-channel logout notifies these applications directly via server-to-server HTTP requests, ensuring complete logout coverage.
Real-World Use
DodaTech's backend notification service maintains its own session with the OIDC provider. When the user logs out, the provider sends a back-channel logout request to the notification service, which immediately stops sending notifications and revokes the service's access token.
sequenceDiagram
participant User
participant Provider as OIDC Provider
participant WebApp as DodaMail
participant Service as Notification Service
User->>Provider: Logout
Provider->>WebApp: Front-Channel (iframe)
Note over Provider,Service: Also send back-channel
Provider->>Service: POST /backchannel_logout
Note over Service: Validate logout_token
Service-->>Provider: 200 OK
Note over Service: Revoke tokens, stop processing
Implementing Back-Channel Logout Endpoint
from flask import Flask, request, jsonify
import jwt
import requests
app = Flask(__name__)
BACKCHANNEL_LOGOUT_ENDPOINT = "/backchannel_logout"
PROVIDER_ISSUER = "https://accounts.example.com"
CLIENT_ID = "doda-notification-service"
@app.route(BACKCHANNEL_LOGOUT_ENDPOINT, methods=['POST'])
def backchannel_logout():
logout_token = request.form.get('logout_token')
if not logout_token:
return jsonify({"error": "missing_logout_token"}), 400
# Fetch provider's JWKS for token validation
jwks_uri = f"{PROVIDER_ISSUER}/.well-known/jwks.json"
jwks = requests.get(jwks_uri).json()
try:
header = jwt.get_unverified_header(logout_token)
key = next(k for k in jwks["keys"] if k["kid"] == header["kid"])
payload = jwt.decode(
logout_token,
key,
algorithms=["RS256"],
issuer=PROVIDER_ISSUER,
audience=CLIENT_ID
)
except jwt.InvalidTokenError as e:
app.logger.error(f"Invalid logout token: {e}")
return jsonify({"error": "invalid_token"}), 400
# Extract user and session info
subject = payload.get('sub')
session_id = payload.get('sid')
events = payload.get('events', {})
# Verify this is a logout token
if 'http://schemas.openid.net/event/backchannel-logout' not in events:
app.logger.error("Not a logout token")
return jsonify({"error": "not_logout_token"}), 400
# Terminate the session
terminate_session(subject, session_id)
app.logger.info(f"Logout processed for user {subject}")
return jsonify({"status": "ok"}), 200
Logout Token Validation
def validate_logout_token(logout_token, jwks_uri, expected_issuer, expected_audience):
"""Validate a back-channel logout token"""
jwks = requests.get(jwks_uri).json()
try:
header = jwt.get_unverified_header(logout_token)
matching_keys = [k for k in jwks["keys"] if k["kid"] == header.get("kid")]
if not matching_keys:
raise ValueError("No matching key found")
payload = jwt.decode(
logout_token,
matching_keys[0],
algorithms=["RS256"],
issuer=expected_issuer,
audience=expected_audience,
options={"require": ["sub", "sid", "events", "iat"]}
)
# Verify events claim
events = payload.get('events', {})
logout_event = 'http://schemas.openid.net/event/backchannel-logout'
if logout_event not in events:
raise ValueError("Missing backchannel-logout event")
# Verify token hasn't been used (replay prevention)
if payload.get('jti') in USED_TOKEN_IDS:
raise ValueError("Token has already been used")
USED_TOKEN_IDS.add(payload.get('jti'))
return payload
except Exception as e:
app.logger.error(f"Logout token validation failed: {e}")
raise
Session Termination
def terminate_session(subject, session_id):
"""Terminate all session data for the given user"""
# Revoke refresh tokens
conn = get_redis_connection()
refresh_token_key = f"refresh_token:{subject}:{session_id}"
stored_token = conn.get(refresh_token_key)
if stored_token:
conn.delete(refresh_token_key)
# Invalidate session cache
session_key = f"session:{subject}:{session_id}"
conn.delete(session_key)
# Log termination
app.logger.info(f"Session terminated: user={subject}, session={session_id}")
Common Mistakes
1. Not Validating the Logout Event Type
The logout token has a specific events claim. Without validating it, non-logout tokens could trigger session termination.
2. Missing Replay Prevention
The logout token contains a jti (JWT ID) claim. Track used jti values to prevent replay attacks.
3. Forgetting to Verify the Audience
The aud claim must match your application's client_id. Otherwise, a logout token intended for another app could terminate your sessions.
4. Returning Non-200 Responses
The provider expects a 200 OK response. Returning an error may cause the provider to retry or consider the logout incomplete.
5. Not Handling Idempotency
The provider may send the same logout token multiple times (network retries). Handle this with idempotency based on the jti claim.
Practice Questions
- How does back-channel logout differ from front-channel logout?
- What claims are required in a logout token?
- How do you prevent logout token replay attacks?
- Why might you need both front-channel and back-channel logout?
- What should your endpoint return on successful logout?
Answers
- Back-channel is server-to-server; front-channel uses browser iframes. 2.
sub,sid,events,iat,jti. 3. Track and validate thejti(JWT ID) claim. 4. Front-channel covers browser-based apps; back-channel covers server-side services and mobile apps. 5. HTTP 200 OK with a JSON body.
Challenge
Build a back-channel logout receiver that handles logout tokens from multiple providers, validates each token, terminates the correct sessions, and returns appropriate error codes for invalid tokens with detailed logging.
FAQ
Mini Project
Create a complete logout system with both front-channel and back-channel: a provider simulation that sends both types of logout, a server-side service that processes back-channel logout tokens, a web app that handles front-channel iframes, and a monitoring dashboard showing all received logouts.
What's Next
- Learn about post-logout redirect URIs for the user experience after logout
- Explore claims requests for fine-grained attribute control
- Continue to distributed and aggregated claims for multi-source identity data
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro