alpha
Login
or
Join now
pds.dad
/
pds-gatekeeper
Star
0
Fork
3
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Microservice to bring 2FA to self hosted PDSes
Star
0
Fork
3
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
down graded to a public client
author
Bailey Townsend
date
3 months ago
(Mar 3, 2026, 5:36 PM -0600)
commit
03c272fd
03c272fdb60c6e8686fd992b82280ac273a982fb
parent
6d50b8de
6d50b8de612956ac0c786148b383224dddf7758f
+29
-71
2 changed files
Expand all
Collapse all
Unified
Split
.gitignore
src
admin
oauth.rs
+2
-1
.gitignore
Reviewed
···
1
1
/target
2
2
.idea
3
3
-
pds.env
3
3
+
pds.env
4
4
+
dev_admin_rbac.yaml
+27
-70
src/admin/oauth.rs
Reviewed
···
1
1
+
use crate::AppState;
1
2
use axum::{
2
3
extract::{Query, State},
3
4
http::StatusCode,
···
16
17
};
17
18
use jose_jwk::Jwk;
18
19
use serde::Deserialize;
19
19
-
20
20
-
use crate::AppState;
20
20
+
use tracing::log;
21
21
22
22
use super::session;
23
23
···
26
26
27
27
/// Initialize the OAuth client for admin portal authentication.
28
28
pub fn init_oauth_client(pds_hostname: &str) -> Result<AdminOAuthClient, anyhow::Error> {
29
29
-
// Generate ES256 keypair
30
30
-
let secret_key = p256::SecretKey::random(&mut p256::elliptic_curve::rand_core::OsRng);
31
31
-
let public_key = secret_key.public_key();
32
32
-
33
33
-
// Build JWK JSON manually with both public and private components
34
34
-
let public_jwk_str = public_key.to_jwk_string();
35
35
-
let mut jwk: serde_json::Value = serde_json::from_str(&public_jwk_str)?;
36
36
-
37
37
-
// Add the private key component 'd'
38
38
-
let secret_scalar = secret_key.to_bytes();
39
39
-
let d_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(secret_scalar.as_slice());
40
40
-
let jwk_obj = jwk
41
41
-
.as_object_mut()
42
42
-
.ok_or_else(|| anyhow::anyhow!("JWK is not an object"))?;
43
43
-
jwk_obj.insert("d".to_string(), serde_json::Value::String(d_b64));
44
44
-
45
45
-
// Add kid and alg
46
46
-
let kid = uuid::Uuid::new_v4().to_string();
47
47
-
jwk_obj.insert("kid".to_string(), serde_json::Value::String(kid));
48
48
-
jwk_obj.insert(
49
49
-
"alg".to_string(),
50
50
-
serde_json::Value::String("ES256".to_string()),
51
51
-
);
52
52
-
jwk_obj.insert(
53
53
-
"use".to_string(),
54
54
-
serde_json::Value::String("sig".to_string()),
55
55
-
);
56
56
-
57
57
-
// Parse into jose-jwk type for Keyset
58
58
-
let jose_jwk: Jwk = serde_json::from_value(jwk)?;
59
59
-
let keyset = Keyset::try_from(vec![jose_jwk])?;
60
60
-
61
29
// Build client metadata
62
30
let client_id = format!("https://{}/admin/client-metadata.json", pds_hostname)
63
31
.parse()
···
74
42
Some(client_uri),
75
43
vec![redirect_uri],
76
44
vec![GrantType::AuthorizationCode],
77
77
-
vec![
78
78
-
jacquard_oauth::scopes::Scope::parse("atproto").expect("valid scope"),
79
79
-
],
45
45
+
vec![jacquard_oauth::scopes::Scope::parse("atproto").expect("valid scope")],
80
46
None,
81
47
);
82
48
83
83
-
let client_data = ClientData::new(Some(keyset), config);
49
49
+
let client_data = ClientData::new(None, config);
84
50
let store = MemoryAuthStore::new();
85
51
let client = OAuthClient::new(store, client_data);
86
52
···
99
65
let redirect_uri = format!("https://{}/admin/oauth/callback", pds_hostname);
100
66
let client_uri = format!("https://{}/admin/", pds_hostname);
101
67
102
102
-
let jwks = oauth_client.jwks();
103
103
-
104
68
let metadata = serde_json::json!({
105
69
"client_id": client_id,
106
70
"client_uri": client_uri,
···
108
72
"grant_types": ["authorization_code"],
109
73
"response_types": ["code"],
110
74
"scope": "atproto",
111
111
-
"token_endpoint_auth_method": "private_key_jwt",
112
112
-
"token_endpoint_auth_signing_alg": "ES256",
75
75
+
"token_endpoint_auth_method": "none",
113
76
"application_type": "web",
114
77
"dpop_bound_access_tokens": true,
115
115
-
"jwks": jwks,
78
78
+
116
79
});
117
80
118
81
(
···
137
100
}
138
101
139
102
use axum_template::TemplateEngine;
140
140
-
match state.template_engine.render("admin/login.hbs", data)
141
141
-
{
103
103
+
match state.template_engine.render("admin/login.hbs", data) {
142
104
Ok(html) => Html(html).into_response(),
143
105
Err(e) => {
144
106
tracing::error!("Failed to render login template: {}", e);
···
168
130
};
169
131
170
132
let pds_hostname = &state.app_config.pds_hostname;
171
171
-
let redirect_uri: url::Url =
172
172
-
match format!("https://{}/admin/oauth/callback", pds_hostname).parse() {
173
173
-
Ok(u) => u,
174
174
-
Err(_) => {
175
175
-
return Redirect::to("/admin/login?error=Invalid+server+configuration")
176
176
-
.into_response()
177
177
-
}
178
178
-
};
133
133
+
let redirect_uri: url::Url = match format!("https://{}/admin/oauth/callback", pds_hostname)
134
134
+
.parse()
135
135
+
{
136
136
+
Ok(u) => u,
137
137
+
Err(_) => {
138
138
+
return Redirect::to("/admin/login?error=Invalid+server+configuration").into_response();
139
139
+
}
140
140
+
};
179
141
180
142
let options = AuthorizeOptions {
181
143
redirect_uri: Some(redirect_uri),
···
242
204
let (did, handle) = oauth_session.session_info().await;
243
205
let did_str = did.to_string();
244
206
let handle_str = handle.to_string();
245
245
-
207
207
+
log::info!("Authenticated as DID {} ({})", did_str, handle_str);
246
208
// Check if this DID is a member in the RBAC config
247
209
if !rbac.is_member(&did_str) {
248
210
tracing::warn!("Access denied for DID {} (not in RBAC config)", did_str);
···
258
220
259
221
// Create admin session
260
222
let ttl_hours = state.app_config.admin_session_ttl_hours;
261
261
-
let session_id = match session::create_session(
262
262
-
&state.pds_gatekeeper_pool,
263
263
-
&did_str,
264
264
-
&handle_str,
265
265
-
ttl_hours,
266
266
-
)
267
267
-
.await
268
268
-
{
269
269
-
Ok(id) => id,
270
270
-
Err(e) => {
271
271
-
tracing::error!("Failed to create admin session: {}", e);
272
272
-
return Redirect::to("/admin/login?error=Session+creation+failed").into_response();
273
273
-
}
274
274
-
};
223
223
+
let session_id =
224
224
+
match session::create_session(&state.pds_gatekeeper_pool, &did_str, &handle_str, ttl_hours)
225
225
+
.await
226
226
+
{
227
227
+
Ok(id) => id,
228
228
+
Err(e) => {
229
229
+
tracing::error!("Failed to create admin session: {}", e);
230
230
+
return Redirect::to("/admin/login?error=Session+creation+failed").into_response();
231
231
+
}
232
232
+
};
275
233
276
234
// Set signed cookie
277
235
let mut cookie = Cookie::new("__gatekeeper_admin_session", session_id);
···
293
251
});
294
252
295
253
use axum_template::TemplateEngine;
296
296
-
match state.template_engine.render("admin/error.hbs", data)
297
297
-
{
254
254
+
match state.template_engine.render("admin/error.hbs", data) {
298
255
Ok(html) => Html(html).into_response(),
299
256
Err(_) => (StatusCode::FORBIDDEN, format!("{}: {}", title, message)).into_response(),
300
257
}