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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
/* 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/. */

//! # Signing in and out
//!
//! These are methods for managing the signed-in state, such as authenticating via
//! an OAuth flow or disconnecting from the user's account.
//!
//! The Firefox Accounts system supports two methods for connecting an application
//! to a user's account:
//!
//!    - A traditional OAuth flow, where the user is directed to a webpage to enter
//!      their account credentials and then redirected back to the application.
//!      This is exposed by the [`begin_oauth_flow`](FirefoxAccount::begin_oauth_flow)
//!      method.
//!
//!    - A device pairing flow, where the user scans a QRCode presented by another
//!      app that is already connected to the account, which then directs them to
//!      a webpage for a simplified signing flow. This is exposed by the
//!      [`begin_pairing_flow`](FirefoxAccount::begin_pairing_flow) method.
//!
//! Technical details of the pairing flow can be found in the [Firefox Accounts
//! documentation hub](https://mozilla.github.io/ecosystem-platform/docs/features/firefox-accounts/pairing).

use crate::{ApiResult, DeviceConfig, Error, FirefoxAccount};
use error_support::handle_error;

impl FirefoxAccount {
    /// Get the current state
    pub fn get_state(&self) -> FxaState {
        self.internal.lock().get_state()
    }

    /// Process an event (login, logout, etc).
    ///
    /// On success, returns the new state.
    /// On error, the state will remain the same.
    #[handle_error(Error)]
    pub fn process_event(&self, event: FxaEvent) -> ApiResult<FxaState> {
        self.internal.lock().process_event(event)
    }

    /// Get the high-level authentication state of the client
    ///
    /// TODO: remove this and the FxaRustAuthState type from the public API
    /// https://bugzilla.mozilla.org/show_bug.cgi?id=1868614
    pub fn get_auth_state(&self) -> FxaRustAuthState {
        self.internal.lock().get_auth_state()
    }

    /// Sets the user data for a user agent
    /// **Important**: This should only be used on user agents such as Firefox
    /// that require the user's session token
    pub fn set_user_data(&self, user_data: UserData) {
        self.internal.lock().set_user_data(user_data)
    }

    /// Initiate a web-based OAuth sign-in flow.
    ///
    /// This method initializes some internal state and then returns a URL at which the
    /// user may perform a web-based authorization flow to connect the application to
    /// their account. The application should direct the user to the provided URL.
    ///
    /// When the resulting OAuth flow redirects back to the configured `redirect_uri`,
    /// the query parameters should be extracting from the URL and passed to the
    /// [`complete_oauth_flow`](FirefoxAccount::complete_oauth_flow) method to finalize
    /// the signin.
    ///
    /// # Arguments
    ///
    ///   - `scopes` - list of OAuth scopes to request.
    ///       - The requested scopes will determine what account-related data
    ///         the application is able to access.
    ///   - `entrypoint` - metrics identifier for UX entrypoint.
    ///       - 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.
    ///   - `metrics` - optionally, additional metrics tracking parameters.
    ///       - These will be included as query parameters in the resulting URL.
    #[handle_error(Error)]
    pub fn begin_oauth_flow<T: AsRef<str>>(
        &self,
        // Allow both &[String] and &[&str] since UniFFI can't represent `&[&str]` yet,
        scopes: &[T],
        entrypoint: &str,
    ) -> ApiResult<String> {
        let scopes = scopes.iter().map(T::as_ref).collect::<Vec<_>>();
        self.internal.lock().begin_oauth_flow(&scopes, entrypoint)
    }

    /// 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
    /// direct them to visit the resulting URL on an already-signed-in device. Doing
    /// so will trigger the other device to show a QR code to be scanned, and the result
    /// from said QR code can be passed to [`begin_pairing_flow`](FirefoxAccount::begin_pairing_flow).
    #[handle_error(Error)]
    pub fn get_pairing_authority_url(&self) -> ApiResult<String> {
        self.internal.lock().get_pairing_authority_url()
    }

    /// Initiate a device-pairing sign-in flow.
    ///
    /// Once the user has scanned a pairing QR code, pass the scanned value to this
    /// method. It will return a URL to which the application should redirect the user
    /// in order to continue the sign-in flow.
    ///
    /// When the resulting flow redirects back to the configured `redirect_uri`,
    /// the resulting OAuth parameters should be extracting from the URL and passed
    /// to [`complete_oauth_flow`](FirefoxAccount::complete_oauth_flow) to finalize
    /// the signin.
    ///
    /// # Arguments
    ///
    ///   - `pairing_url` - the URL scanned from a QR code on another device.
    ///   - `scopes` - list of OAuth scopes to request.
    ///       - The requested scopes will determine what account-related data
    ///         the application is able to access.
    ///   - `entrypoint` - metrics identifier for UX entrypoint.
    ///       - 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.
    ///   - `metrics` - optionally, additional metrics tracking parameters.
    ///       - These will be included as query parameters in the resulting URL.
    #[handle_error(Error)]
    pub fn begin_pairing_flow(
        &self,
        pairing_url: &str,
        scopes: &[String],
        entrypoint: &str,
    ) -> ApiResult<String> {
        // UniFFI can't represent `&[&str]` yet, so convert it internally here.
        let scopes = scopes.iter().map(String::as_str).collect::<Vec<_>>();
        self.internal
            .lock()
            .begin_pairing_flow(pairing_url, &scopes, entrypoint)
    }

