1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

//! # Firefox Accounts Client
//!
//! The fxa-client component lets applications integrate with the
//! [Firefox Accounts](https://mozilla.github.io/ecosystem-platform/docs/features/firefox-accounts/fxa-overview)
//! identity service. The shape of a typical integration would look
//! something like:
//!
//! * Out-of-band, register your application with the Firefox Accounts service,
//!   providing an OAuth `redirect_uri` controlled by your application and
//!   obtaining an OAuth `client_id`.
//!
//! * On application startup, create a [`FirefoxAccount`] object to represent the
//!   signed-in state of the application.
//!     * On first startup, a new [`FirefoxAccount`] can be created by calling
//!       [`FirefoxAccount::new`] and passing the application's `client_id`.
//!     * For subsequent startups the object can be persisted using the
//!       [`to_json`](FirefoxAccount::to_json) method and re-created by
//!       calling [`FirefoxAccount::from_json`].
//!
//! * When the user wants to sign in to your application, direct them through
//!   a web-based OAuth flow using [`begin_oauth_flow`](FirefoxAccount::begin_oauth_flow)
//!   or [`begin_pairing_flow`](FirefoxAccount::begin_pairing_flow); when they return
//!   to your registered `redirect_uri`, pass the resulting authorization state back to
//!   [`complete_oauth_flow`](FirefoxAccount::complete_oauth_flow) to sign them in.
//!
//! * Display information about the signed-in user by using the data from
//!   [`get_profile`](FirefoxAccount::get_profile).
//!
//! * Access account-related services on behalf of the user by obtaining OAuth
//!   access tokens via [`get_access_token`](FirefoxAccount::get_access_token).
//!
//! * If the user opts to sign out of the application, calling [`disconnect`](FirefoxAccount::disconnect)
//!   and then discarding any persisted account data.

mod account;
mod auth;
mod device;
mod error;
mod internal;
mod profile;
mod push;
mod state_machine;
mod storage;
mod telemetry;
mod token;

use std::fmt;

pub use sync15::DeviceType;
use url::Url;

pub use auth::{AuthorizationInfo, FxaEvent, FxaRustAuthState, FxaState, UserData};
pub use device::{AttachedClient, Device, DeviceCapability, DeviceConfig, LocalDevice};
pub use error::{Error, FxaError};
use parking_lot::Mutex;
pub use profile::Profile;
pub use push::{
    AccountEvent, DevicePushSubscription, IncomingDeviceCommand, SendTabPayload, TabHistoryEntry,
};
pub use token::{AccessTokenInfo, AuthorizationParameters, ScopedKey};

// Used for auth state checking.  Remove this once firefox-android and firefox-ios are migrated to
// using FxaAuthStateMachine
pub use state_machine::checker::{
    FxaStateCheckerEvent, FxaStateCheckerState, FxaStateMachineChecker,
};

/// Result returned by internal functions
pub type Result<T> = std::result::Result<T, Error>;
/// Result returned by public-facing API functions
pub type ApiResult<T> = std::result::Result<T, FxaError>;

/// Object representing the signed-in state of an application.
///
/// The `FirefoxAccount` object is the main interface provided by this crate.
/// It represents the signed-in state of an application that may be connected to
/// user's Firefox Account, and provides methods for inspecting the state of the
/// account and accessing other services on behalf of the user.
///
pub struct FirefoxAccount {
    // For now, we serialize all access on a single `Mutex` for thread safety across
    // the FFI. We should make the locking more granular in future.
    internal: Mutex<internal::FirefoxAccount>,
}

impl FirefoxAccount {
    /// Create a new [`FirefoxAccount`] instance, not connected to any account.
    ///
    /// **💾 This method alters the persisted account state.**
    ///
    /// This method constructs as new [`FirefoxAccount`] instance configured to connect
    /// the application to a user's account.
    pub fn new(config: FxaConfig) -> FirefoxAccount {
        FirefoxAccount {
            internal: Mutex::new(internal::FirefoxAccount::new(config)),
        }
    }

    /// Used by the application to test auth token issues
    pub fn simulate_network_error(&self) {
        self.internal.lock().simulate_network_error()
    }
}

