Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 🦊
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,10 @@ class FxaClient(inner: FirefoxAccount, persistCallback: PersistCallback?) : Auto
fun beginOAuthFlow(
scopes: Array<String>,
entrypoint: String,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should expose the service param here and use the default value from here since there is this thin wrapper around the rust generated FFI code.

Suggested change
entrypoint: String,
entrypoint: String,
services: List<String> = listOf("sync"),

services: List<String> = listOf("sync"),
): String {
return withMetrics {
this.inner.beginOauthFlow(scopes.toList(), entrypoint)
this.inner.beginOauthFlow(scopes.toList(), entrypoint, services)
}
}

Expand All @@ -133,9 +134,10 @@ class FxaClient(inner: FirefoxAccount, persistCallback: PersistCallback?) : Auto
pairingUrl: String,
scopes: Array<String>,
entrypoint: String,
services: List<String> = listOf("sync"),
): String {
return withMetrics {
this.inner.beginPairingFlow(pairingUrl, scopes.toList(), entrypoint)
this.inner.beginPairingFlow(pairingUrl, scopes.toList(), entrypoint, services)
}
}

Expand Down
14 changes: 12 additions & 2 deletions components/fxa-client/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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<String> {
let scopes = scopes.iter().map(T::as_ref).collect::<Vec<_>>();
self.internal.lock().begin_oauth_flow(&scopes, entrypoint)
let service = service.iter().map(T::as_ref).collect::<Vec<_>>();
self.internal
.lock()
.begin_oauth_flow(&scopes, entrypoint, &service)
}

/// Get the URL at which to begin a device-pairing signin flow.
Expand Down Expand Up @@ -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)]
Expand All @@ -129,12 +135,14 @@ impl FirefoxAccount {
pairing_url: &str,
scopes: &[String],
entrypoint: &str,
service: &[String],
) -> ApiResult<String> {
// UniFFI can't represent `&[&str]` yet, so convert it internally here.
let scopes = scopes.iter().map(String::as_str).collect::<Vec<_>>();
let service = service.iter().map(String::as_str).collect::<Vec<_>>();
self.internal
.lock()
.begin_pairing_flow(pairing_url, &scopes, entrypoint)
.begin_pairing_flow(pairing_url, &scopes, entrypoint, &service)
}

/// Complete an OAuth flow.
Expand Down Expand Up @@ -263,6 +271,7 @@ pub enum FxaEvent {
BeginOAuthFlow {
scopes: Vec<String>,
entrypoint: String,
service: Vec<String>,
},
/// Begin an oauth flow using a URL from a pairing code
///
Expand All @@ -276,6 +285,7 @@ pub enum FxaEvent {
pairing_url: String,
scopes: Vec<String>,
entrypoint: String,
service: Vec<String>,
},
/// Complete an OAuth flow.
///
Expand Down
18 changes: 9 additions & 9 deletions components/fxa-client/src/fxa_client.udl
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> scopes, [ByRef] string entrypoint);


string begin_oauth_flow([ByRef] sequence<string> scopes, [ByRef] string entrypoint, [ByRef] optional sequence<string> 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
Expand Down Expand Up @@ -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<string> scopes, [ByRef] string entrypoint);
string begin_pairing_flow([ByRef] string pairing_url, [ByRef] sequence<string> scopes, [ByRef] string entrypoint, [ByRef] optional sequence<string> service = []);


/// Complete an OAuth flow.
///
Expand Down Expand Up @@ -1001,8 +1001,8 @@ interface FxaState {
[Enum]
interface FxaEvent {
Initialize(DeviceConfig device_config);
BeginOAuthFlow(sequence<string> scopes, string entrypoint);
BeginPairingFlow(string pairing_url, sequence<string> scopes, string entrypoint);
BeginOAuthFlow(sequence<string> scopes, string entrypoint, sequence<string> service);
BeginPairingFlow(string pairing_url, sequence<string> scopes, string entrypoint, sequence<string> service);
CompleteOAuthFlow(string code, string state);
CancelOAuthFlow();
CheckAuthorizationStatus();
Expand Down Expand Up @@ -1137,8 +1137,8 @@ interface FxaStateCheckerEvent {
[Enum]
interface FxaStateCheckerState {
GetAuthState();
BeginOAuthFlow(sequence<string> scopes, string entrypoint);
BeginPairingFlow(string pairing_url, sequence<string> scopes, string entrypoint);
BeginOAuthFlow(sequence<string> scopes, string entrypoint, sequence<string> service);
BeginPairingFlow(string pairing_url, sequence<string> scopes, string entrypoint, sequence<string> service);
CompleteOAuthFlow(string code, string state);
InitializeDevice();
EnsureDeviceCapabilities();
Expand Down
50 changes: 38 additions & 12 deletions components/fxa-client/src/internal/oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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);
Expand All @@ -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<String> {
pub fn begin_oauth_flow(
&mut self,
scopes: &[&str],
entrypoint: &str,
service: &[&str],
) -> Result<String> {
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()
Expand Down Expand Up @@ -621,15 +633,15 @@ 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();

assert_eq!(flow_url.host_str(), Some("accounts.firefox.com"));
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")))
Expand All @@ -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")))
Expand Down Expand Up @@ -692,7 +708,7 @@ mod tests {
let email = "[email protected]";
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");
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -772,14 +793,18 @@ 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((
Cow::Borrowed("entrypoint"),
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")))
Expand Down Expand Up @@ -832,6 +857,7 @@ mod tests {
PAIRING_URL,
&["https://identity.mozilla.com/apps/oldsync"],
"test_pairiong_flow_origin_mismatch",
&["sync"],
);

assert!(url.is_err());
Expand Down Expand Up @@ -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();
Expand Down
30 changes: 24 additions & 6 deletions components/fxa-client/src/state_machine/checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ pub enum FxaStateCheckerState {
BeginOAuthFlow {
scopes: Vec<String>,
entrypoint: String,
service: Vec<String>,
},
BeginPairingFlow {
pairing_url: String,
scopes: Vec<String>,
entrypoint: String,
service: Vec<String>,
},
CompleteOAuthFlow {
code: String,
Expand Down Expand Up @@ -217,17 +219,25 @@ impl From<InternalState> 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 }
Expand All @@ -247,17 +257,25 @@ impl From<FxaStateCheckerState> 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 }
Expand Down
Loading