Skip to content

Commit a322032

Browse files
authored
Add a new "Forbidden" FxA error. (#7320)
This is distinct from Authentication in that there's no implication the account state isn't good, it's just that you aren't allowed to do what you tried. An example of this is asking for an access token you don't have the scopes for. This has the side effect of fixing a bug in `get_access_token()` - when that code calls `exchange_token_for_scope()`, the server responds with a http 403 response - which ends up getting re-thrown as "other error". It arranges for `NoCachedToken` errors to turn into this (that was previously `Authentication`) and for `NoSessionToken` to be `Authentication`. It also tweaks `get_access_token()` to explicitly throw `NoSessionToken` when there's no session token. That way `get_access_token()` continues to return `Authentication` when not logged in, but otherwise will generally return `Forbidden`. Technically a breaking change, but both Android and iOS seem fine with new variants here.
1 parent 2e48b81 commit a322032

4 files changed

Lines changed: 44 additions & 13 deletions

File tree

components/fxa-client/src/error.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ pub enum FxaError {
1818
/// or retry the operation with a freshly-generated token.
1919
#[error("authentication error")]
2020
Authentication,
21+
/// Thrown when an authenticated account isn't allowed to perform some operation. Unlike
22+
/// `Authentication`, there's no problem with the account status. In some cases it
23+
/// might be possible to request additional scopes, and once granted, the operation
24+
/// may succeed.
25+
#[error("forbidden")]
26+
Forbidden,
2127
/// Thrown if an operation fails due to network access problems.
2228
/// The application may retry at a later time once connectivity is restored.
2329
#[error("network error")]
@@ -70,9 +76,6 @@ pub enum Error {
7076
#[error("Multiple OAuth scopes requested")]
7177
MultipleScopesRequested,
7278

73-
#[error("No cached token for scope {0}")]
74-
NoCachedToken(String),
75-
7679
#[error("No cached scoped keys for scope {0}")]
7780
NoScopedKey(String),
7881

@@ -112,6 +115,15 @@ pub enum Error {
112115
#[error("Remote key and local key mismatch")]
113116
MismatchedKeys,
114117

118+
// For example, we requested an access token, the server responded with a 200, but the response body
119+
// had no token. This is a little bit vague, because in many cases you would get a JSON error if the
120+
// shape of the payload was entirely wrong.
121+
#[error("The response from the server, or the content in that reponse, was unexpected")]
122+
UnexpectedServerResponse,
123+
124+
// This should probably be rolled up into `UnexpectedServerResponse`, but the way it is implemented
125+
// and even exposed to consumers makes that a little tricky - but at the end of the day, this can
126+
// only happen when the server gave us a confused response.
115127
#[error("The sync scoped key was missing in the server response")]
116128
SyncScopedKeyMissingInServerResponse,
117129

@@ -209,10 +221,13 @@ impl GetErrorHandling for Error {
209221
match self {
210222
Error::RemoteError { code: 401, .. }
211223
| Error::NoRefreshToken
212-
| Error::NoScopedKey(_)
213-
| Error::NoCachedToken(_) => {
224+
| Error::NoSessionToken
225+
| Error::NoScopedKey(_) => {
214226
ErrorHandling::convert(FxaError::Authentication).log_warning()
215227
}
228+
Error::RemoteError { code: 403, .. } => {
229+
ErrorHandling::convert(FxaError::Forbidden).log_warning()
230+
}
216231
Error::RequestError(_) => ErrorHandling::convert(FxaError::Network).log_warning(),
217232
Error::SyncScopedKeyMissingInServerResponse => {
218233
ErrorHandling::convert(FxaError::SyncScopedKeyMissingInServerResponse)

components/fxa-client/src/fxa_client.udl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ enum FxaError {
5454
/// or retry the operation with a freshly-generated token.
5555
"Authentication",
5656

57+
/// Thrown when an authenticated account isn't allowed to perform some operation. Unlike
58+
/// `Authentication`, there's no problem with the account status. In some cases it
59+
/// might be possible to request additional scopes, and once granted, the operation
60+
/// may succeed.
61+
"Forbidden",
62+
5763
/// Thrown if an operation fails due to network access problems.
5864
/// The application may retry at a later time once connectivity is restored.
5965
"Network",

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,8 @@ mod tests {
325325
let mut fxa = FirefoxAccount::with_config(config);
326326
// No current user -> Error.
327327
match fxa.get_manage_account_url("test").unwrap_err() {
328-
Error::NoCachedToken(_) => {}
329-
_ => panic!("error not NoCachedToken"),
328+
Error::NoSessionToken => {}
329+
_ => panic!("error not NoSessionToken"),
330330
};
331331
// With current user -> expected Url.
332332
fxa.add_cached_profile("123", "test@example.com");
@@ -361,8 +361,8 @@ mod tests {
361361
let mut fxa = FirefoxAccount::with_config(config);
362362
// No current user -> Error.
363363
match fxa.get_manage_devices_url("test").unwrap_err() {
364-
Error::NoCachedToken(_) => {}
365-
_ => panic!("error not NoCachedToken"),
364+
Error::NoSessionToken => {}
365+
_ => panic!("error not NoSessionToken"),
366366
};
367367
// With current user -> expected Url.
368368
fxa.add_cached_profile("123", "test@example.com");

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use super::{
1212
util, FirefoxAccount,
1313
};
1414
use crate::auth::UserData;
15-
use crate::{warn, AuthorizationParameters, Error, FxaServer, Result, ScopedKey};
15+
use crate::{error, warn, AuthorizationParameters, Error, FxaServer, Result, ScopedKey};
1616
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
1717
use jwcrypto::{EncryptionAlgorithm, EncryptionParameters};
1818
use rate_limiter::RateLimiter;
@@ -71,10 +71,16 @@ impl FirefoxAccount {
7171
new_refresh_token,
7272
exchange_resp.scope,
7373
));
74+
} else {
75+
// A request for a new token succeeding but without a new token is unexpected.
76+
error!("successful response for a new refresh token with additional scopes, but no token was delivered");
77+
// at this stage we are almost certainly still going to fail to get a token...
7478
}
7579
// Get the updated refresh token from state.
7680
refresh_token = match self.state.refresh_token() {
77-
None => return Err(Error::NoCachedToken(scope.to_string())),
81+
// We had a refresh token, we must either still have the original or maybe a new one,
82+
// but it's impossible for us to not have one at this point.
83+
None => unreachable!("lost the refresh token"),
7884
Some(token) => token,
7985
};
8086
}
@@ -86,7 +92,11 @@ impl FirefoxAccount {
8692
&[scope],
8793
)?
8894
} else {
89-
return Err(Error::NoCachedToken(scope.to_string()));
95+
// This should be impossible - if we don't have the scope we would have entered
96+
// the block where we try and get it, that succeeded and we got a new refresh token,
97+
// but still don't have the scope.
98+
error!("New refresh token doesn't have the scope we requested: {scope}");
99+
return Err(Error::UnexpectedServerResponse);
90100
}
91101
}
92102
None => match self.state.session_token() {
@@ -95,7 +105,7 @@ impl FirefoxAccount {
95105
session_token,
96106
&[scope],
97107
)?,
98-
None => return Err(Error::NoCachedToken(scope.to_string())),
108+
None => return Err(Error::NoSessionToken),
99109
},
100110
};
101111
let since_epoch = SystemTime::now()

0 commit comments

Comments
 (0)