    /// Complete an OAuth flow.
    ///
    /// **💾 This method alters the persisted account state.**
    ///
    /// At the conclusion of an OAuth flow, the user will be redirect to the
    /// application's registered `redirect_uri`. It should extract the `code`
    /// and `state` parameters from the resulting URL and pass them to this
    /// method in order to complete the sign-in.
    ///
    /// # Arguments
    ///
    ///   - `code` - the OAuth authorization code obtained from the redirect URI.
    ///   - `state` - the OAuth state parameter obtained from the redirect URI.
    #[handle_error(Error)]
    pub fn complete_oauth_flow(&self, code: &str, state: &str) -> ApiResult<()> {
        self.internal.lock().complete_oauth_flow(code, state)
    }

    /// Check authorization status for this application.
    ///
    /// **💾 This method alters the persisted account state.**
    ///
    /// Applications may call this method to check with the FxA server about the status
    /// of their authentication tokens. It returns an [`AuthorizationInfo`] struct
    /// with details about whether the tokens are still active.
    #[handle_error(Error)]
    pub fn check_authorization_status(&self) -> ApiResult<AuthorizationInfo> {
        Ok(self.internal.lock().check_authorization_status()?.into())
    }

    /// Disconnect from the user's account.
    ///
    /// **💾 This method alters the persisted account state.**
    ///
    /// This method destroys any tokens held by the client, effectively disconnecting
    /// from the user's account. Applications should call this when the user opts to
    /// sign out.
    ///
    /// The persisted account state after calling this method will contain only the
    /// user's last-seen profile information, if any. This may be useful in helping
    /// the user to reconnect to their account. If reconnecting to the same account
    /// is not desired then the application should discard the persisted account state.
    pub fn disconnect(&self) {
        self.internal.lock().disconnect()
    }

    /// Update the state based on authentication issues.
    ///
    /// **💾 This method alters the persisted account state.**
    ///
    /// Call this if you know there's an authentication / authorization issue that requires the
    /// user to re-authenticated.  It transitions the user to the [FxaRustAuthState.AuthIssues] state.
    pub fn on_auth_issues(&self) {
        self.internal.lock().on_auth_issues()
    }

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

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

/// Information about the authorization state of the application.
///
/// This struct represents metadata about whether the application is currently
/// connected to the user's account.
pub struct AuthorizationInfo {
    pub active: bool,
}

/// High-level view of the authorization state
///
/// This is named `FxaRustAuthState` because it doesn't track all the states we want yet and needs
/// help from the wrapper code.  The wrapper code defines the actual `FxaAuthState` type based on
/// this, adding the extra data.
///
/// In the long-term, we should track that data in Rust, remove the wrapper, and rename this to
/// `FxaAuthState`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FxaRustAuthState {
    Disconnected,
    Connected,
    AuthIssues,
}

/// Fxa state
///
/// These are the states of [crate::FxaStateMachine] that consumers observe.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FxaState {
    /// The state machine needs to be initialized via [Event::Initialize].
    Uninitialized,
    /// User has not connected to FxA or has logged out
    Disconnected,
    /// User is currently performing an OAuth flow
    Authenticating { oauth_url: String },
    /// User is currently connected to FxA
    Connected,
    /// User was connected to FxA, but we observed issues with the auth tokens.
    /// The user needs to reauthenticate before the account can be used.
    AuthIssues,
}

/// Fxa event
///
/// These are the events that consumers send to [crate::FxaStateMachine::process_event]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FxaEvent {
    /// Initialize the state machine.  This must be the first event sent.
    Initialize { device_config: DeviceConfig },
    /// Begin an oauth flow
    ///
    /// If successful, the state machine will transition the [FxaState::Authenticating].  The next
    /// step is to navigate the user to the `oauth_url` and let them sign and authorize the client.
    BeginOAuthFlow {
        scopes: Vec<String>,
        entrypoint: String,
    },
    /// Begin an oauth flow using a URL from a pairing code
    ///
    /// If successful, the state machine will transition the [FxaState::Authenticating].  The next
    /// step is to navigate the user to the `oauth_url` and let them sign and authorize the client.
    BeginPairingFlow {
        pairing_url: String,
        scopes: Vec<String>,
        entrypoint: String,
    },
    /// Complete an OAuth flow.
    ///
    /// Send this event after the user has navigated through the OAuth flow and has reached the
    /// redirect URI.  Extract `code` and `state` from the query parameters or web channel.  If
    /// successful the state machine will transition to [FxaState::Connected].
    CompleteOAuthFlow { code: String, state: String },
    /// Cancel an OAuth flow.
    ///
    /// Use this to cancel an in-progress OAuth, returning to [FxaState::Disconnected] so the
    /// process can begin again.
    CancelOAuthFlow,
    /// Check the authorization status for a connected account.
    ///
    /// Send this when issues are detected with the auth tokens for a connected account.  It will
    /// double check for authentication issues with the account.  If it detects them, the state
    /// machine will transition to [FxaState::AuthIssues].  From there you can start an OAuth flow
    /// again to re-connect the user.
    CheckAuthorizationStatus,
    /// Disconnect the user
    ///
    /// Send this when the user is asking to be logged out.  The state machine will transition to
    /// [FxaState::Disconnected].
    Disconnect,
    /// Force a call to [FirefoxAccount::get_profile]
    ///
    /// This is used for testing the auth/network retry code, since it hits the network and
    /// requires and auth token.
    CallGetProfile,
}

/// User data provided by the web content, meant to be consumed by user agents
#[derive(Debug, Clone)]
pub struct UserData {
    pub(crate) session_token: String,
    pub(crate) uid: String,
    pub(crate) email: String,
    pub(crate) verified: bool,
}