Commit fc58f766d35e
tarball3 files changed+97-9
@@ -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 an3030
/// 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 })@@ -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, to1075
+ /// restore it with [`OAuth::restore_pending_authorization()`], typically1076
+ /// in another process.1077
+ ///1078
+ /// The returned data contains the PKCE code verifier of the authorization1079
+ /// request, so it must be stored as a secret.1080
+ ///1081
+ /// # Arguments1082
+ ///1083
+ /// * `state` - The state provided in [`OAuthAuthorizationData`] after1084
+ /// 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 another1090
+ /// process, and exported with [`OAuth::pending_authorization()`], so it1091
+ /// can be finished with [`OAuth::finish_login()`].1092
+ ///1093
+ /// The client must have been registered first, for example with1094
+ /// [`OAuth::restore_registered_client()`] and the same client ID as when1095
+ /// 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
+ /// # Arguments1101
+ ///1102
+ /// * `state` - The state provided in [`OAuthAuthorizationData`] after1103
+ /// 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 another10751117
/// 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 the17421784
/// Authorization Code flow.1743
-#[derive(Debug)]1744
-struct AuthorizationValidationData {1785
+///1786
+/// It can be exported with [`OAuth::pending_authorization()`] and restored1787
+/// with [`OAuth::restore_pending_authorization()`], to finish a login in1788
+/// another process than the one that started it.1789
+///1790
+/// It contains the PKCE code verifier of the authorization request, so it1791
+/// 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 successful17591819
/// authorization.17601820
#[derive(Debug, Clone)]@@ -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 = oauth211
+ .login(redirect_uri.clone(), None, Some(registration_data), None)212
+ .build()213
+ .await214
+ .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())?,