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

This commit is contained in:
nelind
2025-12-24 03:13:07 +01:00
committed by Tangled
parent 014d4b57f0
commit 1280f8044d
2 changed files with 41 additions and 35 deletions

View File

@@ -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<void> {
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<void> {
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<OAuthTokens> {
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,
}),
})

View File

@@ -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<ClientMetadata, OAuthError> {
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::<String>::new();
let mut scope: Option<String> = 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()),
})
}