this repo has no description

fix "localhost client" support and use in frontend when in dev mode

authored by nelind and committed by tangled.org c69c7749 68e7f4ee

Changed files
+41 -35
frontend
src
lib
src
oauth
+18 -21
frontend/src/lib/oauth.ts
··· 1 const OAUTH_STATE_KEY = 'tranquil_pds_oauth_state' 2 const OAUTH_VERIFIER_KEY = 'tranquil_pds_oauth_verifier' 3 4 interface OAuthState { 5 state: string ··· 65 66 saveOAuthState({ state, codeVerifier }) 67 68 - const clientId = `${window.location.origin}/oauth/client-metadata.json` 69 - const redirectUri = `${window.location.origin}/` 70 - 71 const parResponse = await fetch('/oauth/par', { 72 method: 'POST', 73 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 74 body: new URLSearchParams({ 75 - client_id: clientId, 76 - redirect_uri: redirectUri, 77 response_type: 'code', 78 - scope: [ 79 - 'atproto', 80 - 'repo:*?action=create', 81 - 'repo:*?action=update', 82 - 'repo:*?action=delete', 83 - 'blob:*/*', 84 - ].join(' '), 85 state: state, 86 code_challenge: codeChallenge, 87 code_challenge_method: 'S256', ··· 96 const { request_uri } = await parResponse.json() 97 98 const authorizeUrl = new URL('/oauth/authorize', window.location.origin) 99 - authorizeUrl.searchParams.set('client_id', clientId) 100 authorizeUrl.searchParams.set('request_uri', request_uri) 101 102 window.location.href = authorizeUrl.toString() ··· 122 throw new Error('OAuth state mismatch. Please try logging in again.') 123 } 124 125 - const clientId = `${window.location.origin}/oauth/client-metadata.json` 126 - const redirectUri = `${window.location.origin}/` 127 - 128 const tokenResponse = await fetch('/oauth/token', { 129 method: 'POST', 130 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 131 body: new URLSearchParams({ 132 grant_type: 'authorization_code', 133 - client_id: clientId, 134 code: code, 135 - redirect_uri: redirectUri, 136 code_verifier: savedState.codeVerifier, 137 }), 138 }) ··· 148 } 149 150 export async function refreshOAuthToken(refreshToken: string): Promise<OAuthTokens> { 151 - const clientId = `${window.location.origin}/oauth/client-metadata.json` 152 - 153 const tokenResponse = await fetch('/oauth/token', { 154 method: 'POST', 155 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 156 body: new URLSearchParams({ 157 grant_type: 'refresh_token', 158 - client_id: clientId, 159 refresh_token: refreshToken, 160 }), 161 })
··· 1 const OAUTH_STATE_KEY = 'tranquil_pds_oauth_state' 2 const OAUTH_VERIFIER_KEY = 'tranquil_pds_oauth_verifier' 3 + const SCOPES = [ 4 + 'atproto', 5 + 'repo:*?action=create', 6 + 'repo:*?action=update', 7 + 'repo:*?action=delete', 8 + 'blob:*/*', 9 + ].join(' ') 10 + const CLIENT_ID = !(import.meta.env.DEV) 11 + ? `${window.location.origin}/oauth/client-metadata.json` 12 + : `http://localhost/oauth/client-metadata.json?scope=${SCOPES}` 13 + const REDIRECT_URI = `${window.location.origin}/` 14 15 interface OAuthState { 16 state: string ··· 76 77 saveOAuthState({ state, codeVerifier }) 78 79 const parResponse = await fetch('/oauth/par', { 80 method: 'POST', 81 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 82 body: new URLSearchParams({ 83 + client_id: CLIENT_ID, 84 + redirect_uri: REDIRECT_URI, 85 response_type: 'code', 86 + scope: SCOPES, 87 state: state, 88 code_challenge: codeChallenge, 89 code_challenge_method: 'S256', ··· 98 const { request_uri } = await parResponse.json() 99 100 const authorizeUrl = new URL('/oauth/authorize', window.location.origin) 101 + authorizeUrl.searchParams.set('client_id', CLIENT_ID) 102 authorizeUrl.searchParams.set('request_uri', request_uri) 103 104 window.location.href = authorizeUrl.toString() ··· 124 throw new Error('OAuth state mismatch. Please try logging in again.') 125 } 126 127 const tokenResponse = await fetch('/oauth/token', { 128 method: 'POST', 129 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 130 body: new URLSearchParams({ 131 grant_type: 'authorization_code', 132 + client_id: CLIENT_ID, 133 code: code, 134 + redirect_uri: REDIRECT_URI, 135 code_verifier: savedState.codeVerifier, 136 }), 137 }) ··· 147 } 148 149 export async function refreshOAuthToken(refreshToken: string): Promise<OAuthTokens> { 150 const tokenResponse = await fetch('/oauth/token', { 151 method: 'POST', 152 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 153 body: new URLSearchParams({ 154 grant_type: 'refresh_token', 155 + client_id: CLIENT_ID, 156 refresh_token: refreshToken, 157 }), 158 })
+23 -14
src/oauth/client.rs
··· 89 fn is_loopback_client(client_id: &str) -> bool { 90 if let Ok(url) = reqwest::Url::parse(client_id) { 91 url.scheme() == "http" 92 - && matches!(url.host_str(), Some("localhost") | Some("127.0.0.1")) 93 - && url.query().is_some() 94 } else { 95 false 96 } ··· 98 99 fn build_loopback_metadata(client_id: &str) -> Result<ClientMetadata, OAuthError> { 100 let url = reqwest::Url::parse(client_id) 101 - .map_err(|_| OAuthError::InvalidClient("Invalid loopback client_id URL".to_string()))?; 102 - let mut redirect_uris = Vec::new(); 103 for (key, value) in url.query_pairs() { 104 if key == "redirect_uri" { 105 redirect_uris.push(value.to_string()); 106 } 107 } 108 if redirect_uris.is_empty() { 109 - redirect_uris.push("http://127.0.0.1/callback".to_string()); 110 - redirect_uris.push("http://localhost/callback".to_string()); 111 } 112 - let scope = Some("atproto transition:generic transition:chat.bsky".to_string()); 113 Ok(ClientMetadata { 114 - client_id: client_id.to_string(), 115 - client_name: Some("Loopback Client".to_string()), 116 client_uri: None, 117 logo_uri: None, 118 redirect_uris, 119 grant_types: vec![ 120 - "authorization_code".to_string(), 121 - "refresh_token".to_string(), 122 ], 123 - response_types: vec!["code".to_string()], 124 scope, 125 - token_endpoint_auth_method: Some("none".to_string()), 126 dpop_bound_access_tokens: Some(false), 127 jwks: None, 128 jwks_uri: None, 129 - application_type: Some("native".to_string()), 130 }) 131 } 132
··· 89 fn is_loopback_client(client_id: &str) -> bool { 90 if let Ok(url) = reqwest::Url::parse(client_id) { 91 url.scheme() == "http" 92 + && url.host_str() == Some("localhost") 93 + && url.port().is_none() 94 + && url.path().is_empty() 95 } else { 96 false 97 } ··· 99 100 fn build_loopback_metadata(client_id: &str) -> Result<ClientMetadata, OAuthError> { 101 let url = reqwest::Url::parse(client_id) 102 + .map_err(|_| OAuthError::InvalidClient("Invalid loopback client_id URL".into()))?; 103 + let mut redirect_uris = Vec::<String>::new(); 104 + let mut scope: Option<String> = None; 105 for (key, value) in url.query_pairs() { 106 if key == "redirect_uri" { 107 redirect_uris.push(value.to_string()); 108 + break; 109 + } 110 + if key == "scope" { 111 + scope = Some(value.into()); 112 + break; 113 } 114 } 115 if redirect_uris.is_empty() { 116 + redirect_uris.push("http://127.0.0.1/".into()); 117 + redirect_uris.push("http://[::1]/".into()); 118 + } 119 + if scope.is_none() { 120 + scope = Some("atproto".into()); 121 } 122 Ok(ClientMetadata { 123 + client_id: client_id.into(), 124 + client_name: Some("Loopback Client".into()), 125 client_uri: None, 126 logo_uri: None, 127 redirect_uris, 128 grant_types: vec![ 129 + "authorization_code".into(), 130 + "refresh_token".into(), 131 ], 132 + response_types: vec!["code".into()], 133 scope, 134 + token_endpoint_auth_method: Some("none".into()), 135 dpop_bound_access_tokens: Some(false), 136 jwks: None, 137 jwks_uri: None, 138 + application_type: Some("native".into()), 139 }) 140 } 141