Skip to content

Commit 3ab0bfc

Browse files
authored
fxa-client: Don't expose the session token directly to consumers. (#7317)
This was exposed primarily for use with the web channel, but the fact it is exposed means that consumers are free to use it in ways we'd like to better control. We do this by adding a couple of new methods which are explicitly used only for webchannels, and the data moved over those APIs are abstracted away - the consumers just pass the raw JSON data from the webchannel message and the component knows what format it is in and decodes it appropriately. This is a breaking change for Android and iOS: TODO: link to PRs for them.
1 parent c03496e commit 3ab0bfc

6 files changed

Lines changed: 125 additions & 105 deletions

File tree

components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/FxaClient.kt

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -135,18 +135,38 @@ class FxaClient(inner: FirefoxAccount, persistCallback: PersistCallback?) : Auto
135135
}
136136

137137
/**
138-
* Sets user data from the web content.
139-
* NOTE: this is only useful for applications that are user agents
140-
* and require the user's session token
141-
* @param userData: The user data including session token, email and uid
138+
* Stores anything necessary to login from a WebChannel login JSON payload. This includes the session
139+
* token, but that is abstracted because the consuming apps should not be aware of the
140+
* specific payload format returned, nor should they get access to the session token
141+
* directly if possible.
142+
*
143+
* @param jsonPayload The `data` object from the `fxaccounts:login` WebChannel command.
142144
*/
143-
fun setUserData(
144-
userData: UserData,
145-
) {
146-
this.inner.setUserData(userData)
145+
fun handleWebChannelLogin(jsonPayload: String) {
146+
this.inner.handleWebChannelLogin(jsonPayload)
147147
tryPersistState()
148148
}
149149

150+
/**
151+
* Handle a WebChannel password-change notification by exchanging the new session token
152+
* for a new refresh token via a network call.
153+
*
154+
* @param jsonPayload is the `data` object from the `fxaccounts:change_password` WebChannel command.
155+
*/
156+
fun handleWebChannelPasswordChange(jsonPayload: String) {
157+
this.inner.handleWebChannelPasswordChange(jsonPayload)
158+
tryPersistState()
159+
}
160+
161+
/**
162+
* Returns a complete signedInUser JSON object for a WebChannel fxaccounts:fxa_status response.
163+
*
164+
* @return An opaque string which holds JSON data and can be directly supplied to the WebChannel.
165+
*/
166+
fun getSignedInUserForWebChannel(): String? {
167+
return this.inner.getSignedInUserForWebChannel()
168+
}
169+
150170
/**
151171
* Authenticates the current account using the code and state parameters fetched from the
152172
* redirect URL reached after completing the sign in flow triggered by [beginOAuthFlow].
@@ -296,15 +316,6 @@ class FxaClient(inner: FirefoxAccount, persistCallback: PersistCallback?) : Auto
296316
return this.inner.checkAuthorizationStatus()
297317
}
298318

299-
/**
300-
* Tries to return a session token
301-
*
302-
* @throws FxaException Will send you an exception if there is no session token set
303-
*/
304-
fun getSessionToken(): String {
305-
return this.inner.getSessionToken()
306-
}
307-
308319
/**
309320
* Get the current device id
310321
*

components/fxa-client/src/auth.rs

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,17 @@ impl FirefoxAccount {
4949
self.internal.lock().get_auth_state()
5050
}
5151

52-
/// Sets the user data for a user agent
53-
/// **Important**: This should only be used on user agents such as Firefox
54-
/// that require the user's session token
55-
pub fn set_user_data(&self, user_data: UserData) {
56-
self.internal.lock().set_user_data(user_data)
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)
5763
}
5864

5965
/// Initiate a web-based OAuth sign-in flow.
@@ -336,12 +342,3 @@ pub enum FxaEvent {
336342
/// This event is valid for the `Connected` state.
337343
CallGetProfile,
338344
}
339-
340-
/// User data provided by the web content, meant to be consumed by user agents
341-
#[derive(Debug, Clone)]
342-
pub struct UserData {
343-
pub(crate) session_token: String,
344-
pub(crate) uid: String,
345-
pub(crate) email: String,
346-
pub(crate) verified: bool,
347-
}

components/fxa-client/src/fxa_client.udl

Lines changed: 19 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,25 @@ interface FirefoxAccount {
168168
///
169169
[Throws=FxaError]
170170
string to_json();
171-
172-
/// Sets the users information based on the web content's login information
173-
/// This is intended to only be used by user agents (eg: Firefox) to set the users
174-
/// session token and tie it to the refresh token that will be issued at the end of the
175-
/// oauth flow.
176-
void set_user_data(UserData user_data);
171+
172+
/// Stores anything necessary from a WebChannel login JSON payload. This includes the session
173+
/// token, but that is abstracted because the consuming apps should not be aware of the
174+
/// specific payload format returned, nor should they get access to the session token
175+
/// directly if possible.
176+
/// The [json_payload] is the `data` object from the `fxaccounts:login` WebChannel command.
177+
[Throws=FxaError]
178+
void handle_web_channel_login(string json_payload);
179+
180+
/// Handle a WebChannel password-change notification by exchanging the new session token
181+
/// for a new refresh token via a network call.
182+
/// The [json_payload] is the `data` object from the `fxaccounts:change_password` WebChannel command.
183+
[Throws=FxaError]
184+
void handle_web_channel_password_change(string json_payload);
185+
186+
/// Returns a complete signedInUser JSON object for a WebChannel fxaccounts:fxa_status response,
187+
/// embedding the session token privately. Email and uid come from the cached profile in internal
188+
/// state. Returns null if no session token is set.
189+
string? get_signed_in_user_for_web_channel();
177190

178191
/// Initiate a web-based OAuth sign-in flow.
179192
///
@@ -650,43 +663,6 @@ interface FirefoxAccount {
650663
[Throws=FxaError]
651664
AccessTokenInfo get_access_token([ByRef] string scope, optional boolean use_cache = true);
652665

653-
/// Get the session token for the user's account, if one is available.
654-
///
655-
/// **💾 This method alters the persisted account state.**
656-
///
657-
/// Applications that function as a web browser may need to hold on to a session token
658-
/// on behalf of Firefox Accounts web content. This method exists so that they can retrieve
659-
/// it an pass it back to said web content when required.
660-
///
661-
/// # Notes
662-
///
663-
/// - Please do not attempt to use the resulting token to directly make calls to the
664-
/// Firefox Accounts servers! All account management functionality should be performed
665-
/// in web content.
666-
/// - A session token is only available to applications that have requested the
667-
/// `https:///identity.mozilla.com/tokens/session` scope.
668-
///
669-
[Throws=FxaError]
670-
string get_session_token();
671-
672-
673-
/// Update the stored session token for the user's account.
674-
///
675-
/// **💾 This method alters the persisted account state.**
676-
///
677-
/// Applications that function as a web browser may need to hold on to a session token
678-
/// on behalf of Firefox Accounts web content. This method exists so that said web content
679-
/// signals that it has generated a new session token, the stored value can be updated
680-
/// to match.
681-
///
682-
/// # Arguments
683-
///
684-
/// - `session_token` - the new session token value provided from web content.
685-
///
686-
[Throws=FxaError]
687-
void handle_session_token_change([ByRef] string session_token );
688-
689-
690666
/// Create a new OAuth authorization code using the stored session token.
691667
///
692668
/// When a signed-in application receives an incoming device pairing request, it can
@@ -1110,10 +1086,3 @@ interface IncomingDeviceCommand {
11101086
/// Indicates that the sender wants to close one or more tabs on this device.
11111087
TabsClosed(Device? sender, CloseTabsPayload payload);
11121088
};
1113-
1114-
dictionary UserData {
1115-
string session_token;
1116-
string uid;
1117-
string email;
1118-
boolean verified;
1119-
};

components/fxa-client/src/internal/oauth.rs

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ use super::{
1111
scoped_keys::ScopedKeysFlow,
1212
util, FirefoxAccount,
1313
};
14-
use crate::auth::UserData;
1514
use crate::{
1615
debug, error, info, warn, AuthorizationParameters, Error, FxaServer, Result, ScopedKey,
1716
};
@@ -125,12 +124,27 @@ impl FirefoxAccount {
125124
Ok(token_info)
126125
}
127126

128-
/// Sets the user data (session token, email, uid)
129-
pub fn set_user_data(&mut self, user_data: UserData) {
130-
// for now, we only have use for the session token
131-
// if we'd like to implement a "Signed in but not verified" state
132-
// we would also consume the other parts of the user data
133-
self.state.set_session_token(user_data.session_token)
127+
/// Extracts and stores the session token from a WebChannel login JSON payload.
128+
/// The JSON payload is the `data` object from the `fxaccounts:login` WebChannel command.
129+
pub fn handle_web_channel_login(&mut self, json_payload: &str) -> Result<()> {
130+
let data: serde_json::Value = serde_json::from_str(json_payload)?;
131+
let token = data
132+
.get("sessionToken")
133+
.and_then(|v| v.as_str())
134+
.ok_or(Error::NoSessionToken)?;
135+
self.state.set_session_token(token.to_string());
136+
Ok(())
137+
}
138+
139+
/// Extracts the session token from a WebChannel password change JSON payload and exchanges it
140+
/// for a new refresh token via a network call.
141+
pub fn handle_web_channel_password_change(&mut self, json_payload: &str) -> Result<()> {
142+
let data: serde_json::Value = serde_json::from_str(json_payload)?;
143+
let token = data
144+
.get("sessionToken")
145+
.and_then(|v| v.as_str())
146+
.ok_or(Error::NoSessionToken)?;
147+
self.handle_session_token_change(token)
134148
}
135149

136150
/// Retrieve the current session token from state
@@ -141,6 +155,26 @@ impl FirefoxAccount {
141155
}
142156
}
143157

158+
/// Builds a complete `signedInUser` JSON object for a WebChannel `fxaccounts:fxa_status`
159+
/// response. Returns `None` if no session token is stored.
160+
/// `email` and `uid` are read from the cached profile; `verified` is always true because
161+
/// the account state machine only completes authentication for verified accounts.
162+
pub fn get_signed_in_user_for_web_channel(&self) -> Option<String> {
163+
let token = self.state.session_token()?;
164+
let profile = self.state.last_seen_profile();
165+
let email = profile.map(|p| p.response.email.as_str());
166+
let uid = profile.map(|p| p.response.uid.as_str());
167+
Some(
168+
serde_json::json!({
169+
"sessionToken": token,
170+
"email": email,
171+
"uid": uid,
172+
"verified": true,
173+
})
174+
.to_string(),
175+
)
176+
}
177+
144178
/// Check whether user is authorized using our refresh token.
145179
pub fn check_authorization_status(&mut self) -> Result<IntrospectInfo> {
146180
let resp = match self.state.refresh_token() {
@@ -1198,17 +1232,14 @@ mod tests {
11981232
}
11991233

12001234
#[test]
1201-
fn test_set_user_data_sets_session_token() {
1235+
fn test_handle_web_channel_login_sets_session_token() {
12021236
nss::ensure_initialized();
12031237
let config = Config::stable_dev("12345678", "https://foo.bar");
12041238
let mut fxa = FirefoxAccount::with_config(config);
1205-
let user_data = UserData {
1206-
session_token: String::from("mock_session_token"),
1207-
uid: String::from("mock_uid_unused"),
1208-
email: String::from("mock_email_usued"),
1209-
verified: true,
1210-
};
1211-
fxa.set_user_data(user_data);
1239+
fxa.handle_web_channel_login(
1240+
r#"{"sessionToken":"mock_session_token","uid":"mock_uid","email":"mock@example.com","verified":true}"#,
1241+
)
1242+
.unwrap();
12121243
assert_eq!(fxa.get_session_token().unwrap(), "mock_session_token");
12131244
}
12141245

@@ -1226,12 +1257,6 @@ mod tests {
12261257
.unwrap();
12271258
let url = Url::parse(&url).unwrap();
12281259
let state = url.query_pairs().find(|(name, _)| name == "state").unwrap();
1229-
let user_data = UserData {
1230-
session_token: String::from("mock_session_token"),
1231-
uid: String::from("mock_uid_unused"),
1232-
email: String::from("mock_email_usued"),
1233-
verified: true,
1234-
};
12351260
let mut client = MockFxAClient::new();
12361261

12371262
client
@@ -1256,8 +1281,7 @@ mod tests {
12561281
.times(1)
12571282
.returning(|_, _| Ok(()));
12581283
fxa.set_client(Arc::new(client));
1259-
1260-
fxa.set_user_data(user_data);
1284+
fxa.set_session_token("mock_session_token");
12611285

12621286
fxa.complete_oauth_flow("mock_code", state.1.as_ref())
12631287
.unwrap();

components/fxa-client/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ use std::fmt;
5353
pub use sync15::DeviceType;
5454
use url::Url;
5555

56-
pub use auth::{AuthorizationInfo, FxaEvent, FxaRustAuthState, FxaState, UserData};
56+
pub use auth::{AuthorizationInfo, FxaEvent, FxaRustAuthState, FxaState};
5757
pub use device::{
5858
AttachedClient, CloseTabsResult, Device, DeviceCapability, DeviceConfig, LocalDevice,
5959
};

components/fxa-client/src/token.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,25 @@ impl FirefoxAccount {
5353
.try_into()
5454
}
5555

56+
/// Builds a complete `signedInUser` JSON object for a WebChannel `fxaccounts:fxa_status`
57+
/// response, embedding the session token without exposing it to the browser layer. Email and
58+
/// uid are read from the cached profile in internal state. Returns `None` if no session token
59+
/// is available.
60+
pub fn get_signed_in_user_for_web_channel(&self) -> Option<String> {
61+
self.internal.lock().get_signed_in_user_for_web_channel()
62+
}
63+
64+
/// Handle a WebChannel password-change notification by exchanging the new session token
65+
/// for a new refresh token.
66+
///
67+
/// **💾 This method alters the persisted account state.**
68+
#[handle_error(Error)]
69+
pub fn handle_web_channel_password_change(&self, json_payload: String) -> ApiResult<()> {
70+
self.internal
71+
.lock()
72+
.handle_web_channel_password_change(&json_payload)
73+
}
74+
5675
/// Get the session token for the user's account, if one is available.
5776
///
5877
/// **💾 This method alters the persisted account state.**

0 commit comments

Comments
 (0)