Our Personal Data Server from scratch! tranquil.farm
oauth atproto pds rust postgresql objectstorage fun

fix: oauth consolidation, include-scope improvements #4

merged opened by lewis.moe targeting main from fix/oauth-on-niche-apps
  • auth extraction should be happening in the auth crate, yes, who coulda thought
  • include: scope should actually be doing the right thing and going out and requesting stuff to expand out the perms
  • more tests!!!1!
  • more correct parsing of the #bsky-appview or whatever suffixes on did webs that come through auth
Labels

None yet.

assignee
Participants 2
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3md3bluniqt22
+88 -84
Interdiff #4 โ†’ #5
.sqlx/query-06eb7c6e1983b6121526ba63612236391290c2e63d37d2bb1cd89ea822950a82.json

This file has not been changed.

.sqlx/query-5031b96c65078d6c54954ce6e57ff9cbba4c48dd8a7546882ab5647114ffab4a.json

This file has not been changed.

.sqlx/query-6258398accee69e0c5f455a3c0ecc273b3da6ef5bb4d8660adafe63d8e3cd2d4.json

This file has not been changed.

.sqlx/query-a4dc8fb22bd094d414c55b9da20b610f7b122b485ab0fd0d0646d68ae8e64fe6.json

This file has not been changed.

.sqlx/query-dec3a21a8e60cc8d2c5dad727750bc88f5535dedae244f7b6e4afa95769b8f1a.json

This file has not been changed.

Cargo.lock

This file has not been changed.

crates/tranquil-pds/src/api/actor/preferences.rs

This file has not been changed.

crates/tranquil-pds/src/api/admin/account/delete.rs

This file has not been changed.

crates/tranquil-pds/src/api/admin/account/email.rs

This file has not been changed.

crates/tranquil-pds/src/api/admin/account/info.rs

This file has not been changed.

crates/tranquil-pds/src/api/admin/account/search.rs

This file has not been changed.

crates/tranquil-pds/src/api/admin/account/update.rs

This file has not been changed.

crates/tranquil-pds/src/api/admin/config.rs

This file has not been changed.

crates/tranquil-pds/src/api/admin/invite.rs

This file has not been changed.

crates/tranquil-pds/src/api/admin/server_stats.rs

This file has not been changed.

crates/tranquil-pds/src/api/admin/status.rs

This file has not been changed.

crates/tranquil-pds/src/api/backup.rs

This file has not been changed.

crates/tranquil-pds/src/api/delegation.rs

This file has not been changed.

-1
crates/tranquil-pds/src/api/error.rs
··· 546 crate::auth::extractor::AuthError::ServiceAuthNotAllowed => Self::AuthenticationFailed( 547 Some("Service authentication not allowed for this endpoint".to_string()), 548 ), 549 - crate::auth::extractor::AuthError::SigningKeyRequired => Self::InvalidSigningKey, 550 crate::auth::extractor::AuthError::InsufficientScope(msg) => { 551 Self::InsufficientScope(Some(msg)) 552 }
··· 546 crate::auth::extractor::AuthError::ServiceAuthNotAllowed => Self::AuthenticationFailed( 547 Some("Service authentication not allowed for this endpoint".to_string()), 548 ), 549 crate::auth::extractor::AuthError::InsufficientScope(msg) => { 550 Self::InsufficientScope(Some(msg)) 551 }
crates/tranquil-pds/src/api/identity/account.rs

This file has not been changed.

crates/tranquil-pds/src/api/identity/did.rs

This file has not been changed.

crates/tranquil-pds/src/api/identity/plc/request.rs

This file has not been changed.

crates/tranquil-pds/src/api/identity/plc/sign.rs

This file has not been changed.

crates/tranquil-pds/src/api/identity/plc/submit.rs

This file has not been changed.

crates/tranquil-pds/src/api/moderation/mod.rs

This file has not been changed.

crates/tranquil-pds/src/api/notification_prefs.rs

This file has not been changed.

crates/tranquil-pds/src/api/proxy.rs

This file has not been changed.

crates/tranquil-pds/src/api/repo/blob.rs

This file has not been changed.

crates/tranquil-pds/src/api/repo/import.rs

This file has not been changed.

crates/tranquil-pds/src/api/repo/record/batch.rs

This file has not been changed.

crates/tranquil-pds/src/api/repo/record/delete.rs

