Skip to content

OAuth2 Back-Channel Logout — RFC 7009 Session Termination Across Providers

DodaTech Updated 2026-06-28 5 min read

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

  1. What is the difference between front-channel and back-channel logout?
  2. What is a logout token?
  3. Why must logout tokens have a short expiry?
  4. How do you handle a logout request for an already-terminated session?
  5. What is RP-initiated logout?

Answers:

  1. Front-channel uses browser redirects through iframes. Back-channel uses server-to-server HTTP POST requests.
  2. A JWT containing an events claim with the back-channel logout event URI, signed by the OP.
  3. Short expiry prevents replay attacks. Logout tokens are sensitive — they instruct RPs to terminate sessions.
  4. Return 200 OK. The session is already terminated, so the desired state is achieved.
  5. 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

Can I use back-channel logout without OpenID Connect?

: The specification is part of OpenID Connect, but you can implement logout tokens independently as long as both sides agree on the format.

What happens if an RP is offline during logout?

: The OP should retry delivery with exponential backoff. Logout notifications should be queued for offline RPs.

How long should I retry logout delivery?

: Until the session's tokens would have expired naturally. Typically 1-7 days.

Does back-channel logout work across different domains?

: Yes. Back-channel uses server-to-server HTTP, avoiding CORS and cross-domain issues.

Can an RP send back-channel logout to an OP?

: No. Back-channel logout is unidirectional (OP to RP). RPs use RP-initiated logout (front-channel redirect).

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