tangled
alpha
login
or
join now
baileytownsend.dev
/
pds-gatekeeper
89
fork
atom
Microservice to bring 2FA to self hosted PDSes
89
fork
atom
overview
issues
1
pulls
3
pipelines
down graded to a public client
baileytownsend.dev
1 week ago
03c272fd
6d50b8de
+29
-71
2 changed files
expand all
collapse all
unified
split
.gitignore
src
admin
oauth.rs
+2
-1
.gitignore
···
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
···
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
}