This file has not been changed.

crates/tranquil-pds/src/api/repo/record/write.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/account_status.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/app_password.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/email.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/invite.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/migration.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/passkeys.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/password.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/reauth.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/service_auth.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/session.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/totp.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/trusted_devices.rs

This file has not been changed.

crates/tranquil-pds/src/api/temp.rs

This file has not been changed.

crates/tranquil-pds/src/auth/auth_extractor.rs

This file has not been changed.

+22 -81
crates/tranquil-pds/src/auth/extractor.rs
··· 27 AccountTakedown, 28 AdminRequired, 29 ServiceAuthNotAllowed, 30 - SigningKeyRequired, 31 InsufficientScope(String), 32 OAuthExpiredToken(String), 33 UseDpopNonce(String), ··· 430 } 431 } 432 433 pub enum AuthAny<P: AuthPolicy = Active> { 434 User(Auth<P>), 435 Service(ServiceAuth), ··· 514 Err(AuthError::MissingToken) => Ok(None), 515 Err(e) => Err(e), 516 } 517 - } 518 - } 519 - 520 - pub struct SigningAuth<P: AuthPolicy = Active> { 521 - pub did: Did, 522 - pub key_bytes: Vec<u8>, 523 - pub is_admin: bool, 524 - pub status: AccountStatus, 525 - pub scope: Option<String>, 526 - pub controller_did: Option<Did>, 527 - is_oauth: bool, 528 - _policy: PhantomData<P>, 529 - } 530 - 531 - impl<P: AuthPolicy> SigningAuth<P> { 532 - pub fn needs_scope_check(&self) -> bool { 533 - self.is_oauth 534 - } 535 - 536 - pub fn permissions(&self) -> ScopePermissions { 537 - if let Some(ref scope) = self.scope 538 - && scope != super::SCOPE_ACCESS 539 - { 540 - return ScopePermissions::from_scope_string(Some(scope)); 541 - } 542 - if !self.is_oauth { 543 - return ScopePermissions::from_scope_string(Some("atproto")); 544 - } 545 - ScopePermissions::from_scope_string(self.scope.as_deref()) 546 - } 547 - 548 - #[allow(clippy::result_large_err)] 549 - pub fn check_repo_scope(&self, action: RepoAction, collection: &str) -> Result<(), Response> { 550 - if !self.needs_scope_check() { 551 - return Ok(()); 552 - } 553 - self.permissions() 554 - .assert_repo(action, collection) 555 - .map_err(|e| ApiError::InsufficientScope(Some(e.to_string())).into_response()) 556 - } 557 - } 558 - 559 - impl<P: AuthPolicy> FromRequestParts<AppState> for SigningAuth<P> { 560 - type Rejection = AuthError; 561 - 562 - async fn from_request_parts( 563 - parts: &mut Parts, 564 - state: &AppState, 565 - ) -> Result<Self, Self::Rejection> { 566 - let user = extract_user_auth_internal(parts, state).await?; 567 - P::validate(&user)?; 568 - 569 - let key_bytes = match user.key_bytes { 570 - Some(kb) => kb, 571 - None => { 572 - let user_with_key = state 573 - .user_repo 574 - .get_with_key_by_did(&user.did) 575 - .await 576 - .ok() 577 - .flatten() 578 - .ok_or(AuthError::SigningKeyRequired)?; 579 - crate::config::decrypt_key( 580 - &user_with_key.key_bytes, 581 - user_with_key.encryption_version, 582 - ) 583 - .map_err(|_| AuthError::SigningKeyRequired)? 584 - } 585 - }; 586 - 587 - Ok(SigningAuth { 588 - did: user.did, 589 - key_bytes, 590 - is_admin: user.is_admin, 591 - status: user.status, 592 - scope: user.scope, 593 - controller_did: user.controller_did, 594 - is_oauth: user.auth_source.is_oauth(), 595 - _policy: PhantomData, 596 - }) 597 } 598 } 599
··· 27 AccountTakedown, 28 AdminRequired, 29 ServiceAuthNotAllowed, 30 InsufficientScope(String), 31 OAuthExpiredToken(String), 32 UseDpopNonce(String), ··· 429 } 430 } 431 432 + impl OptionalFromRequestParts<AppState> for ServiceAuth { 433 + type Rejection = AuthError; 434 + 435 + async fn from_request_parts( 436 + parts: &mut Parts, 437 + state: &AppState, 438 + ) -> Result<Option<Self>, Self::Rejection> { 439 + match extract_auth_internal(parts, state).await { 440 + Ok(ExtractedAuth::Service(claims)) => { 441 + let did: Did = claims 442 + .iss 443 + .parse() 444 + .map_err(|_| AuthError::AuthenticationFailed)?; 445 + Ok(Some(ServiceAuth { did, claims })) 446 + } 447 + Ok(ExtractedAuth::User(_)) => Err(AuthError::AuthenticationFailed), 448 + Err(AuthError::MissingToken) => Ok(None), 449 + Err(e) => Err(e), 450 + } 451 + } 452 + } 453 + 454 pub enum AuthAny<P: AuthPolicy = Active> { 455 User(Auth<P>), 456 Service(ServiceAuth), ··· 535 Err(AuthError::MissingToken) => Ok(None), 536 Err(e) => Err(e), 537 } 538 } 539 } 540
+1 -2
crates/tranquil-pds/src/auth/mod.rs
··· 18 19 pub use extractor::{ 20 Active, Admin, AnyUser, Auth, AuthAny, AuthError, AuthPolicy, ExtractedToken, NotTakendown, 21 - Permissive, ServiceAuth, SigningAuth, extract_auth_token_from_header, 22 - extract_bearer_token_from_header, 23 }; 24 pub use service::{ServiceTokenClaims, ServiceTokenVerifier, is_service_token}; 25
··· 18 19 pub use extractor::{ 20 Active, Admin, AnyUser, Auth, AuthAny, AuthError, AuthPolicy, ExtractedToken, NotTakendown, 21 + Permissive, ServiceAuth, extract_auth_token_from_header, extract_bearer_token_from_header, 22 }; 23 pub use service::{ServiceTokenClaims, ServiceTokenVerifier, is_service_token}; 24
crates/tranquil-pds/src/lib.rs