#[derive(Clone, Debug)]
pub struct FxaConfig {
    /// FxaServer to connect with
    pub server: FxaServer,
    /// registered OAuth client id of the application.
    pub client_id: String,
    /// `redirect_uri` - the registered OAuth redirect URI of the application.
    pub redirect_uri: String,
    ///  URL for the user's Sync Tokenserver. This can be used to support users who self-host their
    ///  sync data. If `None` then it will default to the Mozilla-hosted Sync server.
    ///
    ///  Note: this lives here for historical reasons, but probably shouldn't.  Applications pass
    ///  the token server URL they get from `fxa-client` to `SyncManager`.  It would be simpler to
    ///  cut out `fxa-client` out of the middle and have applications send the overridden URL
    ///  directly to `SyncManager`.
    pub token_server_url_override: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FxaServer {
    Release,
    Stable,
    Stage,
    China,
    LocalDev,
    Custom { url: String },
}

impl FxaServer {
    fn content_url(&self) -> &str {
        match self {
            Self::Release => "https://accounts.firefox.com",
            Self::Stable => "https://stable.dev.lcip.org",
            Self::Stage => "https://accounts.stage.mozaws.net",
            Self::China => "https://accounts.firefox.com.cn",
            Self::LocalDev => "http://127.0.0.1:3030",
            Self::Custom { url } => url,
        }
    }
}

impl From<&Url> for FxaServer {
    fn from(url: &Url) -> Self {
        let origin = url.origin();
        // Note: we can call unwrap() below because parsing content_url for known servers should
        // never fail and `test_from_url` tests this.
        if origin == Url::parse(Self::Release.content_url()).unwrap().origin() {
            Self::Release
        } else if origin == Url::parse(Self::Stable.content_url()).unwrap().origin() {
            Self::Stable
        } else if origin == Url::parse(Self::Stage.content_url()).unwrap().origin() {
            Self::Stage
        } else if origin == Url::parse(Self::China.content_url()).unwrap().origin() {
            Self::China
        } else if origin == Url::parse(Self::LocalDev.content_url()).unwrap().origin() {
            Self::LocalDev
        } else {
            Self::Custom {
                url: origin.ascii_serialization(),
            }
        }
    }
}

/// Display impl
///
/// This identifies the variant, without recording the URL for custom servers.  It's good for
/// Sentry reports when we don't want to give away any PII.
impl fmt::Display for FxaServer {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let variant_name = match self {
            Self::Release => "Release",
            Self::Stable => "Stable",
            Self::Stage => "Stage",
            Self::China => "China",
            Self::LocalDev => "LocalDev",
            Self::Custom { .. } => "Custom",
        };
        write!(f, "{variant_name}")
    }
}

impl FxaConfig {
    pub fn release(client_id: impl ToString, redirect_uri: impl ToString) -> Self {
        Self {
            server: FxaServer::Release,
            client_id: client_id.to_string(),
            redirect_uri: redirect_uri.to_string(),
            token_server_url_override: None,
        }
    }

    pub fn stable(client_id: impl ToString, redirect_uri: impl ToString) -> Self {
        Self {
            server: FxaServer::Stable,
            client_id: client_id.to_string(),
            redirect_uri: redirect_uri.to_string(),
            token_server_url_override: None,
        }
    }

    pub fn stage(client_id: impl ToString, redirect_uri: impl ToString) -> Self {
        Self {
            server: FxaServer::Stage,
            client_id: client_id.to_string(),
            redirect_uri: redirect_uri.to_string(),
            token_server_url_override: None,
        }
    }

    pub fn china(client_id: impl ToString, redirect_uri: impl ToString) -> Self {
        Self {
            server: FxaServer::China,
            client_id: client_id.to_string(),
            redirect_uri: redirect_uri.to_string(),
            token_server_url_override: None,
        }
    }

    pub fn dev(client_id: impl ToString, redirect_uri: impl ToString) -> Self {
        Self {
            server: FxaServer::LocalDev,
            client_id: client_id.to_string(),
            redirect_uri: redirect_uri.to_string(),
            token_server_url_override: None,
        }
    }
}

uniffi::include_scaffolding!("fxa_client");

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_from_url() {
        let test_cases = [
            ("https://accounts.firefox.com", FxaServer::Release),
            ("https://stable.dev.lcip.org", FxaServer::Stable),
            ("https://accounts.stage.mozaws.net", FxaServer::Stage),
            ("https://accounts.firefox.com.cn", FxaServer::China),
            ("http://127.0.0.1:3030", FxaServer::LocalDev),
            (
                "http://my-fxa-server.com",
                FxaServer::Custom {
                    url: "http://my-fxa-server.com".to_owned(),
                },
            ),
        ];
        for (content_url, expected_result) in test_cases {
            let url = Url::parse(content_url).unwrap();
            assert_eq!(FxaServer::from(&url), expected_result);
        }
    }
}