Accepted and Implemented - 2026-05-08
Implement OAuth 2.1 Authorization Server and Resource Server with DPoP
proof-of-possession directly in TeaAgent, using a zero-dependency HMAC-SHA256
core with optional cryptography library for asymmetric DPoP signature
verification (ES256/RS256).
Git History:
- Created: 2026-05-08 23:54:34 +0800
- Commit:
2ab09cd8f87dbfcaf7fb9eeb4dc34be613179baa - Message: "Modularize oauth21, add gitignore-aware listing with pagination, enforce mypy strict mode"
Updated: 2026-05-10 14:11:18 +0800
- Commit:
091b27189d0c84102745828cccc6cda923067680 - Message: "Update ADRs, CHANGELOG, and replace stale scope files with backlog-priority"
Updated: 2026-05-22 01:03:20 +0800
- Commit:
d4ac38d3c0890ac907886501e2cd9aa3102afab4 - Message: "Implement OAuth refresh-token rotation deferred in ADR 0004"
Files Added:
teaagent/oauth21/_jwt.py- JWT implementationteaagent/oauth21/_dpop.py- DPoP proof-of-possessionteaagent/oauth21/_types.py- OAuth typesteaagent/oauth21/_server.py- Authorization serverteaagent/oauth21/_resource.py- Resource serverteaagent/oauth21/_pkce.py- PKCE implementation
Key Components:
- HMAC-SHA256 core: Zero-dependency JWT signing
- Optional cryptography: Asymmetric DPoP signature verification (ES256/RS256)
- DPoP proof-of-possession: Binds access tokens to client key pair
- Refresh-token rotation: Deferred in original ADR, implemented 2026-05-22
Tests:
- Unit tests for OAuth 2.1 and DPoP
- E2E tests for refresh-token rotation
- All tests passing
- MCP Streamable HTTP requires authentication for non-loopback binds. The minimal viable option is a bearer token, but bearer tokens are replayable by any network observer.
- DPoP (RFC 9449) binds access tokens to a client-generated asymmetric key pair, preventing token replay. This is the only OAuth extension that materially improves MCP transport security without requiring TLS.
- HS256 is the only JWT algorithm available without
cryptography. When DPoP is disabled, HS256 access tokens are sufficient for the bearer token use case. - The optional-dependency design (
pip install teaagent[crypto]) keeps the P0 zero-dependency posture intact while making DPoP available on demand.
- The module was split into
oauth21/submodules (_jwt.py,_dpop.py,_types.py,_server.py,_resource.py,_pkce.py) at 851 lines for maintainability. - Internal state (clients, codes, nonces) is in-memory dicts. Multi-process or persistent deployments require an external store.
- Key rotation and external client storage were deferred to ADR 0006 (now implemented). Refresh-token rotation was deferred here and implemented 2026-05-22.
- The
cryptographyimport is conditional (HAS_CRYPTOGRAPHYflag);
Key rotation and external client storage have been implemented:
OAuthKeyRing.rotatewith configurable rotation overlap window,key_for_validationJWTkid-based lookup, and--oauth-rotation-windowCLI (teaagent/oauth21/_store.py).- Cross-host persistence via
PostgreSQLOAuthStoreandRedisOAuthStorewith atomic consume semantics (teaagent/oauth21/_pg_store.py,teaagent/oauth21/_redis_store.py).
Refresh-token rotation is implemented (2026-05-22):
- Rotating refresh tokens issued on authorization-code exchange when
refresh_token_ttl > 0(default 30 days;--oauth-refresh-token-ttl/refresh_token_ttl=0disables). refresh_tokengrant at/tokenwith one-time consume, new refresh token on each use, and family revocation on reuse detection.- Persisted in
OAuthStoreimplementations (InMemoryOAuthStore,SQLiteOAuthStore,PostgreSQLOAuthStore,RedisOAuthStore).
- Authlib: Adds 10+ dependencies; overkill for a single JWT sign/verify plus DPoP validation.
- PyJWT: HMAC-only JWT is 20 lines; adding a dependency for it is waste.
- TLS-only (no DPoP): Rejected because the MCP server is designed to run behind a reverse proxy and DPoP protects against replay even after TLS termination.