mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-07-31 01:46:51 -04:00
fix(oauth): fetch server metadata before capturing the refresh token
refresh_access_token validated the session hash and captured the refresh token before awaiting server_metadata(). On iOS the process can be suspended during that request; while suspended the cross-process lock lease lapses and a sibling process (the NSE) can refresh and rotate the token. On resume the app exchanged the now-stale token and was hard logged out with invalid_grant. Fetch the server metadata and client id before taking the cross-process lock, and read the refresh token after the hash check, so a rotation that happens during the metadata request is detected as a hash mismatch and handled via the existing optimistic exit instead of exchanging a stale token.
This commit is contained in:
@@ -1228,6 +1228,31 @@ impl OAuth {
|
||||
|
||||
debug!("no other refresh happening in background, starting.");
|
||||
|
||||
// Fetch the authorization server metadata *before* taking the cross-process
|
||||
// lock, checking the session hash, or reading the refresh token. This
|
||||
// request can stall for a long time when the OS suspends the process (e.g.
|
||||
// iOS background suspension). Doing it first means that when we resume we
|
||||
// still (re)acquire the cross-process lock and re-check the session hash
|
||||
// afterwards, so a refresh token that a sibling process (such as the NSE)
|
||||
// rotated while we were suspended is detected here as a hash mismatch and
|
||||
// handled via the optimistic exit below — instead of being exchanged while
|
||||
// stale and getting the account hard-logged-out with `invalid_grant`.
|
||||
let server_metadata = match self.server_metadata().await {
|
||||
Ok(metadata) => metadata,
|
||||
Err(err) => {
|
||||
warn!("couldn't get authorization server metadata: {err:?}");
|
||||
fail!(refresh_status_guard, RefreshTokenError::OAuth(Arc::new(err.into())));
|
||||
}
|
||||
};
|
||||
|
||||
let Some(client_id) = self.client_id().cloned() else {
|
||||
warn!("invalid state: missing client ID");
|
||||
fail!(
|
||||
refresh_status_guard,
|
||||
RefreshTokenError::OAuth(Arc::new(OAuthError::NotAuthenticated))
|
||||
);
|
||||
};
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
let cross_process_guard =
|
||||
if let Some(manager) = self.ctx().cross_process_token_refresh_manager.get() {
|
||||
@@ -1259,6 +1284,9 @@ impl OAuth {
|
||||
None
|
||||
};
|
||||
|
||||
// Read the refresh token only now — after the post-metadata hash check — so
|
||||
// we always exchange the token that is current in the store, never one that
|
||||
// was rotated out from under us while we were suspended.
|
||||
let Some(session_tokens) = self.client.session_tokens() else {
|
||||
warn!("invalid state: missing session tokens");
|
||||
fail!(refresh_status_guard, RefreshTokenError::RefreshTokenRequired);
|
||||
@@ -1269,22 +1297,6 @@ impl OAuth {
|
||||
fail!(refresh_status_guard, RefreshTokenError::RefreshTokenRequired);
|
||||
};
|
||||
|
||||
let server_metadata = match self.server_metadata().await {
|
||||
Ok(metadata) => metadata,
|
||||
Err(err) => {
|
||||
warn!("couldn't get authorization server metadata: {err:?}");
|
||||
fail!(refresh_status_guard, RefreshTokenError::OAuth(Arc::new(err.into())));
|
||||
}
|
||||
};
|
||||
|
||||
let Some(client_id) = self.client_id().cloned() else {
|
||||
warn!("invalid state: missing client ID");
|
||||
fail!(
|
||||
refresh_status_guard,
|
||||
RefreshTokenError::OAuth(Arc::new(OAuthError::NotAuthenticated))
|
||||
);
|
||||
};
|
||||
|
||||
// Do not interrupt refresh access token requests and processing, by detaching
|
||||
// the request sending and response processing.
|
||||
// Make sure to keep the `refresh_status_guard` during the entire processing.
|
||||
|
||||
Reference in New Issue
Block a user