OAuth2 Back-Channel Logout — RFC 7009 Session Termination Across Providers
In this tutorial, you will learn about OAuth2 Back. We cover key concepts, practical examples, and best practices to help you master this topic.
OAuth2 back-channel logout enables central session termination from the authorization server to all relying parties, ensuring that when a user logs out from one application, their session is terminated everywhere.
What You'll Learn
- RP-initiated vs OP-initiated logout
- Openid Connect session management
- Back-channel logout requests
- Logout tokens and event claims
- Handling logout failures and retries
Why It Matters
Without centralized logout, users must log out of every application individually. Back-channel logout ensures single logout (SLO) across all services. DodaTech's session management broadcasts logout events to 20+ services within 2 seconds, preventing session persistence after logout.
sequenceDiagram
participant User
participant OP as Authorization Server
participant RP1 as Dashboard App
participant RP2 as Threat App
participant RP3 as Report App
User->>OP: Initiate logout
OP->>OP: Invalidate session
OP->>RP1: POST back-channel logout (logout_token)
OP->>RP2: POST back-channel logout (logout_token)
OP->>RP3: POST back-channel logout (logout_token)
RP1->>RP1: Clear local session
RP2->>RP2: Clear local session
RP3->>RP3: Clear local session
RP1-->>OP: 200 OK
RP2-->>OP: 200 OK
RP3-->>OP: 200 OK
OP-->>User: Logout complete
Code Examples
Example 1: Generating Logout Tokens
import jwt
from datetime import datetime, timedelta, timezone
def create_logout_token(session_id, user_id, issuer, private_key,
events=None, sid=None):
"""Create a logout token per OpenID Connect Back-Channel Logout spec."""
if events is None:
events = {
'http://schemas.openid.net/event/backchannel-logout': {}
}
now = datetime.now(timezone.utc)
payload = {
'iss': issuer,
'sub': user_id,
'aud': issuer, # Each RP will verify with its own client_id
'iat': now,
'exp': now + timedelta(minutes=5),
'jti': str(uuid.uuid4()),
'events': events,
'sid': sid or session_id
}
token = jwt.encode(payload, private_key, algorithm='RS256')
print(f"Logout token issued for session {session_id[:8]}...")
return token
# Usage
logout_token = create_logout_token(
session_id='sess_abc123',
user_id='user_456',
issuer='https://auth.dodatech.com',
private_key=PRIVATE_KEY
)
Example 2: Back-Channel Logout Endpoint
from flask import Flask, request, jsonify
import jwt
from jwt import PyJWKClient
app = Flask(__name__)
jwks_client = PyJWKClient('https://auth.dodatech.com/.well-known/jwks.json')
@app.route('/backchannel-logout', methods=['POST'])
def backchannel_logout():
"""Receive back-channel logout request from the OP."""
logout_token = request.form.get('logout_token')
if not logout_token:
return jsonify({'error': 'missing_logout_token'}), 400
try:
# Validate logout token
signing_key = jwks_client.get_signing_key_from_jwt(logout_token)
payload = jwt.decode(
logout_token,
signing_key.key,
algorithms=['RS256'],
issuer='https://auth.dodatech.com',
audience=CLIENT_ID,
options={'require': ['events', 'sub', 'sid', 'iat']}
)
# Verify it's a logout event
events = payload.get('events', {})
if 'http://schemas.openid.net/event/backchannel-logout' not in events:
return jsonify({'error': 'invalid_logout_event'}), 400
# Terminate local session
session_id = payload['sid']
user_id = payload['sub']
if terminate_local_session(session_id, user_id):
print(f"Session terminated: user={user_id}, session={session_id[:8]}...")
return jsonify({'result': 'ok'}), 200
else:
# Session not found — still return 200 per spec
return jsonify({'result': 'ok'}), 200
except jwt.ExpiredSignatureError:
return jsonify({'error': 'expired_logout_token'}), 400
except Exception as e:
print(f"Logout token validation failed: {e}")
return jsonify({'error': 'invalid_logout_token'}), 400
def terminate_local_session(session_id, user_id):
"""Terminate the local session. Return False if session not found."""
session = find_local_session(session_id, user_id)
if session:
delete_local_session(session_id)
# Invalidate all tokens issued for this session
invalidate_session_tokens(session_id)
return True
return False
Example 3: RP-Initiated Logout
@app.route('/logout')
def rp_initiated_logout():
"""Handle RP-initiated logout per OpenID Connect RP-Initiated Logout."""
id_token_hint = request.args.get('id_token_hint')
post_logout_redirect_uri = request.args.get('post_logout_redirect_uri')
state = request.args.get('state')
# Validate the logout request
if id_token_hint:
try:
payload = validate_id_token(id_token_hint)
user_id = payload['sub']
session_id = payload.get('sid')
# Clear local session
clear_user_session(user_id)
# Redirect to OP for global logout
op_logout_url = build_op_logout_url(
id_token_hint,
post_logout_redirect_uri,
state
)
return redirect(op_logout_url)
except Exception as e:
print(f"Invalid id_token_hint: {e}")
# Simple local logout
clear_session(request)
return redirect('/goodbye')
def build_op_logout_url(id_token_hint, post_logout_redirect_uri, state):
"""Build logout URL to send the user to the OP for global logout."""
params = {
'id_token_hint': id_token_hint,
'post_logout_redirect_uri': post_logout_redirect_uri or
'https://dashboard.dodatech.com/logged-out',
'state': state or secrets.token_urlsafe(16)
}
query = urlencode(params)
return f"https://auth.dodatech.com/logout?{query}"
Common Mistakes
1. Not Validating the Logout Token as a JWT
Logout tokens are JWTs and must be validated for signature, issuer, audience, and expiry.
2. Returning Error for Unknown Sessions
Always return 200 OK even if the session is not found. Otherwise, attackers can enumerate valid sessions.
3. Ignoring the events Claim
The events claim distinguishes logout tokens from regular JWTs. Verify the correct event URI.
4. Not Retrying Failed Logout Delivery
Back-channel logout uses HTTP. Network failures happen. Implement retries with exponential backoff.
5. Missing sid Claim
The session ID (sid) ties the logout token to a specific session. Without it, you may terminate the wrong session.
Practice Questions
- What is the difference between front-channel and back-channel logout?
- What is a logout token?
- Why must logout tokens have a short expiry?
- How do you handle a logout request for an already-terminated session?
- What is RP-initiated logout?
Answers:
- Front-channel uses browser redirects through iframes. Back-channel uses server-to-server HTTP POST requests.
- A JWT containing an
eventsclaim with the back-channel logout event URI, signed by the OP. - Short expiry prevents replay attacks. Logout tokens are sensitive — they instruct RPs to terminate sessions.
- Return 200 OK. The session is already terminated, so the desired state is achieved.
- The relying party initiates logout for a user and redirects them to the OP for global logout across all RPs.
Challenge: Build a back-channel logout system with an OP that broadcasts logout tokens to multiple RPs, retrying failed deliveries. Each RP must validate logout tokens and terminate sessions on receipt.
FAQ
What's Next
Combine back-channel logout with {{< ilink "OAuth" "OAuth2 Token Revocation" }} for complete session termination, or explore {{< ilink "OAuth" "OAuth2 Federation" }} for cross-domain identity management.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro