MCP OAuth 2.1 Authorization
Build OAuth 2.1 authorization server for MCP clients with PKCE, refresh-token rotation, and scope-based access control.
AACWorkflow exposes its API via MCP (Model Context Protocol) with OAuth 2.1 authorization. External MCP clients — including Claude, ChatGPT connectors, and custom tools — authenticate via the OAuth server to obtain scoped, refreshable access tokens bound to a (user, workspace) pair.
Overview
The OAuth server implements the Authorization Code grant with PKCE (Proof Key for Code Exchange), refresh-token rotation, and dynamic client registration. This ensures:
- User control — users explicitly grant workspace access via a consent screen
- Scope-based access — clients declare what resources they need (issues, comments, agents, etc.)
- Token lifecycle — short-lived access tokens (~15 min) and refreshable longer-lived tokens
- Reuse detection — replaying a refresh token revokes the entire token family, catching token leaks
OAuth flow
1. Client registration
Clients register themselves once via:
POST /oauth/register
Content-Type: application/json
{
"client_name": "My MCP Tool",
"redirect_uris": ["http://localhost:5000/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none"
}Returns client_id and client_secret (or no secret for public clients).
2. Authorization code flow
The client initiates login:
GET /oauth/authorize?
client_id=<id>&
redirect_uri=http://localhost:5000/callback&
response_type=code&
scope=issues:read+issues:write+comments:read&
code_challenge=<sha256_hash>&
code_challenge_method=S256The user sees a consent screen listing the requested scopes and the workspace to grant. On approval, they receive an auth code (valid for 10 minutes):
HTTP 302
Location: http://localhost:5000/callback?code=<auth_code>&state=<state>3. Exchange code for token
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=<auth_code>&
client_id=<id>&
code_verifier=<original_plain_challenge>&
redirect_uri=http://localhost:5000/callbackReturns:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "<refresh_token>",
"scope": "issues:read issues:write comments:read"
}The access token is a signed JWT containing:
sub— user IDwsid— workspace IDscope— space-delimited scopesexp— expiration (900 seconds)
4. Refresh token rotation
When an access token expires, use the refresh token:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=<refresh_token>&
client_id=<id>Returns a new access token and a new refresh token. The old refresh token is automatically invalidated. If the old refresh token is replayed, the server revokes the entire token family, signaling a possible token leak.
Scopes
Scope tokens define what resources a client can access:
| Scope | Access |
|---|---|
issues:read | Read issues, status, timeline |
issues:write | Create, update issues; change status |
comments:read | Read issue comments and threads |
comments:write | Create and edit comments |
agents:read | List agents, view details |
agents:write | Create agents, assign to issues |
runtimes:read | Query runtime status and logs |
autopilot:run | Trigger autopilots |
dashboard:read | Query usage and quality dashboards |
Clients request a subset of scopes during authorization. The token is issued with only the requested scopes. If a refresh narrows scopes, the new token carries fewer scopes than the original.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /.well-known/oauth-authorization-server | RFC 8414 metadata discovery |
GET | /oauth/authorize | Consent UI; issues auth codes |
POST | /oauth/token | Code-to-token and refresh flows |
POST | /oauth/register | Dynamic client registration (RFC 7591) |
POST | /oauth/revoke | RFC 7009 token revocation |
Discovery
Clients can discover the authorization server metadata:
GET /.well-known/oauth-authorization-serverReturns:
{
"issuer": "https://aacworkflow.example.com",
"authorization_endpoint": "https://aacworkflow.example.com/oauth/authorize",
"token_endpoint": "https://aacworkflow.example.com/oauth/token",
"registration_endpoint": "https://aacworkflow.example.com/oauth/register",
"revocation_endpoint": "https://aacworkflow.example.com/oauth/revoke",
"scopes_supported": [
"issues:read", "issues:write", "comments:read", "comments:write",
"agents:read", "agents:write", "runtimes:read", "autopilot:run", "dashboard:read"
],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["none"],
"code_challenge_methods_supported": ["S256"]
}Security considerations
PKCE is mandatory — code_challenge_method=S256 is required; plain is not accepted.
- Exact redirect URI matching — redirect URIs are validated exactly against registered values (no prefix match)
- Auth codes are single-use — an auth code can only be exchanged once; a second attempt fails
- Refresh-token rotation — each refresh invalidates the prior refresh token to detect and respond to token leaks
- Short-lived access tokens — access tokens expire after 15 minutes; clients must refresh to renew
- Secure token storage — tokens are hashed at rest (same KDF as personal access tokens); plaintext tokens are never persisted
Revoking access
Revoke a token (access or refresh) at any time:
POST /oauth/revoke
Content-Type: application/x-www-form-urlencoded
token=<access_or_refresh_token>&
client_id=<id>Revoking a refresh token cascades: all access tokens issued from that refresh are also invalidated.
Integration with MCP
External MCP servers use the OAuth tokens to call AACWorkflow tools. Tokens are passed in the Authorization header:
GET /api/issues
Authorization: Bearer <access_token>The server validates the token's signature, checks expiration, and enforces the scopes. If a tool requires issues:write but the token only has issues:read, the call is denied with a 403 Forbidden.
Audit logging
Every OAuth event (login, token issue, refresh, revocation, reuse detection) is recorded in the audit log for compliance and investigation.