diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f27681d8e..780ffaf19a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ ### Glean - Updated to v65.0.0 ([#6901](https://github.com/mozilla/application-services/pull/6901)) +### Android +- Added service parameter to fxa-client flow allowing clients to specify the list of services to request. ([bug 1925091](https://bugzilla.mozilla.org/show_bug.cgi?id=1925091)) + # v143.0 (_2025-08-18_) ## 🦊 What's Changed 🦊 diff --git a/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/FxaClient.kt b/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/FxaClient.kt index ac60aa81b5..b8d61b78e6 100644 --- a/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/FxaClient.kt +++ b/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/FxaClient.kt @@ -113,9 +113,10 @@ class FxaClient(inner: FirefoxAccount, persistCallback: PersistCallback?) : Auto fun beginOAuthFlow( scopes: Array, entrypoint: String, + services: List = listOf("sync"), ): String { return withMetrics { - this.inner.beginOauthFlow(scopes.toList(), entrypoint) + this.inner.beginOauthFlow(scopes.toList(), entrypoint, services) } } @@ -133,9 +134,10 @@ class FxaClient(inner: FirefoxAccount, persistCallback: PersistCallback?) : Auto pairingUrl: String, scopes: Array, entrypoint: String, + services: List = listOf("sync"), ): String { return withMetrics { - this.inner.beginPairingFlow(pairingUrl, scopes.toList(), entrypoint) + this.inner.beginPairingFlow(pairingUrl, scopes.toList(), entrypoint, services) } } diff --git a/components/fxa-client/src/auth.rs b/components/fxa-client/src/auth.rs index 1487fd265f..8301c457bc 100644 --- a/components/fxa-client/src/auth.rs +++ b/components/fxa-client/src/auth.rs @@ -76,6 +76,7 @@ impl FirefoxAccount { /// - This parameter is used for metrics purposes, to identify the /// UX entrypoint from which the user triggered the signin request. /// For example, the application toolbar, on the onboarding flow. + /// - `service` - list of services to request. /// - `metrics` - optionally, additional metrics tracking parameters. /// - These will be included as query parameters in the resulting URL. #[handle_error(Error)] @@ -84,9 +85,13 @@ impl FirefoxAccount { // Allow both &[String] and &[&str] since UniFFI can't represent `&[&str]` yet, scopes: &[T], entrypoint: &str, + service: &[T], ) -> ApiResult { let scopes = scopes.iter().map(T::as_ref).collect::>(); - self.internal.lock().begin_oauth_flow(&scopes, entrypoint) + let service = service.iter().map(T::as_ref).collect::>(); + self.internal + .lock() + .begin_oauth_flow(&scopes, entrypoint, &service) } /// Get the URL at which to begin a device-pairing signin flow. @@ -121,6 +126,7 @@ impl FirefoxAccount { /// - This parameter is used for metrics purposes, to identify the /// UX entrypoint from which the user triggered the signin request. /// For example, the application toolbar, on the onboarding flow. + /// - `service` - list of services to request. /// - `metrics` - optionally, additional metrics tracking parameters. /// - These will be included as query parameters in the resulting URL. #[handle_error(Error)] @@ -129,12 +135,14 @@ impl FirefoxAccount { pairing_url: &str, scopes: &[String], entrypoint: &str, + service: &[String], ) -> ApiResult { // UniFFI can't represent `&[&str]` yet, so convert it internally here. let scopes = scopes.iter().map(String::as_str).collect::>(); + let service = service.iter().map(String::as_str).collect::>(); self.internal .lock() - .begin_pairing_flow(pairing_url, &scopes, entrypoint) + .begin_pairing_flow(pairing_url, &scopes, entrypoint, &service) } /// Complete an OAuth flow. @@ -263,6 +271,7 @@ pub enum FxaEvent { BeginOAuthFlow { scopes: Vec, entrypoint: String, + service: Vec, }, /// Begin an oauth flow using a URL from a pairing code /// @@ -276,6 +285,7 @@ pub enum FxaEvent { pairing_url: String, scopes: Vec, entrypoint: String, + service: Vec, }, /// Complete an OAuth flow. /// diff --git a/components/fxa-client/src/fxa_client.udl b/components/fxa-client/src/fxa_client.udl index 2feb48ab7f..0f5e7add5a 100644 --- a/components/fxa-client/src/fxa_client.udl +++ b/components/fxa-client/src/fxa_client.udl @@ -189,13 +189,12 @@ interface FirefoxAccount { /// - This parameter is used for metrics purposes, to identify the /// UX entrypoint from which the user triggered the signin request. /// For example, the application toolbar, on the onboarding flow. + /// - `service` - list of services to request. /// - `metrics` - optionally, additional metrics tracking parameters. /// - These will be included as query parameters in the resulting URL. /// [Throws=FxaError] - string begin_oauth_flow([ByRef] sequence scopes, [ByRef] string entrypoint); - - + string begin_oauth_flow([ByRef] sequence scopes, [ByRef] string entrypoint, [ByRef] optional sequence service = []); /// Get the URL at which to begin a device-pairing signin flow. /// /// If the user wants to sign in using device pairing, call this method and then @@ -228,12 +227,13 @@ interface FirefoxAccount { /// - This parameter is used for metrics purposes, to identify the /// UX entrypoint from which the user triggered the signin request. /// For example, the application toolbar, on the onboarding flow. + /// - `service` - list of services to request. /// - `metrics` - optionally, additional metrics tracking parameters. /// - These will be included as query parameters in the resulting URL. /// [Throws=FxaError] - string begin_pairing_flow([ByRef] string pairing_url, [ByRef] sequence scopes, [ByRef] string entrypoint); - + string begin_pairing_flow([ByRef] string pairing_url, [ByRef] sequence scopes, [ByRef] string entrypoint, [ByRef] optional sequence service = []); + /// Complete an OAuth flow. /// @@ -1001,8 +1001,8 @@ interface FxaState { [Enum] interface FxaEvent { Initialize(DeviceConfig device_config); - BeginOAuthFlow(sequence scopes, string entrypoint); - BeginPairingFlow(string pairing_url, sequence scopes, string entrypoint); + BeginOAuthFlow(sequence scopes, string entrypoint, sequence service); + BeginPairingFlow(string pairing_url, sequence scopes, string entrypoint, sequence service); CompleteOAuthFlow(string code, string state); CancelOAuthFlow(); CheckAuthorizationStatus(); @@ -1137,8 +1137,8 @@ interface FxaStateCheckerEvent { [Enum] interface FxaStateCheckerState { GetAuthState(); - BeginOAuthFlow(sequence scopes, string entrypoint); - BeginPairingFlow(string pairing_url, sequence scopes, string entrypoint); + BeginOAuthFlow(sequence scopes, string entrypoint, sequence service); + BeginPairingFlow(string pairing_url, sequence scopes, string entrypoint, sequence service); CompleteOAuthFlow(string code, string state); InitializeDevice(); EnsureDeviceCapabilities(); diff --git a/components/fxa-client/src/internal/oauth.rs b/components/fxa-client/src/internal/oauth.rs index 6a91decb24..51c1b798f3 100644 --- a/components/fxa-client/src/internal/oauth.rs +++ b/components/fxa-client/src/internal/oauth.rs @@ -128,15 +128,20 @@ impl FirefoxAccount { /// the pairing authority. /// * `scopes` - Space-separated list of requested scopes by the pairing supplicant. /// * `entrypoint` - The entrypoint to be used for data collection + /// * `service` - Space-separated list of requested services. /// * `metrics` - Optional parameters for metrics pub fn begin_pairing_flow( &mut self, pairing_url: &str, scopes: &[&str], entrypoint: &str, + service: &[&str], ) -> Result { let mut url = self.state.config().pair_supp_url()?; - url.query_pairs_mut().append_pair("entrypoint", entrypoint); + let service_param = service.join(","); + url.query_pairs_mut() + .append_pair("entrypoint", entrypoint) + .append_pair("service", &service_param); let pairing_url = Url::parse(pairing_url)?; if url.host_str() != pairing_url.host_str() { let fxa_server = FxaServer::from(&url); @@ -153,19 +158,26 @@ impl FirefoxAccount { /// /// * `scopes` - Space-separated list of requested scopes. /// * `entrypoint` - The entrypoint to be used for metrics + /// * `service` - Space-separated list of requested services. /// * `metrics` - Optional metrics parameters - pub fn begin_oauth_flow(&mut self, scopes: &[&str], entrypoint: &str) -> Result { + pub fn begin_oauth_flow( + &mut self, + scopes: &[&str], + entrypoint: &str, + service: &[&str], + ) -> Result { self.state.on_begin_oauth(); let mut url = if self.state.last_seen_profile().is_some() { self.state.config().oauth_force_auth_url()? } else { self.state.config().authorization_endpoint()? }; - + let service_param = service.join(","); url.query_pairs_mut() .append_pair("action", "email") .append_pair("response_type", "code") - .append_pair("entrypoint", entrypoint); + .append_pair("entrypoint", entrypoint) + .append_pair("service", &service_param); if let Some(cached_profile) = self.state.last_seen_profile() { url.query_pairs_mut() @@ -621,7 +633,7 @@ mod tests { ); let mut fxa = FirefoxAccount::with_config(config); let url = fxa - .begin_oauth_flow(&["profile"], "test_oauth_flow_url") + .begin_oauth_flow(&["profile"], "test_oauth_flow_url", &["sync"]) .unwrap(); let flow_url = Url::parse(&url).unwrap(); @@ -629,7 +641,7 @@ mod tests { assert_eq!(flow_url.path(), "/authorization"); let mut pairs = flow_url.query_pairs(); - assert_eq!(pairs.count(), 11); + assert_eq!(pairs.count(), 12); assert_eq!( pairs.next(), Some((Cow::Borrowed("action"), Cow::Borrowed("email"))) @@ -645,6 +657,10 @@ mod tests { Cow::Borrowed("test_oauth_flow_url") )) ); + assert_eq!( + pairs.next(), + Some((Cow::Borrowed("service"), Cow::Borrowed("sync"))) + ); assert_eq!( pairs.next(), Some((Cow::Borrowed("client_id"), Cow::Borrowed("12345678"))) @@ -692,7 +708,7 @@ mod tests { let email = "test@example.com"; fxa.add_cached_profile("123", email); let url = fxa - .begin_oauth_flow(&["profile"], "test_force_auth_url") + .begin_oauth_flow(&["profile"], "test_force_auth_url", &["sync"]) .unwrap(); let url = Url::parse(&url).unwrap(); assert_eq!(url.path(), "/oauth/force_auth"); @@ -716,7 +732,7 @@ mod tests { ); let mut fxa = FirefoxAccount::with_config(config); let url = fxa - .begin_oauth_flow(SCOPES, "test_webchannel_context_url") + .begin_oauth_flow(SCOPES, "test_webchannel_context_url", &["sync"]) .unwrap(); let url = Url::parse(&url).unwrap(); let query_params: HashMap<_, _> = url.query_pairs().into_owned().collect(); @@ -738,7 +754,12 @@ mod tests { ); let mut fxa = FirefoxAccount::with_config(config); let url = fxa - .begin_pairing_flow(PAIRING_URL, SCOPES, "test_webchannel_pairing_context_url") + .begin_pairing_flow( + PAIRING_URL, + SCOPES, + "test_webchannel_pairing_context_url", + &["sync"], + ) .unwrap(); let url = Url::parse(&url).unwrap(); let query_params: HashMap<_, _> = url.query_pairs().into_owned().collect(); @@ -762,7 +783,7 @@ mod tests { let mut fxa = FirefoxAccount::with_config(config); let url = fxa - .begin_pairing_flow(PAIRING_URL, SCOPES, "test_pairing_flow_url") + .begin_pairing_flow(PAIRING_URL, SCOPES, "test_pairing_flow_url", &["sync"]) .unwrap(); let flow_url = Url::parse(&url).unwrap(); let expected_parsed_url = Url::parse(EXPECTED_URL).unwrap(); @@ -772,7 +793,7 @@ mod tests { assert_eq!(flow_url.fragment(), expected_parsed_url.fragment()); let mut pairs = flow_url.query_pairs(); - assert_eq!(pairs.count(), 9); + assert_eq!(pairs.count(), 10); assert_eq!( pairs.next(), Some(( @@ -780,6 +801,10 @@ mod tests { Cow::Borrowed("test_pairing_flow_url") )) ); + assert_eq!( + pairs.next(), + Some((Cow::Borrowed("service"), Cow::Borrowed("sync"))) + ); assert_eq!( pairs.next(), Some((Cow::Borrowed("client_id"), Cow::Borrowed("12345678"))) @@ -832,6 +857,7 @@ mod tests { PAIRING_URL, &["https://identity.mozilla.com/apps/oldsync"], "test_pairiong_flow_origin_mismatch", + &["sync"], ); assert!(url.is_err()); @@ -1099,7 +1125,7 @@ mod tests { ); let mut fxa = FirefoxAccount::with_config(config); let url = fxa - .begin_oauth_flow(&[OLD_SYNC, "profile"], "test_entrypoint") + .begin_oauth_flow(&[OLD_SYNC, "profile"], "test_entrypoint", &["sync"]) .unwrap(); let url = Url::parse(&url).unwrap(); let state = url.query_pairs().find(|(name, _)| name == "state").unwrap(); diff --git a/components/fxa-client/src/state_machine/checker.rs b/components/fxa-client/src/state_machine/checker.rs index ffd760caff..62bdc48a5a 100644 --- a/components/fxa-client/src/state_machine/checker.rs +++ b/components/fxa-client/src/state_machine/checker.rs @@ -23,11 +23,13 @@ pub enum FxaStateCheckerState { BeginOAuthFlow { scopes: Vec, entrypoint: String, + service: Vec, }, BeginPairingFlow { pairing_url: String, scopes: Vec, entrypoint: String, + service: Vec, }, CompleteOAuthFlow { code: String, @@ -217,17 +219,25 @@ impl From for FxaStateCheckerState { fn from(state: InternalState) -> Self { match state { InternalState::GetAuthState => Self::GetAuthState, - InternalState::BeginOAuthFlow { scopes, entrypoint } => { - Self::BeginOAuthFlow { scopes, entrypoint } - } + InternalState::BeginOAuthFlow { + scopes, + entrypoint, + service, + } => Self::BeginOAuthFlow { + scopes, + entrypoint, + service, + }, InternalState::BeginPairingFlow { pairing_url, scopes, entrypoint, + service, } => Self::BeginPairingFlow { pairing_url, scopes, entrypoint, + service, }, InternalState::CompleteOAuthFlow { code, state } => { Self::CompleteOAuthFlow { code, state } @@ -247,17 +257,25 @@ impl From for InternalState { fn from(state: FxaStateCheckerState) -> Self { match state { FxaStateCheckerState::GetAuthState => Self::GetAuthState, - FxaStateCheckerState::BeginOAuthFlow { scopes, entrypoint } => { - Self::BeginOAuthFlow { scopes, entrypoint } - } + FxaStateCheckerState::BeginOAuthFlow { + scopes, + entrypoint, + service, + } => Self::BeginOAuthFlow { + scopes, + entrypoint, + service, + }, FxaStateCheckerState::BeginPairingFlow { pairing_url, scopes, entrypoint, + service, } => Self::BeginPairingFlow { pairing_url, scopes, entrypoint, + service, }, FxaStateCheckerState::CompleteOAuthFlow { code, state } => { Self::CompleteOAuthFlow { code, state } diff --git a/components/fxa-client/src/state_machine/internal_machines/auth_issues.rs b/components/fxa-client/src/state_machine/internal_machines/auth_issues.rs index 1deef7a9ad..28f6784b3b 100644 --- a/components/fxa-client/src/state_machine/internal_machines/auth_issues.rs +++ b/components/fxa-client/src/state_machine/internal_machines/auth_issues.rs @@ -14,9 +14,14 @@ use State::*; impl InternalStateMachine for AuthIssuesStateMachine { fn initial_state(&self, event: FxaEvent) -> Result { match event { - FxaEvent::BeginOAuthFlow { scopes, entrypoint } => Ok(BeginOAuthFlow { + FxaEvent::BeginOAuthFlow { + scopes, + entrypoint, + service, + } => Ok(BeginOAuthFlow { scopes: scopes.clone(), entrypoint: entrypoint.clone(), + service: service.clone(), }), FxaEvent::Disconnect => Ok(Complete(FxaState::Disconnected)), e => Err(Error::InvalidStateTransition(format!("AuthIssues -> {e}"))), @@ -46,6 +51,7 @@ mod test { FxaEvent::BeginOAuthFlow { scopes: vec!["profile".to_owned()], entrypoint: "test-entrypoint".to_owned(), + service: vec!["sync".to_owned()], }, ); @@ -53,7 +59,8 @@ mod test { tester.state, BeginOAuthFlow { scopes: vec!["profile".to_owned()], - entrypoint: "test-entrypoint".to_owned() + entrypoint: "test-entrypoint".to_owned(), + service: vec!["sync".to_owned()], } ); assert_eq!(tester.peek_next_state(CallError), Cancel); diff --git a/components/fxa-client/src/state_machine/internal_machines/authenticating.rs b/components/fxa-client/src/state_machine/internal_machines/authenticating.rs index 0b69c31ad1..658ce98088 100644 --- a/components/fxa-client/src/state_machine/internal_machines/authenticating.rs +++ b/components/fxa-client/src/state_machine/internal_machines/authenticating.rs @@ -21,17 +21,25 @@ impl InternalStateMachine for AuthenticatingStateMachine { FxaEvent::CancelOAuthFlow => Ok(Complete(FxaState::Disconnected)), // These next 2 cases allow apps to begin a new oauth flow when we're already in the // middle of an existing one. - FxaEvent::BeginOAuthFlow { scopes, entrypoint } => { - Ok(State::BeginOAuthFlow { scopes, entrypoint }) - } + FxaEvent::BeginOAuthFlow { + scopes, + entrypoint, + service, + } => Ok(State::BeginOAuthFlow { + scopes, + entrypoint, + service, + }), FxaEvent::BeginPairingFlow { pairing_url, scopes, entrypoint, + service, } => Ok(State::BeginPairingFlow { pairing_url, scopes, entrypoint, + service, }), e => Err(Error::InvalidStateTransition(format!( "Authenticating -> {e}" @@ -114,6 +122,7 @@ mod test { FxaEvent::BeginOAuthFlow { scopes: vec!["profile".to_owned()], entrypoint: "test-entrypoint".to_owned(), + service: vec!["sync".to_owned()], }, ); assert_eq!( @@ -121,6 +130,7 @@ mod test { BeginOAuthFlow { scopes: vec!["profile".to_owned()], entrypoint: "test-entrypoint".to_owned(), + service: vec!["sync".to_owned()], } ); assert_eq!( @@ -146,6 +156,7 @@ mod test { pairing_url: "https://example.com/pairing-url".to_owned(), scopes: vec!["profile".to_owned()], entrypoint: "test-entrypoint".to_owned(), + service: vec!["sync".to_owned()], }, ); assert_eq!( @@ -154,6 +165,7 @@ mod test { pairing_url: "https://example.com/pairing-url".to_owned(), scopes: vec!["profile".to_owned()], entrypoint: "test-entrypoint".to_owned(), + service: vec!["sync".to_owned()], } ); assert_eq!( diff --git a/components/fxa-client/src/state_machine/internal_machines/disconnected.rs b/components/fxa-client/src/state_machine/internal_machines/disconnected.rs index 9d99f1686c..f65df2eaee 100644 --- a/components/fxa-client/src/state_machine/internal_machines/disconnected.rs +++ b/components/fxa-client/src/state_machine/internal_machines/disconnected.rs @@ -14,17 +14,25 @@ use State::*; impl InternalStateMachine for DisconnectedStateMachine { fn initial_state(&self, event: FxaEvent) -> Result { match event { - FxaEvent::BeginOAuthFlow { scopes, entrypoint } => { - Ok(State::BeginOAuthFlow { scopes, entrypoint }) - } + FxaEvent::BeginOAuthFlow { + scopes, + entrypoint, + service, + } => Ok(State::BeginOAuthFlow { + scopes, + entrypoint, + service, + }), FxaEvent::BeginPairingFlow { pairing_url, scopes, entrypoint, + service, } => Ok(State::BeginPairingFlow { pairing_url, scopes, entrypoint, + service, }), e => Err(Error::InvalidStateTransition(format!( "Disconnected -> {e}" @@ -59,6 +67,7 @@ mod test { FxaEvent::BeginOAuthFlow { scopes: vec!["profile".to_owned()], entrypoint: "test-entrypoint".to_owned(), + service: vec!["sync".to_owned()], }, ); assert_eq!( @@ -66,6 +75,7 @@ mod test { BeginOAuthFlow { scopes: vec!["profile".to_owned()], entrypoint: "test-entrypoint".to_owned(), + service: vec!["sync".to_owned()], } ); assert_eq!(tester.peek_next_state(CallError), Cancel); @@ -87,6 +97,7 @@ mod test { pairing_url: "https://example.com/pairing-url".to_owned(), scopes: vec!["profile".to_owned()], entrypoint: "test-entrypoint".to_owned(), + service: vec!["sync".to_owned()], }, ); assert_eq!( @@ -95,6 +106,7 @@ mod test { pairing_url: "https://example.com/pairing-url".to_owned(), scopes: vec!["profile".to_owned()], entrypoint: "test-entrypoint".to_owned(), + service: vec!["sync".to_owned()], } ); assert_eq!(tester.peek_next_state(CallError), Cancel); diff --git a/components/fxa-client/src/state_machine/internal_machines/mod.rs b/components/fxa-client/src/state_machine/internal_machines/mod.rs index f7d200653f..941a70c9ce 100644 --- a/components/fxa-client/src/state_machine/internal_machines/mod.rs +++ b/components/fxa-client/src/state_machine/internal_machines/mod.rs @@ -41,11 +41,13 @@ pub enum State { BeginOAuthFlow { scopes: Vec, entrypoint: String, + service: Vec, }, BeginPairingFlow { pairing_url: String, scopes: Vec, entrypoint: String, + service: Vec, }, CompleteOAuthFlow { code: String, @@ -124,20 +126,28 @@ impl State { account.ensure_capabilities(&device_config.capabilities)?; Event::EnsureDeviceCapabilitiesSuccess } - State::BeginOAuthFlow { scopes, entrypoint } => { + State::BeginOAuthFlow { + scopes, + entrypoint, + service, + } => { account.cancel_existing_oauth_flows(); let scopes: Vec<&str> = scopes.iter().map(String::as_str).collect(); - let oauth_url = account.begin_oauth_flow(&scopes, entrypoint)?; + let service: Vec<&str> = service.iter().map(AsRef::as_ref).collect(); + let oauth_url = account.begin_oauth_flow(&scopes, entrypoint, &service)?; Event::BeginOAuthFlowSuccess { oauth_url } } State::BeginPairingFlow { pairing_url, scopes, entrypoint, + service, } => { account.cancel_existing_oauth_flows(); let scopes: Vec<&str> = scopes.iter().map(String::as_str).collect(); - let oauth_url = account.begin_pairing_flow(pairing_url, &scopes, entrypoint)?; + let service: Vec<&str> = service.iter().map(AsRef::as_ref).collect(); + let oauth_url = + account.begin_pairing_flow(pairing_url, &scopes, entrypoint, &service)?; Event::BeginPairingFlowSuccess { oauth_url } } State::CompleteOAuthFlow { code, state } => { diff --git a/examples/cli-support/src/fxa_creds.rs b/examples/cli-support/src/fxa_creds.rs index 36d953d963..172299b8d0 100644 --- a/examples/cli-support/src/fxa_creds.rs +++ b/examples/cli-support/src/fxa_creds.rs @@ -52,7 +52,7 @@ fn create_fxa_creds(path: &str, cfg: FxaConfig, scopes: &[&str]) -> Result Result<()> { - let oauth_uri = acct.begin_oauth_flow(scopes, "fxa_creds")?; + let oauth_uri = acct.begin_oauth_flow(scopes, "fxa_creds", &["sync"])?; if open::that(&oauth_uri).is_err() { log::warn!("Failed to open a web browser D:");