feat: browser-based OAuth login flow (story 406)

Add three HTTP endpoints for OAuth login without terminal access:
- GET /oauth/authorize — generates PKCE params, redirects to
  claude.com/cai/oauth/authorize with code=true and full scopes
- GET /callback — exchanges auth code for tokens via JSON POST to
  platform.claude.com/v1/oauth/token, writes ~/.claude/.credentials.json
- GET /oauth/status — returns current credential state as JSON

Uses SHA-256 (sha2 crate) for PKCE code challenge. The authorize URL
targets claude.com/cai/ (not platform.claude.com) which is required
for Max/Pro subscriptions to grant user:inference scope.

Users visit http://localhost:3001/oauth/authorize in their browser
to authenticate. Matrix/WhatsApp can send this link when auth fails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Timmy
2026-03-26 19:58:18 +00:00
parent 710b604b7c
commit 877f69c897
6 changed files with 472 additions and 0 deletions
+15
View File
@@ -9,6 +9,7 @@ pub mod health;
pub mod io;
pub mod mcp;
pub mod model;
pub mod oauth;
pub mod settings;
pub mod workflow;
@@ -65,6 +66,8 @@ pub fn build_routes(
let (api_service, docs_service) = build_openapi_service(ctx_arc.clone());
let oauth_state = Arc::new(oauth::OAuthState::new(resolve_port()));
let mut route = Route::new()
.nest("/api", api_service)
.nest("/docs", docs_service.swagger_ui())
@@ -78,6 +81,18 @@ pub fn build_routes(
post(mcp::mcp_post_handler).get(mcp::mcp_get_handler),
)
.at("/health", get(health::health))
.at(
"/oauth/authorize",
get(oauth::oauth_authorize).data(oauth_state.clone()),
)
.at(
"/callback",
get(oauth::oauth_callback).data(oauth_state.clone()),
)
.at(
"/oauth/status",
get(oauth::oauth_status),
)
.at("/assets/*path", get(assets::embedded_asset))
.at("/", get(assets::embedded_index))
.at("/*path", get(assets::embedded_file));