From 063d775a7e05b7bdfcbfbb472e96da0504f90d05 Mon Sep 17 00:00:00 2001 From: aecsocket <43144841+aecsocket@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:28:07 +0100 Subject: [PATCH 1/3] fix: require first-party tokens for paypal linking --- apps/labrinth/src/routes/internal/flows.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs index 0f791ed4d4..abaf0c87d6 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -1135,6 +1135,7 @@ pub struct AuthorizationInit { pub token: Option, /// If the user is already logged in, and is linking a PayPal account, /// this will be set to the user's auth token from the frontend. + /// Only first-party session tokens may authorize linking. pub auth_token: Option, } #[derive(Serialize, Deserialize, utoipa::ToSchema)] @@ -1170,7 +1171,11 @@ pub async fn init( // This can happen when linking to a PayPal account (logging in) when already // logged in. let existing_user_id = if let Some(auth_token) = &info.auth_token { - get_user_record_from_bearer_token( + if !auth_token.starts_with("mra_") { + return Err(AuthenticationError::InvalidCredentials); + } + + let (scopes, user) = get_user_record_from_bearer_token( &req, Some(auth_token), &**client, @@ -1178,10 +1183,14 @@ pub async fn init( &session_queue, false, ) - .await - .ok() - .flatten() - .map(|(_scopes, user)| user.id) + .await? + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + + if !scopes.contains(Scopes::USER_AUTH_WRITE) { + return Err(AuthenticationError::InvalidCredentials); + } + + Some(user.id) } else { None }; From 75e990e29e73ae9b22020067b3f4ec2cbb39c77b Mon Sep 17 00:00:00 2001 From: aecsocket <43144841+aecsocket@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:42:57 +0100 Subject: [PATCH 2/3] perform paypal acct linking in a txn with lock --- apps/labrinth/src/database/advisory_lock.rs | 4 ++++ apps/labrinth/src/routes/internal/flows.rs | 26 +++++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/apps/labrinth/src/database/advisory_lock.rs b/apps/labrinth/src/database/advisory_lock.rs index 5ac3bc844c..a90cca49fa 100644 --- a/apps/labrinth/src/database/advisory_lock.rs +++ b/apps/labrinth/src/database/advisory_lock.rs @@ -13,6 +13,7 @@ pub enum AdvisoryLock { DelphiFile(DBFileId), DelphiScan(Uuid), DiscordRoleEmailCampaign, + PayPalAccount(String), } impl AdvisoryLock { @@ -57,6 +58,9 @@ impl Display for AdvisoryLock { Self::DiscordRoleEmailCampaign => { formatter.write_str("discord_role_email_campaign") } + Self::PayPalAccount(payer_id) => { + write!(formatter, "labrinth:paypal-account:{payer_id}") + } } } } diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs index abaf0c87d6..625d1a778e 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -1351,11 +1351,6 @@ pub async fn auth_callback( .await .wrap_err("failed to get user from provider")?; - let user_id_opt = provider - .get_user_id(&oauth_user.id, &**client) - .await - .wrap_err("failed to get user ID from provider")?; - let mut transaction = client .begin() .await @@ -1376,6 +1371,22 @@ pub async fn auth_callback( return Err(AuthenticationError::AccountLocked); } + crate::database::advisory_lock::AdvisoryLock::PayPalAccount( + oauth_user.id.clone(), + ) + .acquire(&mut transaction) + .await + .wrap_err("failed to lock PayPal account for linking")?; + + let linked_user_id = provider + .get_user_id(&oauth_user.id, &mut transaction) + .await + .wrap_err("failed to get linked user ID from PayPal")?; + + if linked_user_id.is_some_and(|id| id != existing_user_id) { + return Err(AuthenticationError::ProviderAlreadyLinked); + } + sqlx::query!( " UPDATE users @@ -1407,6 +1418,11 @@ pub async fn auth_callback( .json(serde_json::json!({ "url": url }))); } + let user_id_opt = provider + .get_user_id(&oauth_user.id, &**client) + .await + .wrap_err("failed to get user ID from provider")?; + if let Some(id) = user_id { if user_id_opt.is_some() { return Err(AuthenticationError::ProviderAlreadyLinked); From 370e4b82193b3d71f8a102ea43642af357893edb Mon Sep 17 00:00:00 2001 From: aecsocket <43144841+aecsocket@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:31:37 +0100 Subject: [PATCH 3/3] dummy commit for CI