#[cfg(feature = "e2e-encryption")]
use std::time::Duration;
use std::{
borrow::Cow,
collections::{BTreeSet, HashMap},
fmt,
sync::Arc,
};
use as_variant::as_variant;
#[cfg(feature = "e2e-encryption")]
use error::CrossProcessRefreshLockError;
use error::{
OAuthAuthorizationCodeError, OAuthClientRegistrationError, OAuthDiscoveryError,
OAuthTokenRevocationError, RedirectUriQueryParseError,
};
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_base::crypto::types::qr_login::QrCodeData;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_base::once_cell::sync::OnceCell;
use matrix_sdk_base::{SessionMeta, store::RoomLoadSettings};
use oauth2::{
AccessToken, PkceCodeVerifier, RedirectUrl, RefreshToken, RevocationUrl, Scope,
StandardErrorResponse, StandardRevocableToken, TokenResponse, TokenUrl,
basic::BasicClient as OAuthClient,
};
pub use oauth2::{ClientId, CsrfToken};
use ruma::{
DeviceId, OwnedDeviceId,
api::client::discovery::get_authorization_server_metadata::{
self,
v1::{AccountManagementAction, AuthorizationServerMetadata},
},
serde::Raw,
};
use serde::{Deserialize, Serialize};
use sha2::Digest as _;
use tokio::sync::Mutex;
use tracing::{debug, error, instrument, trace, warn};
use url::Url;
mod account_management_url;
mod auth_code_builder;
#[cfg(feature = "e2e-encryption")]
mod cross_process;
pub mod error;
mod http_client;
#[cfg(feature = "e2e-encryption")]
pub mod qrcode;
pub mod registration;
#[cfg(all(test, not(target_family = "wasm")))]
mod tests;
#[cfg(feature = "e2e-encryption")]
use self::cross_process::{CrossProcessRefreshLockGuard, CrossProcessRefreshManager};
#[cfg(feature = "e2e-encryption")]
use self::qrcode::{
GrantLoginWithGeneratedQrCode, GrantLoginWithScannedQrCode, LoginWithGeneratedQrCode,
LoginWithQrCode,
};
pub use self::{
account_management_url::{AccountManagementActionFull, AccountManagementUrlBuilder},
auth_code_builder::{OAuthAuthCodeUrlBuilder, OAuthAuthorizationData},
error::OAuthError,
};
use self::{
http_client::OAuthHttpClient,
registration::{ClientMetadata, ClientRegistrationResponse, register_client},
};
use super::{AuthData, SessionTokens};
use crate::{Client, HttpError, RefreshTokenError, Result, client::SessionChange, executor::spawn};
pub(crate) struct OAuthCtx {
#[cfg(feature = "e2e-encryption")]
cross_process_token_refresh_manager: OnceCell<CrossProcessRefreshManager>,
#[cfg(feature = "e2e-encryption")]
deferred_cross_process_lock_init: Mutex<Option<String>>,
insecure_discover: bool,
}
impl OAuthCtx {
pub(crate) fn new(insecure_discover: bool) -> Self {
Self {
insecure_discover,
#[cfg(feature = "e2e-encryption")]
cross_process_token_refresh_manager: Default::default(),
#[cfg(feature = "e2e-encryption")]
deferred_cross_process_lock_init: Default::default(),
}
}
}
pub(crate) struct OAuthAuthData {
pub(crate) client_id: ClientId,
authorization_data: Mutex<HashMap<CsrfToken, AuthorizationValidationData>>,
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for OAuthAuthData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OAuthAuthData").finish_non_exhaustive()
}
}
#[derive(Debug, Clone)]
pub struct OAuth {
client: Client,
http_client: OAuthHttpClient,
}
impl OAuth {
pub(crate) fn new(client: Client) -> Self {
let http_client = OAuthHttpClient {
inner: client.inner.http_client.inner.clone(),
#[cfg(test)]
insecure_rewrite_https_to_http: false,
};
Self { client, http_client }
}
#[cfg(test)]
pub(crate) fn insecure_rewrite_https_to_http(mut self) -> Self {
self.http_client.insecure_rewrite_https_to_http = true;
self
}
fn ctx(&self) -> &OAuthCtx {
&self.client.auth_ctx().oauth
}
fn http_client(&self) -> &OAuthHttpClient {
&self.http_client
}
#[cfg(feature = "e2e-encryption")]
pub async fn enable_cross_process_refresh_lock(
&self,
lock_value: String,
) -> Result<(), OAuthError> {
let mut lock = self.ctx().deferred_cross_process_lock_init.lock().await;
if lock.is_some() {
return Err(CrossProcessRefreshLockError::DuplicatedLock.into());
}
*lock = Some(lock_value);
Ok(())
}
#[cfg(feature = "e2e-encryption")]
async fn deferred_enable_cross_process_refresh_lock(&self) {
let deferred_init_lock = self.ctx().deferred_cross_process_lock_init.lock().await;
let Some(lock_value) = deferred_init_lock.as_ref() else {
return;
};
let olm_machine_lock = self.client.olm_machine().await;
let olm_machine =
olm_machine_lock.as_ref().expect("there has to be an olm machine, hopefully?");
let store = olm_machine.store();
let lock =
store.create_store_lock("oidc_session_refresh_lock".to_owned(), lock_value.clone());
let manager = CrossProcessRefreshManager::new(store.clone(), lock);
let _ = self.ctx().cross_process_token_refresh_manager.set(manager);
}
fn data(&self) -> Option<&OAuthAuthData> {
let data = self.client.auth_ctx().auth_data.get()?;
as_variant!(data, AuthData::OAuth)
}
#[cfg(feature = "e2e-encryption")]
pub fn login_with_qr_code<'a>(
&'a self,
registration_data: Option<&'a ClientRegistrationData>,
) -> LoginWithQrCodeBuilder<'a> {
LoginWithQrCodeBuilder { client: &self.client, registration_data }
}
#[cfg(feature = "e2e-encryption")]
pub fn grant_login_with_qr_code<'a>(&'a self) -> GrantLoginWithQrCodeBuilder<'a> {
GrantLoginWithQrCodeBuilder::new(&self.client)
}
async fn use_registration_data(
&self,
server_metadata: &AuthorizationServerMetadata,
data: Option<&ClientRegistrationData>,
) -> std::result::Result<(), OAuthError> {
if self.client_id().is_some() {
tracing::info!("OAuth 2.0 is already configured.");
return Ok(());
}
let Some(data) = data else {
return Err(OAuthError::NotRegistered);
};
if let Some(static_registrations) = &data.static_registrations {
let client_id = static_registrations
.get(&self.client.homeserver())
.or_else(|| static_registrations.get(&server_metadata.issuer));
if let Some(client_id) = client_id {
self.restore_registered_client(client_id.clone());
return Ok(());
}
}
self.register_client_inner(server_metadata, &data.metadata).await?;
Ok(())
}
pub async fn account_management_actions_supported(
&self,
) -> Result<BTreeSet<AccountManagementAction>, OAuthError> {
let server_metadata = self.server_metadata().await?;
Ok(server_metadata.account_management_actions_supported)
}
pub async fn fetch_account_management_url(
&self,
) -> Result<Option<AccountManagementUrlBuilder>, OAuthError> {
let server_metadata = self.server_metadata().await?;
Ok(server_metadata.account_management_uri.map(AccountManagementUrlBuilder::new))
}
pub async fn account_management_url(
&self,
) -> Result<Option<AccountManagementUrlBuilder>, OAuthError> {
const CACHE_KEY: &str = "SERVER_METADATA";
let mut cache = self.client.inner.caches.server_metadata.lock().await;
let metadata = if let Some(metadata) = cache.get(CACHE_KEY) {
metadata
} else {
let server_metadata = self.server_metadata().await?;
cache.insert(CACHE_KEY.to_owned(), server_metadata.clone());
server_metadata
};
Ok(metadata.account_management_uri.map(AccountManagementUrlBuilder::new))
}
pub async fn server_metadata(
&self,
) -> Result<AuthorizationServerMetadata, OAuthDiscoveryError> {
let is_endpoint_unsupported = |error: &HttpError| {
error
.as_client_api_error()
.is_some_and(|err| err.status_code == http::StatusCode::NOT_FOUND)
};
let response =
self.client.send(get_authorization_server_metadata::v1::Request::new()).await.map_err(
|error| {
if is_endpoint_unsupported(&error) {
OAuthDiscoveryError::NotSupported
} else {
error.into()
}
},
)?;
let metadata = response.metadata.deserialize()?;
if self.ctx().insecure_discover {
metadata.insecure_validate_urls()?;
} else {
metadata.validate_urls()?;
}
Ok(metadata)
}
pub fn client_id(&self) -> Option<&ClientId> {
self.data().map(|data| &data.client_id)
}
pub fn user_session(&self) -> Option<UserSession> {
let meta = self.client.session_meta()?.to_owned();
let tokens = self.client.session_tokens()?;
Some(UserSession { meta, tokens })
}
pub fn full_session(&self) -> Option<OAuthSession> {
let user = self.user_session()?;
let data = self.data()?;
Some(OAuthSession { client_id: data.client_id.clone(), user })
}
pub async fn register_client(
&self,
client_metadata: &Raw<ClientMetadata>,
) -> Result<ClientRegistrationResponse, OAuthError> {
let server_metadata = self.server_metadata().await?;
Ok(self.register_client_inner(&server_metadata, client_metadata).await?)
}
async fn register_client_inner(
&self,
server_metadata: &AuthorizationServerMetadata,
client_metadata: &Raw<ClientMetadata>,
) -> Result<ClientRegistrationResponse, OAuthClientRegistrationError> {
let registration_endpoint = server_metadata
.registration_endpoint
.as_ref()
.ok_or(OAuthClientRegistrationError::NotSupported)?;
let registration_response =
register_client(self.http_client(), registration_endpoint, client_metadata).await?;
self.restore_registered_client(registration_response.client_id.clone());
Ok(registration_response)
}
pub fn restore_registered_client(&self, client_id: ClientId) {
let data = OAuthAuthData { client_id, authorization_data: Default::default() };
self.client
.auth_ctx()
.auth_data
.set(AuthData::OAuth(data))
.expect("Client authentication data was already set");
}
pub async fn restore_session(
&self,
session: OAuthSession,
room_load_settings: RoomLoadSettings,
) -> Result<()> {
let OAuthSession { client_id, user: UserSession { meta, tokens } } = session;
let data = OAuthAuthData { client_id, authorization_data: Default::default() };
self.client.auth_ctx().set_session_tokens(tokens.clone());
self.client
.base_client()
.activate(
meta,
room_load_settings,
#[cfg(feature = "e2e-encryption")]
None,
)
.await?;
#[cfg(feature = "e2e-encryption")]
self.deferred_enable_cross_process_refresh_lock().await;
self.client
.inner
.auth_ctx
.auth_data
.set(AuthData::OAuth(data))
.expect("Client authentication data was already set");
#[cfg(feature = "e2e-encryption")]
if let Some(cross_process_lock) = self.ctx().cross_process_token_refresh_manager.get() {
cross_process_lock.restore_session(&tokens).await;
let mut guard = cross_process_lock
.spin_lock()
.await
.map_err(|err| crate::Error::OAuth(Box::new(err.into())))?;
if guard.hash_mismatch {
Box::pin(self.handle_session_hash_mismatch(&mut guard))
.await
.map_err(|err| crate::Error::OAuth(Box::new(err.into())))?;
} else {
guard
.save_in_memory_and_db(&tokens)
.await
.map_err(|err| crate::Error::OAuth(Box::new(err.into())))?;
}
}
#[cfg(feature = "e2e-encryption")]
self.client.encryption().spawn_initialization_task(None).await;
Ok(())
}
#[cfg(feature = "e2e-encryption")]
async fn handle_session_hash_mismatch(
&self,
guard: &mut CrossProcessRefreshLockGuard,
) -> Result<(), CrossProcessRefreshLockError> {
trace!("Handling hash mismatch.");
let callback = self
.client
.auth_ctx()
.reload_session_callback
.get()
.ok_or(CrossProcessRefreshLockError::MissingReloadSession)?;
match callback(self.client.clone()) {
Ok(tokens) => {
guard.handle_mismatch(&tokens).await?;
self.client.auth_ctx().set_session_tokens(tokens.clone());
}
Err(err) => {
error!("when reloading OAuth 2.0 session tokens from callback: {err}");
}
}
Ok(())
}
fn login_scopes(
device_id: Option<OwnedDeviceId>,
additional_scopes: Option<Vec<Scope>>,
) -> (Vec<Scope>, OwnedDeviceId) {
const SCOPE_MATRIX_CLIENT_SERVER_API_FULL_ACCESS: &str =
"urn:matrix:org.matrix.msc2967.client:api:*";
const SCOPE_MATRIX_DEVICE_ID_PREFIX: &str = "urn:matrix:org.matrix.msc2967.client:device:";
let device_id = device_id.unwrap_or_else(DeviceId::new);
let mut scopes = vec![
Scope::new(SCOPE_MATRIX_CLIENT_SERVER_API_FULL_ACCESS.to_owned()),
Scope::new(format!("{SCOPE_MATRIX_DEVICE_ID_PREFIX}{device_id}")),
];
if let Some(extra_scopes) = additional_scopes {
scopes.extend(extra_scopes);
}
(scopes, device_id)
}
pub fn login(
&self,
redirect_uri: Url,
device_id: Option<OwnedDeviceId>,
registration_data: Option<ClientRegistrationData>,
additional_scopes: Option<Vec<Scope>>,
) -> OAuthAuthCodeUrlBuilder {
let (scopes, device_id) = Self::login_scopes(device_id, additional_scopes);
OAuthAuthCodeUrlBuilder::new(
self.clone(),
scopes.to_vec(),
device_id,
redirect_uri,
registration_data,
)
}
pub async fn finish_login(&self, url_or_query: UrlOrQuery) -> Result<()> {
let response = AuthorizationResponse::parse_url_or_query(&url_or_query)
.map_err(|error| OAuthError::from(OAuthAuthorizationCodeError::from(error)))?;
let auth_code = match response {
AuthorizationResponse::Success(code) => code,
AuthorizationResponse::Error(error) => {
self.abort_login(&error.state).await;
return Err(OAuthError::from(OAuthAuthorizationCodeError::from(error.error)).into());
}
};
let device_id = self.finish_authorization(auth_code).await?;
self.load_session(device_id).await
}
pub(crate) async fn load_session(&self, device_id: OwnedDeviceId) -> Result<()> {
let whoami_res = self.client.whoami().await.map_err(crate::Error::from)?;
let new_session = SessionMeta { user_id: whoami_res.user_id, device_id };
if let Some(current_session) = self.client.session_meta() {
if new_session != *current_session {
return Err(OAuthError::SessionMismatch.into());
}
} else {
self.client
.base_client()
.activate(
new_session,
RoomLoadSettings::default(),
#[cfg(feature = "e2e-encryption")]
None,
)
.await?;
#[cfg(feature = "e2e-encryption")]
self.enable_cross_process_lock().await.map_err(OAuthError::from)?;
#[cfg(feature = "e2e-encryption")]
self.client.encryption().spawn_initialization_task(None).await;
}
Ok(())
}
#[cfg(feature = "e2e-encryption")]
pub(crate) async fn enable_cross_process_lock(
&self,
) -> Result<(), CrossProcessRefreshLockError> {
self.deferred_enable_cross_process_refresh_lock().await;
if let Some(cross_process_manager) = self.ctx().cross_process_token_refresh_manager.get()
&& let Some(tokens) = self.client.session_tokens()
{
let mut cross_process_guard = cross_process_manager.spin_lock().await?;
if cross_process_guard.hash_mismatch {
warn!("unexpected cross-process hash mismatch when finishing login (see comment)");
}
cross_process_guard.save_in_memory_and_db(&tokens).await?;
}
Ok(())
}
async fn finish_authorization(
&self,
auth_code: AuthorizationCode,
) -> Result<OwnedDeviceId, OAuthError> {
let data = self.data().ok_or(OAuthError::NotAuthenticated)?;
let client_id = data.client_id.clone();
let validation_data = data
.authorization_data
.lock()
.await
.remove(&auth_code.state)
.ok_or(OAuthAuthorizationCodeError::InvalidState)?;
let token_uri = TokenUrl::from_url(validation_data.server_metadata.token_endpoint.clone());
let response = OAuthClient::new(client_id)
.set_token_uri(token_uri)
.exchange_code(oauth2::AuthorizationCode::new(auth_code.code))
.set_pkce_verifier(validation_data.pkce_verifier)
.set_redirect_uri(Cow::Owned(validation_data.redirect_uri))
.request_async(self.http_client())
.await
.map_err(OAuthAuthorizationCodeError::RequestToken)?;
self.client.auth_ctx().set_session_tokens(SessionTokens {
access_token: response.access_token().secret().clone(),
refresh_token: response.refresh_token().map(RefreshToken::secret).cloned(),
});
Ok(validation_data.device_id)
}
pub async fn abort_login(&self, state: &CsrfToken) {
if let Some(data) = self.data() {
data.authorization_data.lock().await.remove(state);
}
}
#[cfg(feature = "e2e-encryption")]
async fn request_device_authorization(
&self,
server_metadata: &AuthorizationServerMetadata,
device_id: Option<OwnedDeviceId>,
) -> Result<oauth2::StandardDeviceAuthorizationResponse, qrcode::DeviceAuthorizationOAuthError>
{
let (scopes, _) = Self::login_scopes(device_id, None);
let client_id = self.client_id().ok_or(OAuthError::NotRegistered)?.clone();
let device_authorization_url = server_metadata
.device_authorization_endpoint
.clone()
.map(oauth2::DeviceAuthorizationUrl::from_url)
.ok_or(qrcode::DeviceAuthorizationOAuthError::NoDeviceAuthorizationEndpoint)?;
let response = OAuthClient::new(client_id)
.set_device_authorization_url(device_authorization_url)
.exchange_device_code()
.add_scopes(scopes)
.request_async(self.http_client())
.await?;
Ok(response)
}
#[cfg(feature = "e2e-encryption")]
async fn exchange_device_code(
&self,
server_metadata: &AuthorizationServerMetadata,
device_authorization_response: &oauth2::StandardDeviceAuthorizationResponse,
) -> Result<(), qrcode::DeviceAuthorizationOAuthError> {
use oauth2::TokenResponse;
let client_id = self.client_id().ok_or(OAuthError::NotRegistered)?.clone();
let token_uri = TokenUrl::from_url(server_metadata.token_endpoint.clone());
let response = OAuthClient::new(client_id)
.set_token_uri(token_uri)
.exchange_device_access_token(device_authorization_response)
.request_async(self.http_client(), tokio::time::sleep, None)
.await?;
self.client.auth_ctx().set_session_tokens(SessionTokens {
access_token: response.access_token().secret().to_owned(),
refresh_token: response.refresh_token().map(|t| t.secret().to_owned()),
});
Ok(())
}
async fn refresh_access_token_inner(
self,
refresh_token: String,
token_endpoint: Url,
client_id: ClientId,
#[cfg(feature = "e2e-encryption")] cross_process_lock: Option<CrossProcessRefreshLockGuard>,
) -> Result<(), OAuthError> {
trace!(
"Token refresh: attempting to refresh with refresh_token {:x}",
hash_str(&refresh_token)
);
let token = RefreshToken::new(refresh_token.clone());
let token_uri = TokenUrl::from_url(token_endpoint);
let response = OAuthClient::new(client_id)
.set_token_uri(token_uri)
.exchange_refresh_token(&token)
.request_async(self.http_client())
.await
.map_err(OAuthError::RefreshToken)?;
let new_access_token = response.access_token().secret().clone();
let new_refresh_token = response.refresh_token().map(RefreshToken::secret).cloned();
trace!(
"Token refresh: new refresh_token: {} / access_token: {:x}",
new_refresh_token
.as_deref()
.map(|token| format!("{:x}", hash_str(token)))
.unwrap_or_else(|| "<none>".to_owned()),
hash_str(&new_access_token)
);
let tokens = SessionTokens {
access_token: new_access_token,
refresh_token: new_refresh_token.or(Some(refresh_token)),
};
#[cfg(feature = "e2e-encryption")]
let tokens_clone = tokens.clone();
self.client.auth_ctx().set_session_tokens(tokens);
if let Some(save_session_callback) = self.client.auth_ctx().save_session_callback.get() {
tracing::debug!("call save_session_callback");
if let Err(err) = save_session_callback(self.client.clone()) {
error!("when saving session after refresh: {err}");
}
}
#[cfg(feature = "e2e-encryption")]
if let Some(mut lock) = cross_process_lock {
lock.save_in_memory_and_db(&tokens_clone).await?;
}
tracing::debug!("broadcast session changed");
_ = self.client.auth_ctx().session_change_sender.send(SessionChange::TokensRefreshed);
Ok(())
}
#[instrument(skip_all)]
pub async fn refresh_access_token(&self) -> Result<(), RefreshTokenError> {
macro_rules! fail {
($lock:expr, $err:expr) => {
let error = $err;
*$lock = Err(error.clone());
return Err(error);
};
}
let client = &self.client;
let refresh_status_lock = client.auth_ctx().refresh_token_lock.clone().try_lock_owned();
let Ok(mut refresh_status_guard) = refresh_status_lock else {
debug!("another refresh is happening, waiting for result.");
let res = client.auth_ctx().refresh_token_lock.lock().await.clone();
debug!("other refresh is a {}", if res.is_ok() { "success" } else { "failure " });
return res;
};
debug!("no other refresh happening in background, starting.");
#[cfg(feature = "e2e-encryption")]
let cross_process_guard =
if let Some(manager) = self.ctx().cross_process_token_refresh_manager.get() {
let mut cross_process_guard = match manager
.spin_lock()
.await
.map_err(|err| RefreshTokenError::OAuth(Arc::new(err.into())))
{
Ok(guard) => guard,
Err(err) => {
warn!("couldn't acquire cross-process lock (timeout)");
fail!(refresh_status_guard, err);
}
};
if cross_process_guard.hash_mismatch {
Box::pin(self.handle_session_hash_mismatch(&mut cross_process_guard))
.await
.map_err(|err| RefreshTokenError::OAuth(Arc::new(err.into())))?;
tracing::info!("other process handled refresh for us, assuming success");
*refresh_status_guard = Ok(());
return Ok(());
}
Some(cross_process_guard)
} else {
None
};
let Some(session_tokens) = self.client.session_tokens() else {
warn!("invalid state: missing session tokens");
fail!(refresh_status_guard, RefreshTokenError::RefreshTokenRequired);
};
let Some(refresh_token) = session_tokens.refresh_token else {
warn!("invalid state: missing session tokens");
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))
);
};
let this = self.clone();
spawn(async move {
match this
.refresh_access_token_inner(
refresh_token,
server_metadata.token_endpoint,
client_id,
#[cfg(feature = "e2e-encryption")]
cross_process_guard,
)
.await
{
Ok(()) => {
debug!("success refreshing a token");
*refresh_status_guard = Ok(());
Ok(())
}
Err(err) => {
let err = RefreshTokenError::OAuth(Arc::new(err));
warn!("error refreshing an OAuth 2.0 token: {err}");
fail!(refresh_status_guard, err);
}
}
})
.await
.expect("joining")
}
pub async fn logout(&self) -> Result<(), OAuthError> {
let client_id = self.client_id().ok_or(OAuthError::NotAuthenticated)?.clone();
let server_metadata = self.server_metadata().await?;
let revocation_url = RevocationUrl::from_url(server_metadata.revocation_endpoint);
let tokens = self.client.session_tokens().ok_or(OAuthError::NotAuthenticated)?;
OAuthClient::new(client_id)
.set_revocation_url(revocation_url)
.revoke_token(StandardRevocableToken::AccessToken(AccessToken::new(
tokens.access_token,
)))
.map_err(OAuthTokenRevocationError::Url)?
.request_async(self.http_client())
.await
.map_err(OAuthTokenRevocationError::Revoke)?;
#[cfg(feature = "e2e-encryption")]
if let Some(manager) = self.ctx().cross_process_token_refresh_manager.get() {
manager.on_logout().await?;
}
Ok(())
}
}
#[cfg(feature = "e2e-encryption")]
#[derive(Debug)]
pub struct LoginWithQrCodeBuilder<'a> {
client: &'a Client,
registration_data: Option<&'a ClientRegistrationData>,
}
#[cfg(feature = "e2e-encryption")]
impl<'a> LoginWithQrCodeBuilder<'a> {
pub fn scan(self, data: &'a QrCodeData) -> LoginWithQrCode<'a> {
LoginWithQrCode::new(self.client, data, self.registration_data)
}
pub fn generate(self) -> LoginWithGeneratedQrCode<'a> {
LoginWithGeneratedQrCode::new(self.client, self.registration_data)
}
}
#[cfg(feature = "e2e-encryption")]
#[derive(Debug)]
pub struct GrantLoginWithQrCodeBuilder<'a> {
client: &'a Client,
device_creation_timeout: Duration,
}
#[cfg(feature = "e2e-encryption")]
impl<'a> GrantLoginWithQrCodeBuilder<'a> {
fn new(client: &'a Client) -> Self {
Self { client, device_creation_timeout: Duration::from_secs(10) }
}
pub fn device_creation_timeout(mut self, device_creation_timeout: Duration) -> Self {
self.device_creation_timeout = device_creation_timeout;
self
}
pub fn scan(self, data: &'a QrCodeData) -> GrantLoginWithScannedQrCode<'a> {
GrantLoginWithScannedQrCode::new(self.client, data, self.device_creation_timeout)
}
pub fn generate(self) -> GrantLoginWithGeneratedQrCode<'a> {
GrantLoginWithGeneratedQrCode::new(self.client, self.device_creation_timeout)
}
}
#[derive(Debug, Clone)]
pub struct OAuthSession {
pub client_id: ClientId,
pub user: UserSession,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserSession {
#[serde(flatten)]
pub meta: SessionMeta,
#[serde(flatten)]
pub tokens: SessionTokens,
}
#[derive(Debug)]
struct AuthorizationValidationData {
server_metadata: AuthorizationServerMetadata,
device_id: OwnedDeviceId,
redirect_uri: RedirectUrl,
pkce_verifier: PkceCodeVerifier,
}
#[derive(Debug, Clone)]
enum AuthorizationResponse {
Success(AuthorizationCode),
Error(AuthorizationError),
}
impl AuthorizationResponse {
fn parse_url_or_query(url_or_query: &UrlOrQuery) -> Result<Self, RedirectUriQueryParseError> {
let query = url_or_query.query().ok_or(RedirectUriQueryParseError::MissingQuery)?;
Self::parse_query(query)
}
fn parse_query(query: &str) -> Result<Self, RedirectUriQueryParseError> {
if let Ok(code) = serde_html_form::from_str(query) {
return Ok(AuthorizationResponse::Success(code));
}
if let Ok(error) = serde_html_form::from_str(query) {
return Ok(AuthorizationResponse::Error(error));
}
Err(RedirectUriQueryParseError::UnknownFormat)
}
}
#[derive(Debug, Clone, Deserialize)]
struct AuthorizationCode {
code: String,
state: CsrfToken,
}
#[derive(Debug, Clone, Deserialize)]
struct AuthorizationError {
#[serde(flatten)]
error: StandardErrorResponse<error::AuthorizationCodeErrorResponseType>,
state: CsrfToken,
}
fn hash_str(x: &str) -> impl fmt::LowerHex {
sha2::Sha256::new().chain_update(x).finalize()
}
#[derive(Debug, Clone)]
pub struct ClientRegistrationData {
pub metadata: Raw<ClientMetadata>,
pub static_registrations: Option<HashMap<Url, ClientId>>,
}
impl ClientRegistrationData {
pub fn new(metadata: Raw<ClientMetadata>) -> Self {
Self { metadata, static_registrations: None }
}
}
impl From<Raw<ClientMetadata>> for ClientRegistrationData {
fn from(value: Raw<ClientMetadata>) -> Self {
Self::new(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UrlOrQuery {
Url(Url),
Query(String),
}
impl UrlOrQuery {
pub fn query(&self) -> Option<&str> {
match self {
Self::Url(url) => url.query(),
Self::Query(query) => Some(query),
}
}
}
impl From<Url> for UrlOrQuery {
fn from(value: Url) -> Self {
Self::Url(value)
}
}