diff --git a/src/auth/routes.rs b/src/auth/routes.rs index e5de216..c41ba38 100644 --- a/src/auth/routes.rs +++ b/src/auth/routes.rs @@ -20,6 +20,7 @@ const LEGACY_REDIRECT_COOKIE: &str = "happyview_redirect"; pub struct LoginQuery { handle: String, redirect_uri: Option, + scope: Option, } /// Parse a whitespace-separated OAuth scope string into typed `Scope` values. @@ -57,26 +58,31 @@ async fn login( jar: SignedCookieJar, Query(query): Query, ) -> Result<(SignedCookieJar, Json), AppError> { - tracing::debug!(handle = %query.handle, redirect_uri = ?query.redirect_uri, "login request"); - - // Read the configured scopes from the settings DB. Falls back to `atproto` - // only if unset — that matches what we serve from `/oauth-client-metadata.json`. - let scopes = match crate::admin::settings::get_setting( - &state.db, - "oauth_scopes", - state.db_backend, - ) - .await - { - Some(s) => { - let parsed = parse_scope_string(&s); - if parsed.is_empty() { - vec![Scope::Known(KnownScope::Atproto)] - } else { - parsed + tracing::debug!(handle = %query.handle, redirect_uri = ?query.redirect_uri, scope = ?query.scope, "login request"); + + // Use scopes from the query param if provided, otherwise fall back to the + // settings DB. The client metadata advertises all possible scopes, but each + // login request can ask for a subset. + let scopes = if let Some(ref scope_str) = query.scope { + let parsed = parse_scope_string(scope_str); + if parsed.is_empty() { + vec![Scope::Known(KnownScope::Atproto)] + } else { + parsed + } + } else { + match crate::admin::settings::get_setting(&state.db, "oauth_scopes", state.db_backend).await + { + Some(s) => { + let parsed = parse_scope_string(&s); + if parsed.is_empty() { + vec![Scope::Known(KnownScope::Atproto)] + } else { + parsed + } } + None => vec![Scope::Known(KnownScope::Atproto)], } - None => vec![Scope::Known(KnownScope::Atproto)], }; tracing::debug!(scopes = ?scopes, "resolved oauth scopes");