Add OAuth 2.1 resource server for external provider auth#95
Conversation
Provider-agnostic JWKS-based token validation — works with any OIDC- compliant provider (WorkOS, Auth0, Cloudflare Access, Keycloak, etc.). Enables Claude Desktop and Claude Code to authenticate via OAuth. - Dual auth: self-signed JWTs (CLI) + OAuth provider tokens both accepted - Auto-provision users on first valid OAuth login (configurable) - Well-known metadata (RFC 9728) for MCP client discovery - WWW-Authenticate headers on 401 responses - 17 new tests (474 total), lint/mypy clean Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove loop_scope="function" parameter from @pytest.mark.asyncio markers — not supported on older pytest-asyncio versions in CI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Alembic migration: oauth_subject, oauth_issuer columns on users table - Unique index on (oauth_issuer, oauth_subject) for provider-agnostic lookup - Auto-provisioning stores OAuth identity on first login - create_tables.sql includes ALTER TABLE fallback for existing schemas - get_user_by_oauth.sql for fast OAuth identity lookup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CI has anyio (via mcp[cli]) but not pytest-asyncio as a direct dependency. Existing middleware tests use @pytest.mark.anyio. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per design decision: tighter control during early access / tester phase. Set to true when ready for open self-service signups. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…rver wiring 8 new tests covering: OAuth identity columns in store, get_user_by_oauth lookup, explicit jwks_uri, JWKS cache refresh, auto-provision in middleware flow, _build_oauth_validator server wiring. oauth.py now at 100% coverage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…wiring Covers _ensure_user with real store and _wrap_with_auth with OAuth issuer. 484 tests total. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Test verifies auto-provisioning failure is swallowed gracefully and doesn't block the authenticated request. 485 tests total. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Codecov note: 3 lines remain uncovered in |
cmeans
left a comment
There was a problem hiding this comment.
QA Review — PR #95: Add OAuth 2.1 resource server
Code Review
OAuthTokenValidator (oauth.py)
- Provider-agnostic JWKS validation via
PyJWKClient— supports RS256, ES256 - JWKS cache with configurable TTL (default 3600s), client re-created on refresh
- Audience verification: required if set, skipped if not (
verify_aud: False) - Extracts
owner_idfrom configurable claim (defaultsub), plusemail,name,oauth_subject,oauth_issuerfor auto-provisioning
AuthMiddleware dual auth (middleware.py)
- Self-signed tried first → fallback to OAuth if configured
_try_self_signed()returns(owner_id, error)tuple — clean error propagation_try_oauth()calls validator, auto-provisions user if enabled_ensure_user()catches all exceptions — auth doesn't fail if provisioning fails_unauthorized()includesWWW-Authenticateheader withresource_metadataURL per RFC 9728/.well-known/paths bypass auth (correct)
WellKnownMiddleware (middleware.py)
- Serves
/.well-known/oauth-protected-resourcewithauthorization_servers,token_methods,resourceURL - RFC 9728 compliant
Migration (i4d5e6f7g8h9)
- Adds
oauth_subject TEXT,oauth_issuer TEXTto users table - Unique index on
(oauth_issuer, oauth_subject)where not null - Index on
oauth_subjectfor fast lookup - Revision chain correct: depends on
h3c4d5e6f7g8
PostgresStore (postgres_store.py)
create_user_if_not_exists(): INSERT ... ON CONFLICT (id) DO NOTHING — safe upsertget_user(),get_user_by_oauth(): clean lookups with SQL files
Server wiring (server.py)
_build_oauth_validator(): creates validator ifOAUTH_ISSUERset_wrap_with_auth(): extracted from inline code — cleaner, handles both auth modes- Auth required now accepts either
JWT_SECRETorOAUTH_ISSUER(or both) — was previously JWT-only WellKnownMiddlewarewraps inside auth (correct — metadata must be public)
Dependencies
PyJWT[crypto]>=2.8—[crypto]extra addscryptographyfor RS256/ES256 support
Observations
- PR body says
AWARENESS_OAUTH_AUTO_PROVISIONdefault:truebut code defaults to"false". CHANGELOG correctly saysdefault: false. PR body table is stale. - Tests #4-5 (OAuth flow with Claude Desktop, auto-provisioning): require live OAuth provider — not testable in QA. The auto-provisioning path is well-covered by unit tests (
test_oauth.pyhas 653 lines of tests). create_tables.sqlhas both CREATE TABLE with columns AND ALTER TABLE ADD COLUMN IF NOT EXISTS for OAuth columns — belt-and-suspenders for fresh vs migrated installs. Works but slightly unusual.
CI Gate Check
| Check | Conclusion |
|---|---|
| lint | ✅ SUCCESS |
| typecheck | ✅ SUCCESS |
| test (3.10) | ✅ SUCCESS |
| test (3.11) | ✅ SUCCESS |
| test (3.12) | ✅ SUCCESS |
| codecov/patch | ✅ SUCCESS |
Zero non-SUCCESS/non-SKIPPED checks.
Test Results
| Check | Result |
|---|---|
| pytest (485 tests) | ✅ 485/485 pass |
| ruff (src/, alembic/) | ✅ Clean |
| mypy | ✅ Clean |
| CI | ✅ All green |
| Manual #1: Self-signed JWT alongside OAuth | ✅ Accepted, MCP session created |
| Manual #2: Well-known metadata | ✅ RFC 9728 JSON with authorization_servers, token_methods |
| Manual #3: 401 WWW-Authenticate header | ✅ Bearer resource_metadata="..." |
| Manual #4: OAuth flow with Claude Desktop | ⏸️ Requires live OAuth provider |
| Manual #5: Auto-provisioning | ⏸️ Requires live OAuth provider (unit tests cover this path) |
Verdict
Zero findings (3 observations). All CI green. 3/5 manual tests verified; 2/5 require live OAuth provider (well-covered by 485 unit tests). Ready for QA Signoff.
|
Adding Ready for QA Signoff — OAuth resource server reviewed. Self-signed JWT, well-known metadata, WWW-Authenticate all verified manually. OAuth flow tests require live provider (covered by unit tests). CI fully green, 485/485 pytest. |
When a user is pre-provisioned via CLI (mcp-awareness-user add) and later authenticates via OAuth, the server links their OAuth identity (sub + issuer) to their existing account by matching on email. Resolution order: OAuth lookup → email link → auto-provision → fallback. Prevents duplicate accounts when CLI and OAuth auth paths converge. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tests Two new tests exercise _resolve_user through the real store: - Already-linked user found via get_user_by_oauth - Pre-provisioned user linked by email on first OAuth login middleware.py now at 100% coverage. 490 tests total. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cmeans
left a comment
There was a problem hiding this comment.
QA Re-Review — PR #95 (Round 2)
New changes (commits `6176b63`, `bba3bf8`)
OAuth identity linking — smart resolution order:
- Look up by OAuth identity (issuer + subject) — already linked
- Email match → link pre-provisioned user on first OAuth login (`link_oauth_identity.sql`: `WHERE email = %s AND oauth_subject IS NULL`)
- Auto-provision new user (if enabled)
- Fallback to token `sub` claim as-is
- `link_oauth_identity.sql`: idempotent — only updates when `oauth_subject IS NULL` (first-time link)
- `_resolve_user()` returns resolved ID or None (caller falls back to `owner_id`)
- Exception handling: resolution failures don't break auth
PR body fixed: `AWARENESS_OAUTH_AUTO_PROVISION` default now correctly shows `false`.
Results
- pytest: 490/490 pass (+5 new identity linking/email match tests)
- CI: all green, codecov/patch SUCCESS
- Prior manual test results still valid (no changes to self-signed JWT, well-known, or WWW-Authenticate paths)
Ready for QA Signoff.
|
Adding Ready for QA Signoff — OAuth identity linking verified (3-step resolution: OAuth lookup → email link → auto-provision). PR body default fixed. CI green, 490/490 pytest. |
|
Adding Ready for QA Signoff — full manual re-test on fresh Docker instance. 3/5 checkboxes verified, 2/5 require live OAuth provider (annotated). CI green, 490/490 pytest. |
cmeans
left a comment
There was a problem hiding this comment.
QA Re-Review — PR #95 (Round 3 — full manual re-test)
Re-ran all manual tests on fresh QA Docker instance.
Manual Test Results
| # | Test | Result |
|---|---|---|
| 1 | Self-signed JWT still works alongside OAuth | ✅ Session created, `get_stats` returned total=17 |
| 2 | Well-known metadata served | ✅ RFC 9728: authorization_servers, token_methods=Bearer, resource URL |
| 3 | 401 includes WWW-Authenticate header | ✅ `Bearer resource_metadata="/.well-known/oauth-protected-resource"` |
| 4 | OAuth flow with Claude Desktop | ⏸️ Requires live WorkOS AuthKit provider |
| 5 | Auto-provisioning creates user | ⏸️ Requires live OAuth (schema verified, unit tests cover path) |
PR checkboxes updated: 3/5 checked, 2/5 annotated as provider-dependent.
Automated Tests
- pytest: 490/490 pass
- CI: all green, codecov/patch SUCCESS
Ready for QA Signoff.
Summary
AWARENESS_OAUTH_AUTO_PROVISION, default: false — tighter control during early access)/.well-known/oauth-protected-resource(RFC 9728) for MCP client OAuth discoveryresource_metadataURL per specoauth_subjectandoauth_issueron users table with unique index, Alembic migration includedNew env vars
AWARENESS_OAUTH_ISSUERAWARENESS_OAUTH_AUDIENCEaudclaimAWARENESS_OAUTH_JWKS_URI{issuer}/.well-known/jwks.jsonAWARENESS_OAUTH_USER_CLAIMsubAWARENESS_OAUTH_AUTO_PROVISIONfalseQA
Prerequisites
pip install -e ".[dev]"AWARENESS_PORT=8421)thoughtful-saga-02-staging.authkit.appManual tests (via MCP tools)
Expected: existing JWT auth continues to work alongside OAuth
Expected: JSON with
authorization_servers,token_methods: ["Bearer"],resourceURLExpected: 401 with
WWW-Authenticate: Bearer resource_metadata="..."headerConfigure connector with WorkOS OAuth Client ID/Secret, connect to awareness MCP URL. Verify login flow completes and MCP tools work.
Expected: successful authentication, tools return user-scoped data
AWARENESS_OAUTH_AUTO_PROVISION=true)After first OAuth login, verify user exists:
Expected: OAuth user appears in user list
🤖 Generated with Claude Code