feat: add OAuth support for WASM tools in web gateway (#489)

* feat: add OAuth support for WASM tools in web gateway

Extract reusable OAuth functions (build_oauth_url, exchange_oauth_code,
store_oauth_tokens, validate_oauth_token) from CLI into shared
oauth_defaults module, then wire them into the web gateway's
ExtensionManager.

Key changes:
- Install auto-activates WASM tools (no separate Activate button)
- Configure button triggers OAuth flow via save_setup_secrets
- Scope merging: installing a second Google tool triggers re-auth with
  merged scopes from all tools sharing the same secret_name
- Cancel-and-retry: aborting stale OAuth listeners prevents port conflicts
- Post-auth validation: wrong account detected via validation_endpoint
- Reconfigure always re-auths (deletes old token before starting fresh)
- UI shows error toast on OAuth failure, refreshes extension list

Flow: Install → Active → Configure (enter client_id/secret) → Save →
OAuth popup → authorize → done. Second Google tool install auto-triggers
scope expansion OAuth.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix: address PR review comments

- Add custom headers support to ValidationEndpointSchema (fixes
  missing Notion-Version header regression)
- Guard activate handler auth check with status == "awaiting_authorization"
  to prevent unexpected OAuth popups
- Add window dimensions to OAuth popup in activateExtension()
- Simplify UTF-8 truncation boundary check

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix: address Copilot PR review comments (security, UX, bugs)

- Add CSRF state parameter to OAuth flow (random state in auth URL, validated in callback)
- Restore MCP server Activate button in web UI (was hidden for all non-channel extensions)
- Abort JoinHandle in cleanup_expired_auths to prevent port 9876 conflicts
- Fix Google-specific error message for non-Google OAuth providers
- Add has_auth field to ExtensionInfo API response (fixes Configure button visibility)
- Use oauth_defaults::callback_url() instead of hardcoded redirect_uri (both CLI and manager)
- Update auth check comment to match actual behavior (scope expansion + first-time auth)
- Add unit tests for build_oauth_url (basic, PKCE, extra params, state uniqueness)
- Check all required setup secrets (client_id + client_secret) before starting OAuth

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
Henry Park
2026-03-04 01:08:40 +08:00
committed by GitHub
co-authored by Claude Opus 4.6
parent f4855962fc
commit 18b59ae9a7
11 changed files with 1109 additions and 232 deletions
+4 -1
View File
@@ -531,7 +531,7 @@ pub async fn wait_for_authorization_callback(
listener: TcpListener,
server_name: &str,
) -> Result<String, AuthError> {
oauth_defaults::wait_for_callback(listener, "/callback", "code", server_name)
oauth_defaults::wait_for_callback(listener, "/callback", "code", server_name, None)
.await
.map_err(|e| match e {
oauth_defaults::OAuthCallbackError::Denied => AuthError::AuthorizationDenied,
@@ -539,6 +539,9 @@ pub async fn wait_for_authorization_callback(
oauth_defaults::OAuthCallbackError::PortInUse(_, msg) => {
AuthError::Http(format!("Port error: {}", msg))
}
oauth_defaults::OAuthCallbackError::StateMismatch { .. } => {
AuthError::Http("CSRF state mismatch in OAuth callback".to_string())
}
oauth_defaults::OAuthCallbackError::Io(msg) => AuthError::Http(msg),
})
}
+5
View File
@@ -512,6 +512,11 @@ pub struct ValidationEndpointSchema {
/// Expected HTTP status code for success (defaults to 200).
#[serde(default = "default_success_status")]
pub success_status: u16,
/// Additional headers to send with the validation request.
/// Used for service-specific requirements (e.g., Notion-Version for Notion API).
#[serde(default)]
pub headers: HashMap<String, String>,
}
fn default_method() -> String {