+18
-21
frontend/src/lib/oauth.ts
+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
+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