OAuth 2.1 in WorkingAgents

WorkingAgents needs to authenticate MCP clients – Claude Desktop, Claude Code, third-party agents – without manual token management. The answer is OAuth 2.1 Authorization Code Flow with PKCE. This document describes how it works and how it is implemented.


The Problem

Before OAuth, every MCP client needed a manually generated bearer token. An admin generates the token in IEx, copies it into the client config, and hopes it doesn’t expire or get rotated. That works for one user. It does not work for a team, and it does not work for clients that users install themselves.

OAuth solves this: any compliant client can authenticate users through a standard browser flow. No manual token management, no admin involvement, no tokens in config files.


The Flow

WorkingAgents implements OAuth 2.1 Authorization Code Flow with PKCE plus Dynamic Client Registration (RFC 7591).

Client                    WorkingAgents              Browser/User
  │                            │                          │
  │── POST /register ─────────>│                          │
  │<─ client_id, secret ───────│                          │
  │                            │                          │
  │── redirect to /authorize ──────────────────────────>  │
  │                            │<── login form submitted ─│
  │                            │── validate credentials   │
  │                            │── issue auth code ───────│
  │<─ redirect with ?code= ────────────────────────────── │
  │                            │                          │
  │── POST /token ────────────>│                          │
  │   (code + code_verifier)   │── verify PKCE            │
  │<─ access_token ────────────│                          │
  │                            │                          │
  │── API calls with Bearer ──>│                          │

Step 1: Dynamic Client Registration

An MCP client self-registers before the first user interaction:

POST /register
Content-Type: application/json

{
  "client_name": "Claude Desktop",
  "redirect_uris": ["http://localhost:3000/callback"],
  "grant_types": ["authorization_code"]
}

Response:

{
  "client_id": "wa_abc123...",
  "client_secret": "cs_xyz789...",
  "redirect_uris": ["http://localhost:3000/callback"]
}

No admin involvement. Any MCP-compliant client can register itself.

Step 2: PKCE Setup

Before redirecting the user, the client generates a PKCE pair:

import secrets, hashlib, base64

code_verifier = secrets.token_urlsafe(64)
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b"=").decode()

The client keeps code_verifier secret. It sends only code_challenge to the server.

Step 3: Authorization Request

GET /authorize
  ?client_id=wa_abc123
  &redirect_uri=http://localhost:3000/callback
  &response_type=code
  &state=random_state_value
  &code_challenge=<SHA256 of verifier>
  &code_challenge_method=S256

The user sees the login form. On successful authentication, WorkingAgents creates a single-use authorization code (valid 10 minutes) and redirects:

http://localhost:3000/callback?code=<auth_code>&state=random_state_value

Step 4: Token Exchange

POST /token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=<auth_code>
&client_id=wa_abc123
&redirect_uri=http://localhost:3000/callback
&code_verifier=<original verifier>

The server verifies SHA256(code_verifier) == code_challenge from step 3. If valid, it issues an access token:

{
  "access_token": "...",
  "token_type": "bearer",
  "expires_in": 2592000
}

The client uses this token for all subsequent requests:

Authorization: Bearer <access_token>

Why PKCE

PKCE (Proof Key for Code Exchange) prevents authorization code interception. Without it, if an attacker intercepts the ?code= redirect, they can exchange it for a token. With PKCE, the auth code is useless without the code_verifier that only the legitimate client holds.

OAuth 2.1 requires PKCE for all public clients. WorkingAgents requires it for all clients, including confidential ones.


Token Format

Access tokens are encrypted user IDs, not opaque random strings backed by a database. The encryption uses the same key as browser session cookies (Plug.Crypto.MessageEncryptor, AES-256). On each request, the server decrypts the token to recover user_id, then loads the user’s permission map from AccessControl.

This means:


Server Discovery

MCP clients can discover the OAuth configuration automatically:

GET /.well-known/oauth-authorization-server
{
  "issuer": "https://workingagents.ai",
  "authorization_endpoint": "https://workingagents.ai/authorize",
  "token_endpoint": "https://workingagents.ai/token",
  "registration_endpoint": "https://workingagents.ai/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code"],
  "code_challenge_methods_supported": ["S256"]
}

A compliant MCP client fetches this once and configures itself automatically.


Integration with the Permission System

Once a token is issued, it carries the full permission model. The bearer token decrypts to a user ID. AccessControl.get_permissions(user_id) returns the permission map: permanent keys, temporary TTL grants, and role-based grants merged together. Every tool call checks this map. Revoking a permission takes effect on the next API call – no token reissue needed.


Current Limitations

No persistence across restarts. Client registrations and active auth codes are stored in ETS (in-memory). A server restart clears them. Clients must re-register and users must re-authenticate. A SQLite backend would fix this.

No token revocation. Tokens are valid for 30 days with no revocation endpoint. The only current remediation is to change the server’s secret key, which invalidates all tokens globally.

No refresh tokens. After 30 days the user goes through the full login flow again.

These are the right trade-offs for an early deployment. The ETS store is fast and simple; persistence and revocation can be added once the system is in regular production use.


Authentication Paths Summary

WorkingAgents supports four authentication paths, all converging on the same permission model:

Path Used by
Browser cookie Web UI users after login
OAuth bearer token MCP clients (Claude Desktop, Claude Code, third-party)
Manually generated bearer token Direct API access, IEx testing
WebSocket ticket Browser WebSocket upgrade (single-use nonce)

All four ultimately resolve to a user ID and a permission map. The access control layer is the same regardless of how the user got in.