This file has not been changed.

crates/tranquil-pds/src/oauth/endpoints/authorize.rs

This file has not been changed.

crates/tranquil-pds/src/oauth/endpoints/delegation.rs

This file has not been changed.

crates/tranquil-pds/src/oauth/verify.rs

This file has not been changed.

crates/tranquil-pds/src/sso/endpoints.rs

This file has not been changed.

crates/tranquil-pds/tests/actor.rs

This file has not been changed.

+65
crates/tranquil-pds/tests/auth_extractor.rs
··· 581 let proof = format!("{}.{}", signing_input, sig_b64); 582 (jwk, proof) 583 }
··· 581 let proof = format!("{}.{}", signing_input, sig_b64); 582 (jwk, proof) 583 } 584 + 585 + #[tokio::test] 586 + async fn test_optional_service_auth_extractor_behavior() { 587 + let url = base_url().await; 588 + let http_client = client(); 589 + let (access_jwt, did) = create_account_and_login(&http_client).await; 590 + 591 + let service_auth_res = http_client 592 + .get(format!("{}/xrpc/com.atproto.server.getServiceAuth", url)) 593 + .bearer_auth(&access_jwt) 594 + .query(&[("aud", "did:web:test.example")]) 595 + .send() 596 + .await 597 + .unwrap(); 598 + assert_eq!(service_auth_res.status(), StatusCode::OK); 599 + let service_body: Value = service_auth_res.json().await.unwrap(); 600 + let service_token = service_body["token"].as_str().unwrap(); 601 + 602 + let no_auth_res = http_client 603 + .get(format!( 604 + "{}/xrpc/com.atproto.sync.getBlob?did={}&cid=bafyreifakecidfornowfakecidfornow1234567", 605 + url, did 606 + )) 607 + .send() 608 + .await 609 + .unwrap(); 610 + assert!( 611 + no_auth_res.status() == StatusCode::NOT_FOUND 612 + || no_auth_res.status() == StatusCode::BAD_REQUEST, 613 + "getBlob with no auth should reach handler (AuthAny optional path) - got {}", 614 + no_auth_res.status() 615 + ); 616 + 617 + let service_auth_blob_res = http_client 618 + .get(format!( 619 + "{}/xrpc/com.atproto.sync.getBlob?did={}&cid=bafyreifakecidfornowfakecidfornow1234567", 620 + url, did 621 + )) 622 + .bearer_auth(service_token) 623 + .send() 624 + .await 625 + .unwrap(); 626 + assert!( 627 + service_auth_blob_res.status() == StatusCode::NOT_FOUND 628 + || service_auth_blob_res.status() == StatusCode::BAD_REQUEST, 629 + "getBlob with service auth should reach handler (AuthAny service path) - got {}", 630 + service_auth_blob_res.status() 631 + ); 632 + 633 + let user_auth_blob_res = http_client 634 + .get(format!( 635 + "{}/xrpc/com.atproto.sync.getBlob?did={}&cid=bafyreifakecidfornowfakecidfornow1234567", 636 + url, did 637 + )) 638 + .bearer_auth(&access_jwt) 639 + .send() 640 + .await 641 + .unwrap(); 642 + assert!( 643 + user_auth_blob_res.status() == StatusCode::NOT_FOUND 644 + || user_auth_blob_res.status() == StatusCode::BAD_REQUEST, 645 + "getBlob with user auth should reach handler (AuthAny user path) - got {}", 646 + user_auth_blob_res.status() 647 + ); 648 + }
crates/tranquil-pds/tests/common/mod.rs

