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
+28 -30
Interdiff #0 โ†’ #1
.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/error.rs

This file has not been changed.

crates/tranquil-pds/src/api/identity/account.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/record/delete.rs

This file has not been changed.

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

This file has not been changed.

+5 -5
crates/tranquil-pds/src/api/server/account_status.rs
··· 525 State(state): State<AppState>, 526 auth: crate::auth::BearerAuthAllowDeactivated, 527 ) -> Response { 528 - let did = auth.0.did.clone(); 529 530 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &did).await { 531 return crate::api::server::reauth::legacy_mfa_required_response( 532 &*state.user_repo, 533 &*state.session_repo, 534 - &did, 535 ) 536 .await; 537 } 538 539 - let user_id = match state.user_repo.get_id_by_did(&did).await { 540 Ok(Some(id)) => id, 541 _ => { 542 return ApiError::InternalError(None).into_response(); ··· 546 let expires_at = Utc::now() + Duration::minutes(15); 547 if let Err(e) = state 548 .infra_repo 549 - .create_deletion_request(&confirmation_token, &did, expires_at) 550 .await 551 { 552 error!("DB error creating deletion token: {:?}", e);
··· 525 State(state): State<AppState>, 526 auth: crate::auth::BearerAuthAllowDeactivated, 527 ) -> Response { 528 + let did = &auth.0.did; 529 530 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, did).await { 531 return crate::api::server::reauth::legacy_mfa_required_response( 532 &*state.user_repo, 533 &*state.session_repo, 534 + did, 535 ) 536 .await; 537 } 538 539 + let user_id = match state.user_repo.get_id_by_did(did).await { 540 Ok(Some(id)) => id, 541 _ => { 542 return ApiError::InternalError(None).into_response(); ··· 546 let expires_at = Utc::now() + Duration::minutes(15); 547 if let Err(e) = state 548 .infra_repo 549 + .create_deletion_request(&confirmation_token, did, expires_at) 550 .await 551 { 552 error!("DB error creating deletion token: {:?}", e);
crates/tranquil-pds/src/api/server/migration.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.

+2 -6
crates/tranquil-pds/src/auth/extractor.rs
··· 160 .await 161 { 162 Ok(result) => { 163 - let result_did: Did = result 164 - .did 165 - .parse() 166 - .map_err(|_| AuthError::AuthenticationFailed)?; 167 let user_info = state 168 .user_repo 169 - .get_user_info_by_did(&result_did) 170 .await 171 .ok() 172 .flatten() ··· 182 return Err(AuthError::AccountTakedown); 183 } 184 Ok(AuthenticatedUser { 185 - did: result_did, 186 key_bytes: user_info.key_bytes.and_then(|kb| { 187 crate::config::decrypt_key(&kb, user_info.encryption_version).ok() 188 }),
··· 160 .await 161 { 162 Ok(result) => { 163 let user_info = state 164 .user_repo 165 + .get_user_info_by_did(&result.did) 166 .await 167 .ok() 168 .flatten() ··· 178 return Err(AuthError::AccountTakedown); 179 } 180 Ok(AuthenticatedUser { 181 + did: result.did, 182 key_bytes: user_info.key_bytes.and_then(|kb| { 183 crate::config::decrypt_key(&kb, user_info.encryption_version).ok() 184 }),
crates/tranquil-pds/src/auth/mod.rs

This file has not been changed.

crates/tranquil-pds/src/lib.rs

This file has not been changed.

+4 -6
crates/tranquil-pds/src/oauth/endpoints/authorize.rs
··· 1118 .map(|s| s.to_string()) 1119 .collect(); 1120 let client_id_typed = ClientId::from(request_data.parameters.client_id.clone()); 1121 - let did_typed = tranquil_types::Did::from(did.to_string()); 1122 let needs_consent = should_show_consent( 1123 state.oauth_repo.as_ref(), 1124 - &did_typed, 1125 &client_id_typed, 1126 &requested_scopes, 1127 ) ··· 3648 headers: HeaderMap, 3649 auth: crate::auth::BearerAuth, 3650 ) -> Response { 3651 - let did = auth.0.did.clone(); 3652 - let did_typed = tranquil_types::Did::from(did.to_string()); 3653 3654 let existing_device = extract_device_cookie(&headers); 3655 ··· 3658 let device_typed = DeviceIdType::from(id.clone()); 3659 let _ = state 3660 .oauth_repo 3661 - .upsert_account_device(&did_typed, &device_typed) 3662 .await; 3663 (id, None) 3664 } ··· 3684 .into_response(); 3685 } 3686 3687 - if let Err(e) = state.oauth_repo.upsert_account_device(&did_typed, &device_typed).await { 3688 tracing::error!(error = ?e, "Failed to link device to account"); 3689 return ( 3690 StatusCode::INTERNAL_SERVER_ERROR,
··· 1118 .map(|s| s.to_string()) 1119 .collect(); 1120 let client_id_typed = ClientId::from(request_data.parameters.client_id.clone()); 1121 let needs_consent = should_show_consent( 1122 state.oauth_repo.as_ref(), 1123 + &did, 1124 &client_id_typed, 1125 &requested_scopes, 1126 ) ··· 3647 headers: HeaderMap, 3648 auth: crate::auth::BearerAuth, 3649 ) -> Response { 3650 + let did = &auth.0.did; 3651 3652 let existing_device = extract_device_cookie(&headers); 3653 ··· 3656 let device_typed = DeviceIdType::from(id.clone()); 3657 let _ = state 3658 .oauth_repo 3659 + .upsert_account_device(did, &device_typed) 3660 .await; 3661 (id, None) 3662 } ··· 3682 .into_response(); 3683 } 3684 3685 + if let Err(e) = state.oauth_repo.upsert_account_device(did, &device_typed).await { 3686 tracing::error!(error = ?e, "Failed to link device to account"); 3687 return ( 3688 StatusCode::INTERNAL_SERVER_ERROR,
crates/tranquil-pds/src/oauth/endpoints/delegation.rs

This file has not been changed.

+17 -13
crates/tranquil-pds/src/oauth/verify.rs
··· 10 use sha2::Sha256; 11 use subtle::ConstantTimeEq; 12 use tranquil_db_traits::{OAuthRepository, UserRepository}; 13 - use tranquil_types::TokenId; 14 15 use super::scopes::ScopePermissions; 16 use super::{DPoPVerifier, OAuthError}; 17 ··· 27 } 28 29 pub struct VerifyResult { 30 - pub did: String, 31 - pub token_id: String, 32 - pub client_id: String, 33 pub scope: Option<String>, 34 } 35 ··· 91 )); 92 } 93 } 94 Ok(VerifyResult { 95 - did: token_data.did, 96 - token_id: token_id.to_string(), 97 - client_id: token_data.client_id, 98 scope: token_data.scope, 99 }) 100 } ··· 202 } 203 204 pub struct OAuthUser { 205 - pub did: String, 206 - pub client_id: Option<String>, 207 pub scope: Option<String>, 208 pub is_oauth: bool, 209 pub permissions: ScopePermissions, ··· 382 } 383 384 struct LegacyAuthResult { 385 - did: String, 386 } 387 388 async fn try_legacy_auth( ··· 390 token: &str, 391 ) -> Result<LegacyAuthResult, ()> { 392 match crate::auth::validate_bearer_token(user_repo, token).await { 393 - Ok(user) if !user.is_oauth => Ok(LegacyAuthResult { 394 - did: user.did.to_string(), 395 - }), 396 _ => Err(()), 397 } 398 }
··· 10 use sha2::Sha256; 11 use subtle::ConstantTimeEq; 12 use tranquil_db_traits::{OAuthRepository, UserRepository}; 13 + use tranquil_types::{ClientId, TokenId}; 14 15 + use crate::types::Did; 16 + 17 use super::scopes::ScopePermissions; 18 use super::{DPoPVerifier, OAuthError}; 19 ··· 29 } 30 31 pub struct VerifyResult { 32 + pub did: Did, 33 + pub token_id: TokenId, 34 + pub client_id: ClientId, 35 pub scope: Option<String>, 36 } 37 ··· 93 )); 94 } 95 } 96 + let did: Did = token_data 97 + .did 98 + .parse() 99 + .map_err(|_| OAuthError::InvalidToken("Invalid DID in token".to_string()))?; 100 Ok(VerifyResult { 101 + did, 102 + token_id, 103 + client_id: ClientId::from(token_data.client_id), 104 scope: token_data.scope, 105 }) 106 } ··· 208 } 209 210 pub struct OAuthUser { 211 + pub did: Did, 212 + pub client_id: Option<ClientId>, 213 pub scope: Option<String>, 214 pub is_oauth: bool, 215 pub permissions: ScopePermissions, ··· 388 } 389 390 struct LegacyAuthResult { 391 + did: Did, 392 } 393 394 async fn try_legacy_auth( ··· 396 token: &str, 397 ) -> Result<LegacyAuthResult, ()> { 398 match crate::auth::validate_bearer_token(user_repo, token).await { 399 + Ok(user) if !user.is_oauth => Ok(LegacyAuthResult { did: user.did }), 400 _ => Err(()), 401 } 402 }
crates/tranquil-pds/tests/auth_extractor.rs

This file has not been changed.

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
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

lewis.moe submitted #1
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