tangled
alpha
login
or
join now
tranquil.farm
/
tranquil-pds
149
fork
atom
Our Personal Data Server from scratch!
tranquil.farm
oauth
atproto
pds
rust
postgresql
objectstorage
fun
149
fork
atom
overview
issues
19
pulls
2
pipelines
fix: delegated acc passkey auth
lewis.moe
1 month ago
66f717c6
f9139d41
+241
-13
4 changed files
expand all
collapse all
unified
split
crates
tranquil-pds
src
oauth
endpoints
authorize.rs
delegation.rs
tests
oauth_security.rs
frontend
src
routes
OAuthDelegation.svelte
+94
-11
crates/tranquil-pds/src/oauth/endpoints/authorize.rs
···
311
311
312
312
if is_delegated {
313
313
tracing::info!("Redirecting to delegation auth");
314
314
+
if let Err(e) = state
315
315
+
.oauth_repo
316
316
+
.set_request_did(&request_id, &user.did)
317
317
+
.await
318
318
+
{
319
319
+
tracing::error!(error = %e, "Failed to set delegated DID on authorization request");
320
320
+
return redirect_to_frontend_error(
321
321
+
"server_error",
322
322
+
"Failed to initialize delegation flow",
323
323
+
);
324
324
+
}
314
325
return redirect_see_other(&format!(
315
326
"/app/oauth/delegation?request_uri={}&delegated_did={}",
316
327
url_encode(&request_uri),
···
2137
2148
pub struct PasskeyStartInput {
2138
2149
pub request_uri: String,
2139
2150
pub identifier: String,
2151
2151
+
pub delegated_did: Option<String>,
2140
2152
}
2141
2153
2142
2154
#[derive(Debug, Serialize)]
···
2394
2406
.into_response();
2395
2407
}
2396
2408
2397
2397
-
if state
2409
2409
+
let delegation_from_param = match &form.delegated_did {
2410
2410
+
Some(delegated_did_str) => {
2411
2411
+
match delegated_did_str.parse::<tranquil_types::Did>() {
2412
2412
+
Ok(delegated_did) if delegated_did != user.did => {
2413
2413
+
match state
2414
2414
+
.delegation_repo
2415
2415
+
.get_delegation(&delegated_did, &user.did)
2416
2416
+
.await
2417
2417
+
{
2418
2418
+
Ok(Some(_)) => Some(delegated_did),
2419
2419
+
Ok(None) => None,
2420
2420
+
Err(e) => {
2421
2421
+
tracing::warn!(
2422
2422
+
error = %e,
2423
2423
+
delegated_did = %delegated_did,
2424
2424
+
controller_did = %user.did,
2425
2425
+
"Failed to verify delegation relationship"
2426
2426
+
);
2427
2427
+
None
2428
2428
+
}
2429
2429
+
}
2430
2430
+
}
2431
2431
+
_ => None,
2432
2432
+
}
2433
2433
+
}
2434
2434
+
None => None,
2435
2435
+
};
2436
2436
+
2437
2437
+
let is_delegation_flow = delegation_from_param.is_some()
2438
2438
+
|| request_data.did.as_ref().map_or(false, |existing_did| {
2439
2439
+
existing_did
2440
2440
+
.parse::<tranquil_types::Did>()
2441
2441
+
.ok()
2442
2442
+
.map_or(false, |parsed| parsed != user.did)
2443
2443
+
});
2444
2444
+
2445
2445
+
if let Some(delegated_did) = delegation_from_param {
2446
2446
+
tracing::info!(
2447
2447
+
delegated_did = %delegated_did,
2448
2448
+
controller_did = %user.did,
2449
2449
+
"Passkey auth with delegated_did param - setting delegation flow"
2450
2450
+
);
2451
2451
+
if state
2452
2452
+
.oauth_repo
2453
2453
+
.set_authorization_did(&passkey_start_request_id, &delegated_did, None)
2454
2454
+
.await
2455
2455
+
.is_err()
2456
2456
+
{
2457
2457
+
return OAuthError::ServerError("An error occurred.".into()).into_response();
2458
2458
+
}
2459
2459
+
if state
2460
2460
+
.oauth_repo
2461
2461
+
.set_controller_did(&passkey_start_request_id, &user.did)
2462
2462
+
.await
2463
2463
+
.is_err()
2464
2464
+
{
2465
2465
+
return OAuthError::ServerError("An error occurred.".into()).into_response();
2466
2466
+
}
2467
2467
+
} else if is_delegation_flow {
2468
2468
+
tracing::info!(
2469
2469
+
delegated_did = ?request_data.did,
2470
2470
+
controller_did = %user.did,
2471
2471
+
"Passkey auth in delegation flow - preserving delegated DID"
2472
2472
+
);
2473
2473
+
if state
2474
2474
+
.oauth_repo
2475
2475
+
.set_controller_did(&passkey_start_request_id, &user.did)
2476
2476
+
.await
2477
2477
+
.is_err()
2478
2478
+
{
2479
2479
+
return OAuthError::ServerError("An error occurred.".into()).into_response();
2480
2480
+
}
2481
2481
+
} else if state
2398
2482
.oauth_repo
2399
2483
.set_authorization_did(&passkey_start_request_id, &user.did, None)
2400
2484
.await
2401
2485
.is_err()
2402
2486
{
2403
2403
-
return (
2404
2404
-
StatusCode::INTERNAL_SERVER_ERROR,
2405
2405
-
Json(serde_json::json!({
2406
2406
-
"error": "server_error",
2407
2407
-
"error_description": "An error occurred."
2408
2408
-
})),
2409
2409
-
)
2410
2410
-
.into_response();
2487
2487
+
return OAuthError::ServerError("An error occurred.".into()).into_response();
2411
2488
}
2412
2489
2413
2490
let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({}));
···
2497
2574
}
2498
2575
};
2499
2576
2577
2577
+
let controller_did: Option<tranquil_types::Did> = request_data
2578
2578
+
.controller_did
2579
2579
+
.as_ref()
2580
2580
+
.and_then(|s| s.parse().ok());
2581
2581
+
let passkey_owner_did = controller_did.as_ref().unwrap_or(&did);
2582
2582
+
2500
2583
let auth_state_json = match state
2501
2584
.user_repo
2502
2502
-
.load_webauthn_challenge(&did, "authentication")
2585
2585
+
.load_webauthn_challenge(passkey_owner_did, "authentication")
2503
2586
.await
2504
2587
{
2505
2588
Ok(Some(s)) => s,
···
2591
2674
2592
2675
if let Err(e) = state
2593
2676
.user_repo
2594
2594
-
.delete_webauthn_challenge(&did, "authentication")
2677
2677
+
.delete_webauthn_challenge(passkey_owner_did, "authentication")
2595
2678
.await
2596
2679
{
2597
2680
tracing::warn!(error = %e, "Failed to delete authentication state");
+7
-1
crates/tranquil-pds/src/oauth/endpoints/delegation.rs
···
127
127
.await
128
128
.is_err()
129
129
{
130
130
-
tracing::warn!("Failed to set delegated DID on authorization request");
130
130
+
return Json(DelegationAuthResponse {
131
131
+
success: false,
132
132
+
needs_totp: None,
133
133
+
redirect_uri: None,
134
134
+
error: Some("Failed to update authorization request".to_string()),
135
135
+
})
136
136
+
.into_response();
131
137
}
132
138
133
139
let grant = match state
+138
crates/tranquil-pds/tests/oauth_security.rs
···
1250
1250
"Error should be InsufficientScope"
1251
1251
);
1252
1252
}
1253
1253
+
1254
1254
+
#[tokio::test]
1255
1255
+
async fn test_delegation_oauth_token_sub_is_delegated_account() {
1256
1256
+
let url = base_url().await;
1257
1257
+
let http_client = client();
1258
1258
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
1259
1259
+
1260
1260
+
let (controller_jwt, controller_did) = create_account_and_login(&http_client).await;
1261
1261
+
1262
1262
+
let delegated_handle = format!("dlgsub{}", suffix);
1263
1263
+
let delegated_res = http_client
1264
1264
+
.post(format!("{}/xrpc/_delegation.createDelegatedAccount", url))
1265
1265
+
.bearer_auth(&controller_jwt)
1266
1266
+
.json(&json!({
1267
1267
+
"handle": delegated_handle,
1268
1268
+
"controllerScopes": "atproto"
1269
1269
+
}))
1270
1270
+
.send()
1271
1271
+
.await
1272
1272
+
.unwrap();
1273
1273
+
assert_eq!(
1274
1274
+
delegated_res.status(),
1275
1275
+
StatusCode::OK,
1276
1276
+
"Should create delegated account"
1277
1277
+
);
1278
1278
+
let delegated_account: Value = delegated_res.json().await.unwrap();
1279
1279
+
let delegated_did = delegated_account["did"].as_str().unwrap();
1280
1280
+
1281
1281
+
assert_ne!(
1282
1282
+
delegated_did, controller_did,
1283
1283
+
"Delegated DID should be different from controller DID"
1284
1284
+
);
1285
1285
+
1286
1286
+
let redirect_uri = "https://example.com/deleg-sub-callback";
1287
1287
+
let mock_client = setup_mock_client_metadata(redirect_uri).await;
1288
1288
+
let client_id = mock_client.uri();
1289
1289
+
let (code_verifier, code_challenge) = generate_pkce();
1290
1290
+
1291
1291
+
let par_body: Value = http_client
1292
1292
+
.post(format!("{}/oauth/par", url))
1293
1293
+
.form(&[
1294
1294
+
("response_type", "code"),
1295
1295
+
("client_id", &client_id),
1296
1296
+
("redirect_uri", redirect_uri),
1297
1297
+
("code_challenge", &code_challenge),
1298
1298
+
("code_challenge_method", "S256"),
1299
1299
+
("scope", "atproto"),
1300
1300
+
("login_hint", delegated_did),
1301
1301
+
])
1302
1302
+
.send()
1303
1303
+
.await
1304
1304
+
.unwrap()
1305
1305
+
.json()
1306
1306
+
.await
1307
1307
+
.unwrap();
1308
1308
+
let request_uri = par_body["request_uri"].as_str().unwrap();
1309
1309
+
1310
1310
+
let auth_res = http_client
1311
1311
+
.post(format!("{}/oauth/delegation/auth", url))
1312
1312
+
.header("Content-Type", "application/json")
1313
1313
+
.json(&json!({
1314
1314
+
"request_uri": request_uri,
1315
1315
+
"delegated_did": delegated_did,
1316
1316
+
"controller_did": controller_did,
1317
1317
+
"password": "Testpass123!",
1318
1318
+
"remember_device": false
1319
1319
+
}))
1320
1320
+
.send()
1321
1321
+
.await
1322
1322
+
.unwrap();
1323
1323
+
assert_eq!(
1324
1324
+
auth_res.status(),
1325
1325
+
StatusCode::OK,
1326
1326
+
"Delegation auth should succeed"
1327
1327
+
);
1328
1328
+
let auth_body: Value = auth_res.json().await.unwrap();
1329
1329
+
assert!(
1330
1330
+
auth_body["success"].as_bool().unwrap_or(false),
1331
1331
+
"Delegation auth should report success: {:?}",
1332
1332
+
auth_body
1333
1333
+
);
1334
1334
+
1335
1335
+
let consent_res = http_client
1336
1336
+
.post(format!("{}/oauth/authorize/consent", url))
1337
1337
+
.header("Content-Type", "application/json")
1338
1338
+
.json(&json!({
1339
1339
+
"request_uri": request_uri,
1340
1340
+
"approved_scopes": ["atproto"],
1341
1341
+
"remember": false
1342
1342
+
}))
1343
1343
+
.send()
1344
1344
+
.await
1345
1345
+
.unwrap();
1346
1346
+
assert_eq!(
1347
1347
+
consent_res.status(),
1348
1348
+
StatusCode::OK,
1349
1349
+
"Consent should succeed"
1350
1350
+
);
1351
1351
+
let consent_body: Value = consent_res.json().await.unwrap();
1352
1352
+
let redirect_location = consent_body["redirect_uri"]
1353
1353
+
.as_str()
1354
1354
+
.expect("Expected redirect_uri");
1355
1355
+
1356
1356
+
let code = redirect_location
1357
1357
+
.split("code=")
1358
1358
+
.nth(1)
1359
1359
+
.unwrap()
1360
1360
+
.split('&')
1361
1361
+
.next()
1362
1362
+
.unwrap();
1363
1363
+
1364
1364
+
let token_res = http_client
1365
1365
+
.post(format!("{}/oauth/token", url))
1366
1366
+
.form(&[
1367
1367
+
("grant_type", "authorization_code"),
1368
1368
+
("code", code),
1369
1369
+
("redirect_uri", redirect_uri),
1370
1370
+
("code_verifier", &code_verifier),
1371
1371
+
("client_id", &client_id),
1372
1372
+
])
1373
1373
+
.send()
1374
1374
+
.await
1375
1375
+
.unwrap();
1376
1376
+
assert_eq!(token_res.status(), StatusCode::OK, "Token exchange should succeed");
1377
1377
+
let tokens: Value = token_res.json().await.unwrap();
1378
1378
+
1379
1379
+
let sub = tokens["sub"].as_str().expect("Token response should have sub claim");
1380
1380
+
1381
1381
+
assert_eq!(
1382
1382
+
sub, delegated_did,
1383
1383
+
"Token sub claim should be the DELEGATED account's DID, not the controller's. Got {} but expected {}",
1384
1384
+
sub, delegated_did
1385
1385
+
);
1386
1386
+
assert_ne!(
1387
1387
+
sub, controller_did,
1388
1388
+
"Token sub claim should NOT be the controller's DID"
1389
1389
+
);
1390
1390
+
}
+2
-1
frontend/src/routes/OAuthDelegation.svelte
···
127
127
},
128
128
body: JSON.stringify({
129
129
request_uri: requestUri,
130
130
-
identifier: controllerIdentifier.trim().replace(/^@/, '')
130
130
+
identifier: controllerIdentifier.trim().replace(/^@/, ''),
131
131
+
delegated_did: delegatedDid
131
132
})
132
133
})
133
134