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 using [`begin_oauth_flow`](FirefoxAccount::begin_oauth_flow)
26//!   or [`begin_pairing_flow`](FirefoxAccount::begin_pairing_flow); when they return
27//!   to your registered `redirect_uri`, pass the resulting authorization state back to
28//!   [`complete_oauth_flow`](FirefoxAccount::complete_oauth_flow) 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, UserData};
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// Used for auth state checking.  Remove this once firefox-android and firefox-ios are migrated to
73// using FxaAuthStateMachine
74pub use state_machine::checker::{
75    FxaStateCheckerEvent, FxaStateCheckerState, FxaStateMachineChecker,
76};
77
78/// Result returned by internal functions
79pub type Result<T> = std::result::Result<T, Error>;
80/// Result returned by public-facing API functions
81pub type ApiResult<T> = std::result::Result<T, FxaError>;
82
83/// Object representing the signed-in state of an application.
84///
85/// The `FirefoxAccount` object is the main interface provided by this crate.
86/// It represents the signed-in state of an application that may be connected to
87/// user's Firefox Account, and provides methods for inspecting the state of the
88/// account and accessing other services on behalf of the user.
89///
90pub struct FirefoxAccount {
91    // For now, we serialize all access on a single `Mutex` for thread safety across
92    // the FFI. We should make the locking more granular in future.
93    internal: Mutex<internal::FirefoxAccount>,
94}
95
96impl FirefoxAccount {
97    /// Create a new [`FirefoxAccount`] instance, not connected to any account.
98    ///
99    /// **💾 This method alters the persisted account state.**
100    ///
101    /// This method constructs as new [`FirefoxAccount`] instance configured to connect
102    /// the application to a user's account.
103    pub fn new(config: FxaConfig) -> FirefoxAccount {
104        FirefoxAccount {
105            internal: Mutex::new(internal::FirefoxAccount::new(config)),
106        }
107    }
108
109    /// Used by the application to test auth token issues
110    pub fn simulate_network_error(&self) {
111        self.internal.lock().simulate_network_error()
112    }
113}
114
115#[derive(Clone, Debug)]
116pub struct FxaConfig {
117    /// FxaServer to connect with
118    pub server: FxaServer,
119    /// registered OAuth client id of the application.
120    pub client_id: String,
121    /// `redirect_uri` - the registered OAuth redirect URI of the application.
122    pub redirect_uri: String,
123    ///  URL for the user's Sync Tokenserver. This can be used to support users who self-host their
124    ///  sync data. If `None` then it will default to the Mozilla-hosted Sync server.
125    ///
126    ///  Note: this lives here for historical reasons, but probably shouldn't.  Applications pass
127    ///  the token server URL they get from `fxa-client` to `SyncManager`.  It would be simpler to
128    ///  cut out `fxa-client` out of the middle and have applications send the overridden URL
129    ///  directly to `SyncManager`.
130    pub token_server_url_override: Option<String>,
131}
132
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub enum FxaServer {
135    Release,
136    Stable,
137    Stage,
138    China,
139    LocalDev,
140    Custom { url: String },
141}
142
143impl FxaServer {
144    fn content_url(&self) -> &str {
145        match self {
146            Self::Release => "https://accounts.firefox.com",
147            Self::Stable => "https://stable.dev.lcip.org",
148            Self::Stage => "https://accounts.stage.mozaws.net",
149            Self::China => "https://accounts.firefox.com.cn",
150            Self::LocalDev => "http://127.0.0.1:3030",
151            Self::Custom { url } => url,
152        }
153    }
154}
155
156impl From<&Url> for FxaServer {
157    fn from(url: &Url) -> Self {
158        let origin = url.origin();
159        // Note: we can call unwrap() below because parsing content_url for known servers should
160        // never fail and `test_from_url` tests this.
161        if origin == Url::parse(Self::Release.content_url()).unwrap().origin() {
162            Self::Release
163        } else if origin == Url::parse(Self::Stable.content_url()).unwrap().origin() {
164            Self::Stable
165        } else if origin == Url::parse(Self::Stage.content_url()).unwrap().origin() {
166            Self::Stage
167        } else if origin == Url::parse(Self::China.content_url()).unwrap().origin() {
168            Self::China
169        } else if origin == Url::parse(Self::LocalDev.content_url()).unwrap().origin() {
170            Self::LocalDev
171        } else {
172            Self::Custom {
173                url: origin.ascii_serialization(),
174            }
175        }
176    }
177}
178
179/// Display impl
180///
181/// This identifies the variant, without recording the URL for custom servers.  It's good for
182/// Sentry reports when we don't want to give away any PII.
183impl fmt::Display for FxaServer {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        let variant_name = match self {
186            Self::Release => "Release",
187            Self::Stable => "Stable",
188            Self::Stage => "Stage",
189            Self::China => "China",
190            Self::LocalDev => "LocalDev",
191            Self::Custom { .. } => "Custom",
192        };
193        write!(f, "{variant_name}")
194    }
195}
196
197impl FxaConfig {
198    pub fn release(client_id: impl ToString, redirect_uri: impl ToString) -> Self {
199        Self {
200            server: FxaServer::Release,
201            client_id: client_id.to_string(),
202            redirect_uri: redirect_uri.to_string(),
203            token_server_url_override: None,
204        }
205    }
206
207    pub fn stable(client_id: impl ToString, redirect_uri: impl ToString) -> Self {
208        Self {
209            server: FxaServer::Stable,
210            client_id: client_id.to_string(),
211            redirect_uri: redirect_uri.to_string(),
212            token_server_url_override: None,
213        }
214    }
215
216    pub fn stage(client_id: impl ToString, redirect_uri: impl ToString) -> Self {
217        Self {
218            server: FxaServer::Stage,
219            client_id: client_id.to_string(),
220            redirect_uri: redirect_uri.to_string(),
221            token_server_url_override: None,
222        }
223    }
224
225    pub fn china(client_id: impl ToString, redirect_uri: impl ToString) -> Self {
226        Self {
227            server: FxaServer::China,
228            client_id: client_id.to_string(),
229            redirect_uri: redirect_uri.to_string(),
230            token_server_url_override: None,
231        }
232    }
233
234    pub fn dev(client_id: impl ToString, redirect_uri: impl ToString) -> Self {
235        Self {
236            server: FxaServer::LocalDev,
237            client_id: client_id.to_string(),
238            redirect_uri: redirect_uri.to_string(),
239            token_server_url_override: None,
240        }
241    }
242}
243
244uniffi::include_scaffolding!("fxa_client");
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn test_from_url() {
252        let test_cases = [
253            ("https://accounts.firefox.com", FxaServer::Release),
254            ("https://stable.dev.lcip.org", FxaServer::Stable),
255            ("https://accounts.stage.mozaws.net", FxaServer::Stage),
256            ("https://accounts.firefox.com.cn", FxaServer::China),
257            ("http://127.0.0.1:3030", FxaServer::LocalDev),
258            (
259                "http://my-fxa-server.com",
260                FxaServer::Custom {
261                    url: "http://my-fxa-server.com".to_owned(),
262                },
263            ),
264        ];
265        for (content_url, expected_result) in test_cases {
266            let url = Url::parse(content_url).unwrap();
267            assert_eq!(FxaServer::from(&url), expected_result);
268        }
269    }
270}