diff --git a/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/FxaClient.kt b/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/FxaClient.kt index 0c9833e4f2..703c607775 100644 --- a/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/FxaClient.kt +++ b/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/FxaClient.kt @@ -135,18 +135,38 @@ class FxaClient(inner: FirefoxAccount, persistCallback: PersistCallback?) : Auto } /** - * Sets user data from the web content. - * NOTE: this is only useful for applications that are user agents - * and require the user's session token - * @param userData: The user data including session token, email and uid + * Stores anything necessary to login from a WebChannel login JSON payload. This includes the session + * token, but that is abstracted because the consuming apps should not be aware of the + * specific payload format returned, nor should they get access to the session token + * directly if possible. + * + * @param jsonPayload The `data` object from the `fxaccounts:login` WebChannel command. */ - fun setUserData( - userData: UserData, - ) { - this.inner.setUserData(userData) + fun handleWebChannelLogin(jsonPayload: String) { + this.inner.handleWebChannelLogin(jsonPayload) tryPersistState() } + /** + * Handle a WebChannel password-change notification by exchanging the new session token + * for a new refresh token via a network call. + * + * @param jsonPayload is the `data` object from the `fxaccounts:change_password` WebChannel command. + */ + fun handleWebChannelPasswordChange(jsonPayload: String) { + this.inner.handleWebChannelPasswordChange(jsonPayload) + tryPersistState() + } + + /** + * Returns a complete signedInUser JSON object for a WebChannel fxaccounts:fxa_status response. + * + * @return An opaque string which holds JSON data and can be directly supplied to the WebChannel. + */ + fun getSignedInUserForWebChannel(): String? { + return this.inner.getSignedInUserForWebChannel() + } + /** * Authenticates the current account using the code and state parameters fetched from the * redirect URL reached after completing the sign in flow triggered by [beginOAuthFlow]. @@ -296,15 +316,6 @@ class FxaClient(inner: FirefoxAccount, persistCallback: PersistCallback?) : Auto return this.inner.checkAuthorizationStatus() } - /** - * Tries to return a session token - * - * @throws FxaException Will send you an exception if there is no session token set - */ - fun getSessionToken(): String { - return this.inner.getSessionToken() - } - /** * Get the current device id * diff --git a/components/fxa-client/src/auth.rs b/components/fxa-client/src/auth.rs index 1487fd265f..39c894ed8c 100644 --- a/components/fxa-client/src/auth.rs +++ b/components/fxa-client/src/auth.rs @@ -49,11 +49,17 @@ impl FirefoxAccount { 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) + /// Stores the session token from a WebChannel login JSON payload without exposing it + /// to the browser layer. + /// + /// The `json_payload` is the `data` object from the `fxaccounts:login` WebChannel + /// command. The session token is extracted and stored internally; callers never hold + /// the raw token value. + /// + /// **💾 This method alters the persisted account state.** + #[handle_error(Error)] + pub fn handle_web_channel_login(&self, json_payload: String) -> ApiResult<()> { + self.internal.lock().handle_web_channel_login(&json_payload) } /// Initiate a web-based OAuth sign-in flow. @@ -316,12 +322,3 @@ pub enum FxaEvent { /// This event is valid for the `Connected` state. 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, -} diff --git a/components/fxa-client/src/fxa_client.udl b/components/fxa-client/src/fxa_client.udl index 9576570e1f..f286a7b2f3 100644 --- a/components/fxa-client/src/fxa_client.udl +++ b/components/fxa-client/src/fxa_client.udl @@ -168,12 +168,25 @@ interface FirefoxAccount { /// [Throws=FxaError] string to_json(); - - /// Sets the users information based on the web content's login information - /// This is intended to only be used by user agents (eg: Firefox) to set the users - /// session token and tie it to the refresh token that will be issued at the end of the - /// oauth flow. - void set_user_data(UserData user_data); + + /// Stores anything necessary from a WebChannel login JSON payload. This includes the session + /// token, but that is abstracted because the consuming apps should not be aware of the + /// specific payload format returned, nor should they get access to the session token + /// directly if possible. + /// The [json_payload] is the `data` object from the `fxaccounts:login` WebChannel command. + [Throws=FxaError] + void handle_web_channel_login(string json_payload); + + /// Handle a WebChannel password-change notification by exchanging the new session token + /// for a new refresh token via a network call. + /// The [json_payload] is the `data` object from the `fxaccounts:change_password` WebChannel command. + [Throws=FxaError] + void handle_web_channel_password_change(string json_payload); + + /// Returns a complete signedInUser JSON object for a WebChannel fxaccounts:fxa_status response, + /// embedding the session token privately. Email and uid come from the cached profile in internal + /// state. Returns null if no session token is set. + string? get_signed_in_user_for_web_channel(); /// Initiate a web-based OAuth sign-in flow. /// @@ -654,43 +667,6 @@ interface FirefoxAccount { [Throws=FxaError] AccessTokenInfo get_access_token([ByRef] string scope, optional boolean use_cache = true); - /// Get the session token for the user's account, if one is available. - /// - /// **💾 This method alters the persisted account state.** - /// - /// Applications that function as a web browser may need to hold on to a session token - /// on behalf of Firefox Accounts web content. This method exists so that they can retrieve - /// it an pass it back to said web content when required. - /// - /// # Notes - /// - /// - Please do not attempt to use the resulting token to directly make calls to the - /// Firefox Accounts servers! All account management functionality should be performed - /// in web content. - /// - A session token is only available to applications that have requested the - /// `https:///identity.mozilla.com/tokens/session` scope. - /// - [Throws=FxaError] - string get_session_token(); - - - /// Update the stored session token for the user's account. - /// - /// **💾 This method alters the persisted account state.** - /// - /// Applications that function as a web browser may need to hold on to a session token - /// on behalf of Firefox Accounts web content. This method exists so that said web content - /// signals that it has generated a new session token, the stored value can be updated - /// to match. - /// - /// # Arguments - /// - /// - `session_token` - the new session token value provided from web content. - /// - [Throws=FxaError] - void handle_session_token_change([ByRef] string session_token ); - - /// Create a new OAuth authorization code using the stored session token. /// /// When a signed-in application receives an incoming device pairing request, it can @@ -1114,10 +1090,3 @@ interface IncomingDeviceCommand { /// Indicates that the sender wants to close one or more tabs on this device. TabsClosed(Device? sender, CloseTabsPayload payload); }; - -dictionary UserData { - string session_token; - string uid; - string email; - boolean verified; -}; diff --git a/components/fxa-client/src/internal/oauth.rs b/components/fxa-client/src/internal/oauth.rs index 607699c321..d2dc160b09 100644 --- a/components/fxa-client/src/internal/oauth.rs +++ b/components/fxa-client/src/internal/oauth.rs @@ -11,7 +11,6 @@ use super::{ scoped_keys::ScopedKeysFlow, util, FirefoxAccount, }; -use crate::auth::UserData; use crate::{error, warn, AuthorizationParameters, Error, FxaServer, Result, ScopedKey}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use jwcrypto::{EncryptionAlgorithm, EncryptionParameters}; @@ -124,12 +123,27 @@ impl FirefoxAccount { Ok(token_info) } - /// Sets the user data (session token, email, uid) - pub fn set_user_data(&mut self, user_data: UserData) { - // for now, we only have use for the session token - // if we'd like to implement a "Signed in but not verified" state - // we would also consume the other parts of the user data - self.state.set_session_token(user_data.session_token) + /// Extracts and stores the session token from a WebChannel login JSON payload. + /// The JSON payload is the `data` object from the `fxaccounts:login` WebChannel command. + pub fn handle_web_channel_login(&mut self, json_payload: &str) -> Result<()> { + let data: serde_json::Value = serde_json::from_str(json_payload)?; + let token = data + .get("sessionToken") + .and_then(|v| v.as_str()) + .ok_or(Error::NoSessionToken)?; + self.state.set_session_token(token.to_string()); + Ok(()) + } + + /// Extracts the session token from a WebChannel password change JSON payload and exchanges it + /// for a new refresh token via a network call. + pub fn handle_web_channel_password_change(&mut self, json_payload: &str) -> Result<()> { + let data: serde_json::Value = serde_json::from_str(json_payload)?; + let token = data + .get("sessionToken") + .and_then(|v| v.as_str()) + .ok_or(Error::NoSessionToken)?; + self.handle_session_token_change(token) } /// Retrieve the current session token from state @@ -140,6 +154,26 @@ impl FirefoxAccount { } } + /// Builds a complete `signedInUser` JSON object for a WebChannel `fxaccounts:fxa_status` + /// response. Returns `None` if no session token is stored. + /// `email` and `uid` are read from the cached profile; `verified` is always true because + /// the account state machine only completes authentication for verified accounts. + pub fn get_signed_in_user_for_web_channel(&self) -> Option { + let token = self.state.session_token()?; + let profile = self.state.last_seen_profile(); + let email = profile.map(|p| p.response.email.as_str()); + let uid = profile.map(|p| p.response.uid.as_str()); + Some( + serde_json::json!({ + "sessionToken": token, + "email": email, + "uid": uid, + "verified": true, + }) + .to_string(), + ) + } + /// Check whether user is authorized using our refresh token. pub fn check_authorization_status(&mut self) -> Result { let resp = match self.state.refresh_token() { @@ -1108,17 +1142,14 @@ mod tests { } #[test] - fn test_set_user_data_sets_session_token() { + fn test_handle_web_channel_login_sets_session_token() { nss::ensure_initialized(); let config = Config::stable_dev("12345678", "https://foo.bar"); let mut fxa = FirefoxAccount::with_config(config); - let user_data = UserData { - session_token: String::from("mock_session_token"), - uid: String::from("mock_uid_unused"), - email: String::from("mock_email_usued"), - verified: true, - }; - fxa.set_user_data(user_data); + fxa.handle_web_channel_login( + r#"{"sessionToken":"mock_session_token","uid":"mock_uid","email":"mock@example.com","verified":true}"#, + ) + .unwrap(); assert_eq!(fxa.get_session_token().unwrap(), "mock_session_token"); } @@ -1136,12 +1167,6 @@ mod tests { .unwrap(); let url = Url::parse(&url).unwrap(); let state = url.query_pairs().find(|(name, _)| name == "state").unwrap(); - let user_data = UserData { - session_token: String::from("mock_session_token"), - uid: String::from("mock_uid_unused"), - email: String::from("mock_email_usued"), - verified: true, - }; let mut client = MockFxAClient::new(); client @@ -1166,8 +1191,7 @@ mod tests { .times(1) .returning(|_, _| Ok(())); fxa.set_client(Arc::new(client)); - - fxa.set_user_data(user_data); + fxa.set_session_token("mock_session_token"); fxa.complete_oauth_flow("mock_code", state.1.as_ref()) .unwrap(); diff --git a/components/fxa-client/src/lib.rs b/components/fxa-client/src/lib.rs index f0e819654b..40e2fdd9f6 100644 --- a/components/fxa-client/src/lib.rs +++ b/components/fxa-client/src/lib.rs @@ -53,7 +53,7 @@ use std::fmt; pub use sync15::DeviceType; use url::Url; -pub use auth::{AuthorizationInfo, FxaEvent, FxaRustAuthState, FxaState, UserData}; +pub use auth::{AuthorizationInfo, FxaEvent, FxaRustAuthState, FxaState}; pub use device::{ AttachedClient, CloseTabsResult, Device, DeviceCapability, DeviceConfig, LocalDevice, }; diff --git a/components/fxa-client/src/token.rs b/components/fxa-client/src/token.rs index 44208a336c..8048527ab2 100644 --- a/components/fxa-client/src/token.rs +++ b/components/fxa-client/src/token.rs @@ -53,6 +53,25 @@ impl FirefoxAccount { .try_into() } + /// Builds a complete `signedInUser` JSON object for a WebChannel `fxaccounts:fxa_status` + /// response, embedding the session token without exposing it to the browser layer. Email and + /// uid are read from the cached profile in internal state. Returns `None` if no session token + /// is available. + pub fn get_signed_in_user_for_web_channel(&self) -> Option { + self.internal.lock().get_signed_in_user_for_web_channel() + } + + /// Handle a WebChannel password-change notification by exchanging the new session token + /// for a new refresh token. + /// + /// **💾 This method alters the persisted account state.** + #[handle_error(Error)] + pub fn handle_web_channel_password_change(&self, json_payload: String) -> ApiResult<()> { + self.internal + .lock() + .handle_web_channel_password_change(&json_payload) + } + /// Get the session token for the user's account, if one is available. /// /// **💾 This method alters the persisted account state.**