fxa_client/lib.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5//! # Firefox Accounts Client
6//!
7//! The fxa-client component lets applications integrate with the
8//! [Firefox Accounts](https://mozilla.github.io/ecosystem-platform/docs/features/firefox-accounts/fxa-overview)
9//! identity service. The shape of a typical integration would look
10//! something like:
11//!
12//! * Out-of-band, register your application with the Firefox Accounts service,
13//! providing an OAuth `redirect_uri` controlled by your application and
14//! obtaining an OAuth `client_id`.
15//!
16//! * On application startup, create a [`FirefoxAccount`] object to represent the
17//! signed-in state of the application.
18//! * On first startup, a new [`FirefoxAccount`] can be created by calling
19//! [`FirefoxAccount::new`] and passing the application's `client_id`.
20//! * For subsequent startups the object can be persisted using the
21//! [`to_json`](FirefoxAccount::to_json) method and re-created by
22//! calling [`FirefoxAccount::from_json`].
23//!
24//! * When the user wants to sign in to your application, direct them through
25//! a web-based OAuth flow by sending the [`FxaEvent::BeginOAuthFlow`] or
26//! [`FxaEvent::BeginPairingFlow`] event to [`FirefoxAccount::process_event`]; when they
27//! return to your registered `redirect_uri`, pass the resulting authorization state back
28//! via the [`FxaEvent::CompleteOAuthFlow`] event to sign them in.
29//!
30//! * Display information about the signed-in user by using the data from
31//! [`get_profile`](FirefoxAccount::get_profile).
32//!
33//! * Access account-related services on behalf of the user by obtaining OAuth
34//! access tokens via [`get_access_token`](FirefoxAccount::get_access_token).
35//!
36//! * If the user opts to sign out of the application, calling [`disconnect`](FirefoxAccount::disconnect)
37//! and then discarding any persisted account data.
38
39mod account;
40mod auth;
41mod device;
42mod error;
43mod internal;
44mod profile;
45mod push;
46mod state_machine;
47mod storage;
48mod telemetry;
49mod token;
50
51use std::fmt;
52
53pub use sync15::DeviceType;
54use url::Url;
55
56pub use auth::{AuthorizationInfo, FxaEvent, FxaRustAuthState, FxaState};
57pub use device::{
58 AttachedClient, CloseTabsResult, Device, DeviceCapability, DeviceConfig, LocalDevice,
59};
60pub use error::{Error, FxaError};
61// reexport logging helpers.
62pub use error_support::{debug, error, info, trace, warn};
63
64use parking_lot::Mutex;
65pub use profile::Profile;
66pub use push::{
67 AccountEvent, CloseTabsPayload, DevicePushSubscription, IncomingDeviceCommand, SendTabPayload,
68 TabHistoryEntry,
69};
70pub use token::{AccessTokenInfo, AuthorizationParameters, ScopedKey};
71
72/// Result returned by internal functions
73pub type Result<T> = std::result::Result<T, Error>;
74/// Result returned by public-facing API functions
75pub type ApiResult<T> = std::result::Result<T, FxaError>;
76
77#[derive(uniffi::Object)]
78/// Object representing the signed-in state of an application.
79///
80/// The `FirefoxAccount` object is the main interface provided by this crate.
81/// It represents the signed-in state of an application that may be connected to
82/// user's Firefox Account, and provides methods for inspecting the state of the
83/// account and accessing other services on behalf of the user.
84pub struct FirefoxAccount {
85 // For now, we serialize all access on a single `Mutex` for thread safety across
86 // the FFI. We should make the locking more granular in future.
87 internal: Mutex<internal::FirefoxAccount>,
88}
89
90#[uniffi::export]
91impl FirefoxAccount {
92 /// Create a new [`FirefoxAccount`] instance, not connected to any account.
93 ///
94 /// **💾 This method alters the persisted account state.**
95 ///
96 /// This method constructs as new [`FirefoxAccount`] instance configured to connect
97 /// the application to a user's account.
98 #[uniffi::constructor]
99 pub fn new(config: FxaConfig) -> FirefoxAccount {
100 FirefoxAccount {
101 internal: Mutex::new(internal::FirefoxAccount::new(config)),
102 }
103 }
104
105 /// Used by the application to test auth token issues
106 pub fn simulate_network_error(&self) {
107 self.internal.lock().simulate_network_error()
108 }
109}
110
111#[derive(uniffi::Record, Clone, Debug)]
112pub struct FxaConfig {
113 /// FxaServer to connect with
114 pub server: FxaServer,
115 /// Registered OAuth client id of the application.
116 pub client_id: String,
117 /// `redirect_uri` - the registered OAuth redirect URI of the application.
118 pub redirect_uri: String,
119 /// URL for the user's Sync Tokenserver. This can be used to support users who self-host their
120 /// sync data. If `None` then it will default to the Mozilla-hosted Sync server.
121 ///
122 /// Note: this lives here for historical reasons, but probably shouldn't. Applications pass
123 /// the token server URL they get from `fxa-client` to `SyncManager`. It would be simpler to
124 /// cut out `fxa-client` out of the middle and have applications send the overridden URL
125 /// directly to `SyncManager`.
126 #[uniffi(default=None)]
127 pub token_server_url_override: Option<String>,
128}
129
130#[derive(uniffi::Enum, Clone, Debug, PartialEq, Eq)]
131pub enum FxaServer {
132 Release,
133 Stable,
134 Stage,
135 China,
136 LocalDev,
137 Custom { url: String },
138}
139
140impl FxaServer {
141 pub fn content_url(&self) -> &str {
142 match self {
143 Self::Release | Self::China => "https://accounts.firefox.com",
144 Self::Stable => "https://stable.dev.lcip.org",
145 Self::Stage => "https://accounts.stage.mozaws.net",
146 Self::LocalDev => "http://127.0.0.1:3030",
147 Self::Custom { url } => url,
148 }
149 }
150}
151
152impl From<&Url> for FxaServer {
153 fn from(url: &Url) -> Self {
154 let origin = url.origin();
155 // Note: we can call unwrap() below because parsing content_url for known servers should
156 // never fail and `test_from_url` tests this.
157 if origin == Url::parse(Self::Release.content_url()).unwrap().origin() {
158 Self::Release
159 } else if origin == Url::parse(Self::Stable.content_url()).unwrap().origin() {
160 Self::Stable
161 } else if origin == Url::parse(Self::Stage.content_url()).unwrap().origin() {
162 Self::Stage
163 } else if origin
164 == Url::parse("https://accounts.firefox.com.cn")
165 .unwrap()
166 .origin()
167 {
168 // FxaServer::China is now the same as FxaServer::Release, but we may still end up being passed the old .cn domain.
169 // This is a little odd as we will not correctly round-trip in this scenario, but it seems better than the
170 // .cn domain being treated as `Self::Custom`.
171 Self::Release
172 } else if origin == Url::parse(Self::LocalDev.content_url()).unwrap().origin() {
173 Self::LocalDev
174 } else {
175 Self::Custom {
176 url: origin.ascii_serialization(),
177 }
178 }
179 }
180}
181
182/// Display impl
183///
184/// This identifies the variant, without recording the URL for custom servers. It's good for
185/// Sentry reports when we don't want to give away any PII.
186impl fmt::Display for FxaServer {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 let variant_name = match self {
189 Self::Release => "Release",
190 Self::Stable => "Stable",
191 Self::Stage => "Stage",
192 Self::China => "China",
193 Self::LocalDev => "LocalDev",
194 Self::Custom { .. } => "Custom",
195 };
196 write!(f, "{variant_name}")
197 }
198}
199
200impl FxaConfig {
201 pub fn release(client_id: impl ToString, redirect_uri: impl ToString) -> Self {
202 Self {
203 server: FxaServer::Release,
204 client_id: client_id.to_string(),
205 redirect_uri: redirect_uri.to_string(),
206 token_server_url_override: None,
207 }
208 }
209
210 pub fn stable(client_id: impl ToString, redirect_uri: impl ToString) -> Self {
211 Self {
212 server: FxaServer::Stable,
213 client_id: client_id.to_string(),
214 redirect_uri: redirect_uri.to_string(),
215 token_server_url_override: None,
216 }
217 }
218
219 pub fn stage(client_id: impl ToString, redirect_uri: impl ToString) -> Self {
220 Self {
221 server: FxaServer::Stage,
222 client_id: client_id.to_string(),
223 redirect_uri: redirect_uri.to_string(),
224 token_server_url_override: None,
225 }
226 }
227
228 pub fn dev(client_id: impl ToString, redirect_uri: impl ToString) -> Self {
229 Self {
230 server: FxaServer::LocalDev,
231 client_id: client_id.to_string(),
232 redirect_uri: redirect_uri.to_string(),
233 token_server_url_override: None,
234 }
235 }
236}
237
238uniffi::setup_scaffolding!("fxa_client");
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_from_url() {
246 let test_cases = [
247 ("https://accounts.firefox.com", FxaServer::Release),
248 ("https://stable.dev.lcip.org", FxaServer::Stable),
249 ("https://accounts.stage.mozaws.net", FxaServer::Stage),
250 ("https://accounts.firefox.com.cn", FxaServer::Release),
251 ("http://127.0.0.1:3030", FxaServer::LocalDev),
252 (
253 "http://my-fxa-server.com",
254 FxaServer::Custom {
255 url: "http://my-fxa-server.com".to_owned(),
256 },
257 ),
258 ];
259 for (content_url, expected_result) in test_cases {
260 let url = Url::parse(content_url).unwrap();
261 assert_eq!(FxaServer::from(&url), expected_result);
262 }
263 }
264}