Commit 3980687
feat: OAuth 2.1 Authentication with DCR and Consent Form (#368)
* feat: add OAuth 2.1 authentication with DCR and consent form
Implements OAuth 2.1 authentication for ha-mcp using FastMCP's OAuthProvider pattern
with Dynamic Client Registration (DCR). This enables secure, zero-config authentication
for MCP clients like Claude.ai.
Features:
- HomeAssistantOAuthProvider subclass with full OAuth 2.1 support
- Consent form for collecting Home Assistant URL and Long-Lived Access Token
- LLAT validation against HA API before issuing auth codes
- Token management with refresh token support
- New `ha-mcp-oauth` entry point for OAuth-enabled server mode
The OAuth flow:
1. Claude.ai registers dynamically via DCR
2. User is redirected to consent form
3. User enters HA URL and LLAT
4. Credentials are validated against HA
5. Auth code is issued and exchanged for tokens
Closes #245
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(oauth): enhance OAuth metadata for Claude.ai compatibility
Add missing OAuth 2.1 metadata fields required by Claude.ai and other MCP clients:
- **response_modes_supported**: Added ["query"] to comply with OAuth spec
- **token_endpoint_auth_methods_supported**: Added "none" for public clients with PKCE
- **revocation_endpoint_auth_methods_supported**: Added "none" for consistency
## Changes Made
### Enhanced OAuth Metadata Endpoint
- Override `/.well-known/oauth-authorization-server` route in `HomeAssistantOAuthProvider`
- Use `metadata.model_dump(mode='json')` to properly serialize `AnyHttpUrl` objects
- Append "none" auth method to support public clients (Claude.ai uses this pattern)
- Add response_modes_supported field per OAuth 2.1 spec
## Comparison with Production MCP Servers
Analyzed metadata from known-good servers (Linear, Asana, Sentry) and identified
that they all include:
- `response_modes_supported: ["query"]`
- `token_endpoint_auth_methods_supported` includes `"none"`
These fields are critical for Claude.ai's OAuth client which operates as a
public client with PKCE (no client_secret).
## Testing
✅ Tested locally on port 8086
✅ Verified via cloudflare tunnel at https://kijiji-mesa-algebra-arrange.trycloudflare.com
✅ Metadata now matches production MCP server patterns
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(oauth): enable full toolset in OAuth mode
Changed OAuth server to use the complete HomeAssistantSmartMCPServer with
all 80+ tools instead of just a test tool.
## Changes
### Config (src/ha_mcp/config.py)
- Made `homeassistant_url` and `homeassistant_token` optional with OAuth-mode defaults
- Updated validators to accept OAuth placeholder values
- Enables server initialization without environment variables in OAuth mode
### OAuth Server (src/ha_mcp/__main__.py)
- Use `HomeAssistantSmartMCPServer` instead of minimal FastMCP instance
- Override `client` property to provide OAuth-aware HomeAssistant clients
- Extract credentials from OAuth access tokens per-request
- Cache clients by credential set for performance
- Fixed `list_tools()` → `get_tools()` and made it async-aware
## How It Works
1. Server starts without requiring HA env vars (uses placeholders)
2. Client property is overridden to dynamically get credentials
3. Each tool request extracts access token from request context
4. OAuth provider returns HA credentials based on token
5. Dynamic client is created/cached with those credentials
This allows multiple users to authenticate with different HA instances
via the same server instance.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(oauth): implement claims-based stateless auth with proxy client pattern
Changes:
- Store HA credentials (URL + LLAT) in JWT access token claims
- Enables stateless operation - credentials survive server restarts
- Implement OAuthProxyClient to dynamically inject credentials per-request
- Fixes closure issue where tools captured placeholder client at registration
- Preserve claims across token refresh
- Fix NameError when accessing ha_credentials during token exchange
Architecture:
- Claims contain: ha_url, ha_token (embedded in JWT)
- Proxy intercepts tool calls via __getattr__
- Extracts credentials from request token.claims
- Creates/reuses HomeAssistantClient with real credentials
Known Issue:
- FastMCP returns 'invalid_token' error during token validation
- Need to debug verify_token() / load_access_token() methods
Related: #245
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(oauth): use FastMCP AccessToken with claims and correct client init
Critical fixes:
- Import AccessToken from fastmcp.server.auth.auth (has claims field)
instead of mcp.server.auth.provider (no claims field)
- Initialize HomeAssistantClient with base_url and token params
instead of settings object
- Store HA credentials in JWT claims for stateless auth
Working:
✅ OAuth 2.1 flow with consent form
✅ Credentials stored in access_token.claims
✅ OAuthProxyClient extracts claims and creates HA client
✅ All 97 tools functional with real HA data
Limitation:
❌ Tokens don't persist across server restart (in-memory storage)
- We generate random strings, not cryptographic JWTs
- For true stateless persistence, need JWT encoding/decoding
Next steps for full persistence:
- Use python-jose or similar to create signed JWTs
- Encode claims in JWT payload
- Decode/validate in load_access_token() without memory lookup
Related: #245
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* feat(oauth): implement stateless encrypted tokens for persistence
Replaced in-memory token storage with Fernet encryption for stateless,
persistent authentication tokens:
- Access tokens now contain encrypted HA credentials (URL + token)
- Tokens survive server restarts when OAUTH_ENCRYPTION_KEY is set
- No server-side session storage required
- Credentials encrypted/decrypted on-demand from token string
Changes:
- Added cryptography dependency for Fernet symmetric encryption
- Updated token generation to encrypt credentials
- Updated token validation to decrypt and verify
- Cleaned up debug print() statements
- Updated all OAuth unit tests (24/24 passing)
- Fixed test_revoke_token for stateless token architecture
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test(oauth): add comprehensive test coverage for routes and proxy client
Added 13 new tests to improve OAuth test coverage from 64% to higher:
**TestOAuthRoutes (7 tests):**
- Enhanced metadata endpoint verification
- Consent form GET (success, missing txn_id, invalid, expired)
- Consent form POST (success, invalid credentials)
**TestEndToEndOAuthFlow (1 test):**
- Complete OAuth flow from registration to token refresh
- Tests full lifecycle: register → authorize → consent → exchange → refresh
**TestOAuthProxyClient (5 tests):**
- Initialization and attribute forwarding
- Client reuse for same credentials
- Error handling (no token, missing claims)
**Code improvements:**
- Moved OAuthProxyClient to top-level for testability
- Fixed all test mocking paths for fastmcp.server.dependencies
All 37 OAuth unit tests now passing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent f3f5d6c commit 3980687
10 files changed
Lines changed: 2369 additions & 11 deletions
File tree
- src/ha_mcp
- auth
- tests/src/unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
| 34 | + | |
34 | 35 | | |
35 | 36 | | |
36 | 37 | | |
| |||
44 | 45 | | |
45 | 46 | | |
46 | 47 | | |
| 48 | + | |
47 | 49 | | |
48 | 50 | | |
49 | 51 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
| |||
32 | 33 | | |
33 | 34 | | |
34 | 35 | | |
| 36 | + | |
35 | 37 | | |
36 | 38 | | |
37 | 39 | | |
| |||
0 commit comments