diff --git a/frontend/src/lib/oauth.ts b/frontend/src/lib/oauth.ts index adbe3b3..a0e825d 100644 --- a/frontend/src/lib/oauth.ts +++ b/frontend/src/lib/oauth.ts @@ -1,5 +1,16 @@ const OAUTH_STATE_KEY = 'tranquil_pds_oauth_state' const OAUTH_VERIFIER_KEY = 'tranquil_pds_oauth_verifier' +const SCOPES = [ + 'atproto', + 'repo:*?action=create', + 'repo:*?action=update', + 'repo:*?action=delete', + 'blob:*/*', +].join(' ') +const CLIENT_ID = !(import.meta.env.DEV) + ? `${window.location.origin}/oauth/client-metadata.json` + : `http://localhost/oauth/client-metadata.json?scope=${SCOPES}` +const REDIRECT_URI = `${window.location.origin}/` interface OAuthState { state: string @@ -65,23 +76,14 @@ export async function startOAuthLogin(): Promise { saveOAuthState({ state, codeVerifier }) - const clientId = `${window.location.origin}/oauth/client-metadata.json` - const redirectUri = `${window.location.origin}/` - const parResponse = await fetch('/oauth/par', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ - client_id: clientId, - redirect_uri: redirectUri, + client_id: CLIENT_ID, + redirect_uri: REDIRECT_URI, response_type: 'code', - scope: [ - 'atproto', - 'repo:*?action=create', - 'repo:*?action=update', - 'repo:*?action=delete', - 'blob:*/*', - ].join(' '), + scope: SCOPES, state: state, code_challenge: codeChallenge, code_challenge_method: 'S256', @@ -96,7 +98,7 @@ export async function startOAuthLogin(): Promise { const { request_uri } = await parResponse.json() const authorizeUrl = new URL('/oauth/authorize', window.location.origin) - authorizeUrl.searchParams.set('client_id', clientId) + authorizeUrl.searchParams.set('client_id', CLIENT_ID) authorizeUrl.searchParams.set('request_uri', request_uri) window.location.href = authorizeUrl.toString() @@ -122,17 +124,14 @@ export async function handleOAuthCallback(code: string, state: string): Promise< throw new Error('OAuth state mismatch. Please try logging in again.') } - const clientId = `${window.location.origin}/oauth/client-metadata.json` - const redirectUri = `${window.location.origin}/` - const tokenResponse = await fetch('/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', - client_id: clientId, + client_id: CLIENT_ID, code: code, - redirect_uri: redirectUri, + redirect_uri: REDIRECT_URI, code_verifier: savedState.codeVerifier, }), }) @@ -148,14 +147,12 @@ export async function handleOAuthCallback(code: string, state: string): Promise< } export async function refreshOAuthToken(refreshToken: string): Promise { - const clientId = `${window.location.origin}/oauth/client-metadata.json` - const tokenResponse = await fetch('/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'refresh_token', - client_id: clientId, + client_id: CLIENT_ID, refresh_token: refreshToken, }), }) diff --git a/src/oauth/client.rs b/src/oauth/client.rs index 1a7ea97..a587174 100644 --- a/src/oauth/client.rs +++ b/src/oauth/client.rs @@ -89,8 +89,9 @@ impl ClientMetadataCache { fn is_loopback_client(client_id: &str) -> bool { if let Ok(url) = reqwest::Url::parse(client_id) { url.scheme() == "http" - && matches!(url.host_str(), Some("localhost") | Some("127.0.0.1")) - && url.query().is_some() + && url.host_str() == Some("localhost") + && url.port().is_none() + && url.path().is_empty() } else { false } @@ -98,35 +99,43 @@ impl ClientMetadataCache { fn build_loopback_metadata(client_id: &str) -> Result { let url = reqwest::Url::parse(client_id) - .map_err(|_| OAuthError::InvalidClient("Invalid loopback client_id URL".to_string()))?; - let mut redirect_uris = Vec::new(); + .map_err(|_| OAuthError::InvalidClient("Invalid loopback client_id URL".into()))?; + let mut redirect_uris = Vec::::new(); + let mut scope: Option = None; for (key, value) in url.query_pairs() { if key == "redirect_uri" { redirect_uris.push(value.to_string()); + break; + } + if key == "scope" { + scope = Some(value.into()); + break; } } if redirect_uris.is_empty() { - redirect_uris.push("http://127.0.0.1/callback".to_string()); - redirect_uris.push("http://localhost/callback".to_string()); + redirect_uris.push("http://127.0.0.1/".into()); + redirect_uris.push("http://[::1]/".into()); + } + if scope.is_none() { + scope = Some("atproto".into()); } - let scope = Some("atproto transition:generic transition:chat.bsky".to_string()); Ok(ClientMetadata { - client_id: client_id.to_string(), - client_name: Some("Loopback Client".to_string()), + client_id: client_id.into(), + client_name: Some("Loopback Client".into()), client_uri: None, logo_uri: None, redirect_uris, grant_types: vec![ - "authorization_code".to_string(), - "refresh_token".to_string(), + "authorization_code".into(), + "refresh_token".into(), ], - response_types: vec!["code".to_string()], + response_types: vec!["code".into()], scope, - token_endpoint_auth_method: Some("none".to_string()), + token_endpoint_auth_method: Some("none".into()), dpop_bound_access_tokens: Some(false), jwks: None, jwks_uri: None, - application_type: Some("native".to_string()), + application_type: Some("native".into()), }) }