atagen

matrix-rust-sdk

public
Star0
Watch0

Commit fc58f766d35e

tarball

atagen <boss@atagen.co>2026-10-22 12:30 UTC

3 files changed+97-9
oauth: let a pending authorization be exported and restored
Modified file: crates/matrix-sdk/src/authentication/oauth/auth_code_builder.rs+2 additions−2 deletions
@@ -24,7 +24,7 @@ use tracing::{info, instrument};
2424 use url::Url;
2525
2626 use super::{ClientRegistrationData, OAuth, OAuthError};
27-use crate::{Result, authentication::oauth::AuthorizationValidationData};
27+use crate::{Result, authentication::oauth::PendingAuthorization};
2828
2929 /// Builder type used to configure optional settings for authorization with an
3030 /// OAuth 2.0 authorization server via the Authorization Code flow.
@@ -156,7 +156,7 @@ impl OAuthAuthCodeUrlBuilder {
156156
157157 data.authorization_data.lock().await.insert(
158158 state.clone(),
159- AuthorizationValidationData { server_metadata, device_id, redirect_uri, pkce_verifier },
159+ PendingAuthorization { server_metadata, device_id, redirect_uri, pkce_verifier },
160160 );
161161
162162 Ok(OAuthAuthorizationData { url, state })
Modified file: crates/matrix-sdk/src/authentication/oauth/mod.rs+63 additions−3 deletions
@@ -262,7 +262,7 @@ impl OAuthCtx {
262262 pub(crate) struct OAuthAuthData {
263263 pub(crate) client_id: ClientId,
264264 /// The data necessary to validate authorization responses.
265- authorization_data: Mutex<HashMap<CsrfToken, AuthorizationValidationData>>,
265+ authorization_data: Mutex<HashMap<CsrfToken, PendingAuthorization>>,
266266 }
267267
268268 #[cfg(not(tarpaulin_include))]
@@ -1071,6 +1071,48 @@ impl OAuth {
10711071 }
10721072 }
10731073
1074+ /// Get the data of a login that was started but not finished yet, to
1075+ /// restore it with [`OAuth::restore_pending_authorization()`], typically
1076+ /// in another process.
1077+ ///
1078+ /// The returned data contains the PKCE code verifier of the authorization
1079+ /// request, so it must be stored as a secret.
1080+ ///
1081+ /// # Arguments
1082+ ///
1083+ /// * `state` - The state provided in [`OAuthAuthorizationData`] after
1084+ /// building the authorization URL.
1085+ pub async fn pending_authorization(&self, state: &CsrfToken) -> Option<PendingAuthorization> {
1086+ self.data()?.authorization_data.lock().await.get(state).cloned()
1087+ }
1088+
1089+ /// Restore the data of a login that was started, possibly in another
1090+ /// process, and exported with [`OAuth::pending_authorization()`], so it
1091+ /// can be finished with [`OAuth::finish_login()`].
1092+ ///
1093+ /// The client must have been registered first, for example with
1094+ /// [`OAuth::restore_registered_client()`] and the same client ID as when
1095+ /// the login was started.
1096+ ///
1097+ /// The data contains the PKCE code verifier of the authorization request,
1098+ /// so it must be stored as a secret.
1099+ ///
1100+ /// # Arguments
1101+ ///
1102+ /// * `state` - The state provided in [`OAuthAuthorizationData`] after
1103+ /// building the authorization URL.
1104+ ///
1105+ /// * `data` - The data returned by [`OAuth::pending_authorization()`].
1106+ pub async fn restore_pending_authorization(
1107+ &self,
1108+ state: CsrfToken,
1109+ data: PendingAuthorization,
1110+ ) -> Result<(), OAuthError> {
1111+ let auth_data = self.data().ok_or(OAuthError::NotRegistered)?;
1112+ auth_data.authorization_data.lock().await.insert(state, data);
1113+ Ok(())
1114+ }
1115+
10741116 /// Request codes from the authorization server for logging in with another
10751117 /// device.
10761118 #[cfg(feature = "e2e-encryption")]
@@ -1740,8 +1782,15 @@ pub struct UserSession {
17401782
17411783 /// The data necessary to validate a response from the Token endpoint in the
17421784 /// Authorization Code flow.
1743-#[derive(Debug)]
1744-struct AuthorizationValidationData {
1785+///
1786+/// It can be exported with [`OAuth::pending_authorization()`] and restored
1787+/// with [`OAuth::restore_pending_authorization()`], to finish a login in
1788+/// another process than the one that started it.
1789+///
1790+/// It contains the PKCE code verifier of the authorization request, so it
1791+/// must be stored as a secret.
1792+#[derive(Debug, Serialize, Deserialize)]
1793+pub struct PendingAuthorization {
17451794 /// The metadata of the server,
17461795 server_metadata: AuthorizationServerMetadata,
17471796
@@ -1755,6 +1804,17 @@ struct AuthorizationValidationData {
17551804 pkce_verifier: PkceCodeVerifier,
17561805 }
17571806
1807+impl Clone for PendingAuthorization {
1808+ fn clone(&self) -> Self {
1809+ Self {
1810+ server_metadata: self.server_metadata.clone(),
1811+ device_id: self.device_id.clone(),
1812+ redirect_uri: self.redirect_uri.clone(),
1813+ pkce_verifier: PkceCodeVerifier::new(self.pkce_verifier.secret().clone()),
1814+ }
1815+ }
1816+}
1817+
17581818 /// The data returned by the server in the redirect URI after a successful
17591819 /// authorization.
17601820 #[derive(Debug, Clone)]
Modified file: crates/matrix-sdk/src/authentication/oauth/tests.rs+32 additions−4 deletions
@@ -20,7 +20,7 @@ use super::{
2020 use crate::{
2121 Client, Error, SessionChange,
2222 authentication::oauth::{
23- AuthorizationValidationData, ClientRegistrationData, OAuthAuthorizationCodeError,
23+ ClientRegistrationData, OAuthAuthorizationCodeError, PendingAuthorization,
2424 error::{AuthorizationCodeErrorResponseType, OAuthClientRegistrationError},
2525 },
2626 client::caches::CachedValue,
@@ -203,6 +203,34 @@ async fn test_high_level_login() -> anyhow::Result<()> {
203203 }
204204
205205 #[async_test]
206+async fn test_high_level_login_in_another_client() -> anyhow::Result<()> {
207+ // Given a login started by a client.
208+ let (oauth, server, mut redirect_uri, registration_data) = mock_environment().await.unwrap();
209+
210+ let authorization_data = oauth
211+ .login(redirect_uri.clone(), None, Some(registration_data), None)
212+ .build()
213+ .await
214+ .unwrap();
215+
216+ // When its pending authorization is exported,
217+ let client_id = oauth.client_id().unwrap().clone();
218+ let pending = oauth.pending_authorization(&authorization_data.state).await.unwrap();
219+ let pending = serde_json::from_str(&serde_json::to_string(&pending)?)?;
220+
221+ // And restored in a fresh client,
222+ let other_oauth = server.client_builder().unlogged().build().await.oauth();
223+ other_oauth.restore_registered_client(client_id);
224+ other_oauth.restore_pending_authorization(authorization_data.state.clone(), pending).await?;
225+
226+ // Then that client can finish the login.
227+ redirect_uri.set_query(Some(&format!("code=42&state={}", authorization_data.state.secret())));
228+ other_oauth.finish_login(redirect_uri.into()).await?;
229+
230+ Ok(())
231+}
232+
233+#[async_test]
206234 async fn test_high_level_login_cancellation() -> anyhow::Result<()> {
207235 // Given a client ready to complete login.
208236 let (oauth, server, mut redirect_uri, registration_data) = mock_environment().await.unwrap();
@@ -410,7 +438,7 @@ async fn test_finish_login() -> anyhow::Result<()> {
410438 let state1 = CsrfToken::new("state1".to_owned());
411439 let redirect_uri = REDIRECT_URI_STRING;
412440 let (_pkce_code_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
413- let auth_validation_data = AuthorizationValidationData {
441+ let auth_validation_data = PendingAuthorization {
414442 server_metadata: server_metadata.clone(),
415443 device_id: owned_device_id!("D3V1C31D"),
416444 redirect_uri: RedirectUrl::new(redirect_uri.to_owned())?,
@@ -465,7 +493,7 @@ async fn test_finish_login() -> anyhow::Result<()> {
465493 let state2 = CsrfToken::new("state2".to_owned());
466494 let redirect_uri = REDIRECT_URI_STRING;
467495 let (_pkce_code_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
468- let auth_validation_data = AuthorizationValidationData {
496+ let auth_validation_data = PendingAuthorization {
469497 server_metadata: server_metadata.clone(),
470498 device_id: owned_device_id!("D3V1C31D"),
471499 redirect_uri: RedirectUrl::new(redirect_uri.to_owned())?,
@@ -509,7 +537,7 @@ async fn test_finish_login() -> anyhow::Result<()> {
509537 let state3 = CsrfToken::new("state3".to_owned());
510538 let redirect_uri = REDIRECT_URI_STRING;
511539 let (_pkce_code_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
512- let auth_validation_data = AuthorizationValidationData {
540+ let auth_validation_data = PendingAuthorization {
513541 server_metadata,
514542 device_id: wrong_device_id.to_owned(),
515543 redirect_uri: RedirectUrl::new(redirect_uri.to_owned())?,