This file has not been changed.

crates/tranquil-pds/tests/oauth_security.rs

This file has not been changed.

crates/tranquil-scopes/Cargo.toml

This file has not been changed.

crates/tranquil-scopes/src/permission_set.rs

This file has not been changed.

crates/tranquil-scopes/src/permissions.rs

This file has not been changed.

crates/tranquil-storage/src/lib.rs

This file has not been changed.

frontend/src/lib/api.ts

This file has not been changed.

frontend/src/lib/auth.svelte.ts

This file has not been changed.

frontend/src/lib/migration/atproto-client.ts

This file has not been changed.

frontend/src/lib/migration/flow.svelte.ts

This file has not been changed.

frontend/src/lib/migration/offline-flow.svelte.ts

This file has not been changed.

frontend/src/lib/oauth.ts

This file has not been changed.

frontend/src/locales/en.json

This file has not been changed.

frontend/src/locales/fi.json

This file has not been changed.

frontend/src/locales/ja.json

This file has not been changed.

frontend/src/locales/ko.json

This file has not been changed.

frontend/src/locales/sv.json

This file has not been changed.

frontend/src/locales/zh.json

This file has not been changed.

frontend/src/routes/Migration.svelte

This file has not been changed.

frontend/src/routes/OAuthAccounts.svelte

This file has not been changed.

frontend/src/routes/OAuthConsent.svelte

This file has not been changed.

History

6 rounds 1 comment
sign up or login to add to the discussion
lewis.moe submitted #5
3 commits
expand
fix: oauth consolidation, include-scope improvements
fix: consolidate auth extractors & standardize usage
fix: match ref pds permission-levels for some endpoints
expand 0 comments
pull request successfully merged
3 commits
expand
fix: oauth consolidation, include-scope improvements
fix: consolidate auth extractors & standardize usage
fix: match ref pds permission-levels for some endpoints
expand 0 comments
2 commits
expand
fix: oauth consolidation, include-scope improvements
fix: consolidate auth extractors & standardize usage
expand 0 comments
2 commits
expand
fix: oauth consolidation, include-scope improvements
fix: consolidate auth extractors & standardize usage
expand 1 comment

so three things:

crates/tranquil-pds/src/auth/auth_extractor.rs does not seem to be used at all anywhere?

crates/tranquil-pds/src/api/temp.rs feels like it just ... shouldnt excist based on the name

and i still dont really like these extractors. the separation of inter service auth is weird to me. inter-service auth is a form of user auth. it shouldnt be separated out from the other types. the AuthExtractor should just be AuthExtractor(pub AuthenticatedUser).

i also discovered https://docs.rs/axum/0.8.8/axum/extract/trait.OptionalFromRequestParts.html which we should be able to reduce optional vs not optional with just an AuthExtractor vs Option.

principly id want whether or not its required that the account is active or not and whether its an admin account or not to both also be type safe configurations on the extractor. probably with generics of some sort. but i cant think of a specific design i like right now so. if you come up with one feel free to do it. otherwise we can do it later

1 commit
expand
fix: oauth consolidation, include-scope improvements
expand 0 comments
lewis.moe submitted #0
1 commit
expand
fix: oauth consolidation, include-scope improvements
expand 0 comments