fxa_client/
auth.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//! # Signing in and out
6//!
7//! These are methods for managing the signed-in state, such as authenticating via
8//! an OAuth flow or disconnecting from the user's account.
9//!
10//! The Firefox Accounts system supports two methods for connecting an application
11//! to a user's account:
12//!
13//!    - A traditional OAuth flow, where the user is directed to a webpage to enter
14//!      their account credentials and then redirected back to the application.
15//!      This is exposed by the [`begin_oauth_flow`](FirefoxAccount::begin_oauth_flow)
16//!      method.
17//!
18//!    - A device pairing flow, where the user scans a QRCode presented by another
19//!      app that is already connected to the account, which then directs them to
20//!      a webpage for a simplified signing flow. This is exposed by the
21//!      [`begin_pairing_flow`](FirefoxAccount::begin_pairing_flow) method.
22//!
23//! Technical details of the pairing flow can be found in the [Firefox Accounts
24//! documentation hub](https://mozilla.github.io/ecosystem-platform/docs/features/firefox-accounts/pairing).
25
26use crate::{ApiResult, DeviceConfig, Error, FirefoxAccount};
27use error_support::handle_error;
28
29impl FirefoxAccount {
30    /// Get the current state
31    pub fn get_state(&self) -> FxaState {
32        self.internal.lock().get_state()
33    }
34
35    /// Process an event (login, logout, etc).
36    ///
37    /// On success, returns the new state.
38    /// On error, the state will remain the same.
39    #[handle_error(Error)]
40    pub fn process_event(&self, event: FxaEvent) -> ApiResult<FxaState> {
41        self.internal.lock().process_event(event)
42    }
43
44    /// Get the high-level authentication state of the client
45    ///
46    /// TODO: remove this and the FxaRustAuthState type from the public API
47    /// https://bugzilla.mozilla.org/show_bug.cgi?id=1868614
48    pub fn get_auth_state(&self) -> FxaRustAuthState {
49        self.internal.lock().get_auth_state()
50    }
51
52    /// Stores the session token from a WebChannel login JSON payload without exposing it
53    /// to the browser layer.
54    ///
55    /// The `json_payload` is the `data` object from the `fxaccounts:login` WebChannel
56    /// command. The session token is extracted and stored internally; callers never hold
57    /// the raw token value.
58    ///
59    /// **💾 This method alters the persisted account state.**
60    #[handle_error(Error)]
61    pub fn handle_web_channel_login(&self, json_payload: String) -> ApiResult<()> {
62        self.internal.lock().handle_web_channel_login(&json_payload)
63    }
64
65    /// Initiate a web-based OAuth sign-in flow.
66    ///
67    /// This method initializes some internal state and then returns a URL at which the
68    /// user may perform a web-based authorization flow to connect the application to
69    /// their account. The application should direct the user to the provided URL.
70    ///
71    /// When the resulting OAuth flow redirects back to the configured `redirect_uri`,
72    /// the query parameters should be extracting from the URL and passed to the
73    /// [`complete_oauth_flow`](FirefoxAccount::complete_oauth_flow) method to finalize
74    /// the signin.
75    ///
76    /// # Arguments
77    ///
78    ///   - `scopes` - list of OAuth scopes to request.
79    ///       - The requested scopes will determine what account-related data
80    ///         the application is able to access.
81    ///   - `entrypoint` - metrics identifier for UX entrypoint.
82    ///       - This parameter is used for metrics purposes, to identify the
83    ///         UX entrypoint from which the user triggered the signin request.
84    ///         For example, the application toolbar, on the onboarding flow.
85    ///   - `metrics` - optionally, additional metrics tracking parameters.
86    ///       - These will be included as query parameters in the resulting URL.
87    #[handle_error(Error)]
88    pub fn begin_oauth_flow<T: AsRef<str>>(
89        &self,
90        // Allow both &[String] and &[&str] since UniFFI can't represent `&[&str]` yet,
91        scopes: &[T],
92        entrypoint: &str,
93        service: &str,
94    ) -> ApiResult<String> {
95        let scopes = scopes.iter().map(T::as_ref).collect::<Vec<_>>();
96        self.internal
97            .lock()
98            .begin_oauth_flow(service, &scopes, entrypoint)
99    }
100
101    /// Get the URL at which to begin a device-pairing signin flow.
102    ///
103    /// If the user wants to sign in using device pairing, call this method and then
104    /// direct them to visit the resulting URL on an already-signed-in device. Doing
105    /// so will trigger the other device to show a QR code to be scanned, and the result
106    /// from said QR code can be passed to [`begin_pairing_flow`](FirefoxAccount::begin_pairing_flow).
107    #[handle_error(Error)]
108    pub fn get_pairing_authority_url(&self) -> ApiResult<String> {
109        self.internal.lock().get_pairing_authority_url()
110    }
111
112    /// Initiate a device-pairing sign-in flow.
113    ///
114    /// Once the user has scanned a pairing QR code, pass the scanned value to this
115    /// method. It will return a URL to which the application should redirect the user
116    /// in order to continue the sign-in flow.
117    ///
118    /// When the resulting flow redirects back to the configured `redirect_uri`,
119    /// the resulting OAuth parameters should be extracting from the URL and passed
120    /// to [`complete_oauth_flow`](FirefoxAccount::complete_oauth_flow) to finalize
121    /// the signin.
122    ///
123    /// # Arguments
124    ///
125    ///   - `pairing_url` - the URL scanned from a QR code on another device.
126    ///   - `scopes` - list of OAuth scopes to request.
127    ///       - The requested scopes will determine what account-related data
128    ///         the application is able to access.
129    ///   - `entrypoint` - metrics identifier for UX entrypoint.
130    ///       - This parameter is used for metrics purposes, to identify the
131    ///         UX entrypoint from which the user triggered the signin request.
132    ///         For example, the application toolbar, on the onboarding flow.
133    ///   - `metrics` - optionally, additional metrics tracking parameters.
134    ///       - These will be included as query parameters in the resulting URL.
135    #[handle_error(Error)]
136    pub fn begin_pairing_flow(
137        &self,
138        pairing_url: &str,
139        scopes: &[String],
140        entrypoint: &str,
141        service: &str,
142    ) -> ApiResult<String> {
143        // UniFFI can't represent `&[&str]` yet, so convert it internally here.
144        let scopes = scopes.iter().map(String::as_str).collect::<Vec<_>>();
145        self.internal
146            .lock()
147            .begin_pairing_flow(pairing_url, service, &scopes, entrypoint)
148    }
149
150    /// Complete an OAuth flow.
151    ///
152    /// **💾 This method alters the persisted account state.**
153    ///
154    /// At the conclusion of an OAuth flow, the user will be redirect to the
155    /// application's registered `redirect_uri`. It should extract the `code`
156    /// and `state` parameters from the resulting URL and pass them to this
157    /// method in order to complete the sign-in.
158    ///
159    /// # Arguments
160    ///
161    ///   - `code` - the OAuth authorization code obtained from the redirect URI.
162    ///   - `state` - the OAuth state parameter obtained from the redirect URI.
163    #[handle_error(Error)]
164    pub fn complete_oauth_flow(&self, code: &str, state: &str) -> ApiResult<()> {
165        self.internal.lock().complete_oauth_flow(code, state)
166    }
167
168    /// Check authorization status for this application.
169    ///
170    /// **💾 This method alters the persisted account state.**
171    ///
172    /// Applications may call this method to check with the FxA server about the status
173    /// of their authentication tokens. It returns an [`AuthorizationInfo`] struct
174    /// with details about whether the tokens are still active.
175    #[handle_error(Error)]
176    pub fn check_authorization_status(&self) -> ApiResult<AuthorizationInfo> {
177        Ok(self.internal.lock().check_authorization_status()?.into())
178    }
179
180    /// Disconnect from the user's account.
181    ///
182    /// **💾 This method alters the persisted account state.**
183    ///
184    /// This method destroys any tokens held by the client, effectively disconnecting
185    /// from the user's account. Applications should call this when the user opts to
186    /// sign out.
187    ///
188    /// The persisted account state after calling this method will contain only the
189    /// user's last-seen profile information, if any. This may be useful in helping
190    /// the user to reconnect to their account. If reconnecting to the same account
191    /// is not desired then the application should discard the persisted account state.
192    pub fn disconnect(&self) {
193        self.internal.lock().disconnect()
194    }
195
196    /// Update the state based on authentication issues.
197    ///
198    /// **💾 This method alters the persisted account state.**
199    ///
200    /// Call this if you know there's an authentication / authorization issue that requires the
201    /// user to re-authenticated.  It transitions the user to the [FxaRustAuthState.AuthIssues] state.
202    pub fn on_auth_issues(&self) {
203        self.internal.lock().on_auth_issues()
204    }
205
206    /// Used by the application to test auth token issues
207    pub fn simulate_temporary_auth_token_issue(&self) {
208        self.internal.lock().simulate_temporary_auth_token_issue()
209    }
210
211    /// Used by the application to test auth token issues
212    pub fn simulate_permanent_auth_token_issue(&self) {
213        self.internal.lock().simulate_permanent_auth_token_issue()
214    }
215}
216
217/// Information about the authorization state of the application.
218///
219/// This struct represents metadata about whether the application is currently
220/// connected to the user's account.
221pub struct AuthorizationInfo {
222    pub active: bool,
223}
224
225/// High-level view of the authorization state
226///
227/// This is named `FxaRustAuthState` because it doesn't track all the states we want yet and needs
228/// help from the wrapper code.  The wrapper code defines the actual `FxaAuthState` type based on
229/// this, adding the extra data.
230///
231/// In the long-term, we should track that data in Rust, remove the wrapper, and rename this to
232/// `FxaAuthState`.
233#[derive(Clone, Copy, Debug, PartialEq, Eq)]
234pub enum FxaRustAuthState {
235    Disconnected,
236    Connected,
237    AuthIssues,
238}
239
240/// Fxa state
241///
242/// These are the states of [crate::FxaStateMachine] that consumers observe.
243#[derive(Clone, Debug, PartialEq, Eq)]
244pub enum FxaState {
245    /// The state machine needs to be initialized via [Event::Initialize].
246    Uninitialized,
247    /// User has not connected to FxA or has logged out
248    Disconnected,
249    /// User is currently performing an OAuth flow - our existing initial state
250    /// when we transition to this state will influence what this means exactly.
251    Authenticating {
252        oauth_url: String,
253        initial_state: FxaRustAuthState,
254    },
255    /// User is currently connected to FxA
256    Connected,
257    /// User was connected to FxA, but we observed issues with the auth tokens.
258    /// The user needs to reauthenticate before the account can be used.
259    AuthIssues,
260}
261
262impl From<FxaRustAuthState> for FxaState {
263    fn from(value: FxaRustAuthState) -> Self {
264        match value {
265            FxaRustAuthState::Connected => FxaState::Connected,
266            FxaRustAuthState::Disconnected => FxaState::Disconnected,
267            FxaRustAuthState::AuthIssues => FxaState::AuthIssues,
268        }
269    }
270}
271
272/// Fxa event
273///
274/// These are the events that consumers send to [crate::FxaStateMachine::process_event]
275#[derive(Clone, Debug, PartialEq, Eq)]
276pub enum FxaEvent {
277    /// Initialize the state machine.  This must be the first event sent.
278    Initialize { device_config: DeviceConfig },
279    /// Begin an oauth flow
280    ///
281    /// If successful, the state machine will transition the [FxaState::Authenticating].  The next
282    /// step is to navigate the user to the `oauth_url` and let them sign and authorize the client.
283    ///
284    /// This event is valid for the `Disconnected`, `AuthIssues`, and `Authenticating` states.  If
285    /// the state machine is in the `Authenticating` state, then this will forget the current OAuth
286    /// flow and start a new one.
287    BeginOAuthFlow {
288        service: String,
289        scopes: Vec<String>,
290        entrypoint: String,
291    },
292    /// Begin an oauth flow using a URL from a pairing code
293    ///
294    /// If successful, the state machine will transition the [FxaState::Authenticating].  The next
295    /// step is to navigate the user to the `oauth_url` and let them sign and authorize the client.
296    ///
297    /// This event is valid for the `Disconnected`, `AuthIssues`, and `Authenticating` states.  If
298    /// the state machine is in the `Authenticating` state, then this will forget the current OAuth
299    /// flow and start a new one.
300    BeginPairingFlow {
301        pairing_url: String,
302        service: String,
303        scopes: Vec<String>,
304        entrypoint: String,
305    },
306    /// Complete an OAuth flow.
307    ///
308    /// Send this event after the user has navigated through the OAuth flow and has reached the
309    /// redirect URI.  Extract `code` and `state` from the query parameters or web channel.  If
310    /// successful the state machine will transition to [FxaState::Connected].
311    ///
312    /// This event is valid for the `Authenticating` state.
313    CompleteOAuthFlow { code: String, state: String },
314    /// Cancel an OAuth flow.
315    ///
316    /// Use this to cancel an in-progress OAuth, returning to [FxaState::Disconnected] so the
317    /// process can begin again.
318    ///
319    /// This event is valid for the `Authenticating` state.
320    CancelOAuthFlow,
321    /// Check the authorization status for a connected account.
322    ///
323    /// Send this when issues are detected with the auth tokens for a connected account.  It will
324    /// double check for authentication issues with the account.  If it detects them, the state
325    /// machine will transition to [FxaState::AuthIssues].  From there you can start an OAuth flow
326    /// again to re-connect the user.
327    ///
328    /// This event is valid for the `Connected` state.
329    CheckAuthorizationStatus,
330    /// Disconnect the user
331    ///
332    /// Send this when the user is asking to be logged out.  The state machine will transition to
333    /// [FxaState::Disconnected].
334    ///
335    /// This event is valid for the `Connected` state.
336    Disconnect,
337    /// Force a call to [FirefoxAccount::get_profile]
338    ///
339    /// This is used for testing the auth/network retry code, since it hits the network and
340    /// requires and auth token.
341    ///
342    /// This event is valid for the `Connected` state.
343    CallGetProfile,
344}