From 735ae1ce75ec46a94808cbdb560439c6c4aa91ca Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 17 Jun 2024 18:08:03 +0100 Subject: [PATCH] ffi: Move OIDC logic into Client. --- .../src/authentication_service.rs | 202 +---------------- bindings/matrix-sdk-ffi/src/client.rs | 204 +++++++++++++++++- 2 files changed, 210 insertions(+), 196 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/authentication_service.rs b/bindings/matrix-sdk-ffi/src/authentication_service.rs index abf5a667b..3820ad69d 100644 --- a/bindings/matrix-sdk-ffi/src/authentication_service.rs +++ b/bindings/matrix-sdk-ffi/src/authentication_service.rs @@ -1,24 +1,20 @@ use std::{ collections::HashMap, - path::Path, sync::{Arc, RwLock as StdRwLock}, }; use matrix_sdk::{ encryption::BackupDownloadStrategy, oidc::{ - registrations::{ClientId, OidcRegistrations, OidcRegistrationsError}, + registrations::OidcRegistrationsError, types::{ - client_credentials::ClientCredentials, - errors::ClientErrorCode::AccessDenied, iana::oauth::OAuthClientAuthenticationMethod, oidc::ApplicationType, registration::{ClientMetadata, Localized, VerifiedClientMetadata}, - requests::{GrantType, Prompt}, + requests::GrantType, }, - AuthorizationResponse, Oidc, OidcError, + OidcError, }, - reqwest::StatusCode, ClientBuildError as MatrixClientBuildError, HttpError, RumaApiError, }; use ruma::api::error::{DeserializationError, FromHttpResponseError}; @@ -175,10 +171,10 @@ pub struct OidcConfiguration { #[derive(uniffi::Object)] pub struct OidcAuthenticationData { /// The underlying URL for authentication. - url: Url, + pub(crate) url: Url, /// A unique identifier for the request, used to ensure the response /// originated from the authentication issuer. - state: String, + pub(crate) state: String, } #[uniffi::export] @@ -370,37 +366,11 @@ impl AuthenticationService { return Err(AuthenticationError::ClientMissing); }; - let oidc = client.inner.oidc(); - - let issuer = match oidc.fetch_authentication_issuer().await { - Ok(issuer) => issuer, - Err(error) => { - if error - .as_client_api_error() - .is_some_and(|err| err.status_code == StatusCode::NOT_FOUND) - { - return Err(AuthenticationError::OidcNotSupported); - } else { - return Err(AuthenticationError::ServerUnreachable(error)); - } - } - }; - let Some(oidc_configuration) = &self.oidc_configuration else { return Err(AuthenticationError::OidcMetadataMissing); }; - let redirect_url = Url::parse(&oidc_configuration.redirect_uri) - .map_err(|_e| AuthenticationError::OidcMetadataInvalid)?; - - self.configure_oidc(&oidc, issuer, oidc_configuration).await?; - - let mut data_builder = oidc.login(redirect_url, None)?; - // TODO: Add a check for the Consent prompt when MAS is updated. - data_builder = data_builder.prompt(vec![Prompt::Consent]); - let data = data_builder.build().await?; - - Ok(Arc::new(OidcAuthenticationData { url: data.url, state: data.state })) + client.url_for_oidc_login(oidc_configuration).await } /// Completes the OIDC login process. @@ -414,36 +384,7 @@ impl AuthenticationService { return Err(AuthenticationError::ClientMissing); }; - let oidc = client.inner.oidc(); - - let url = - Url::parse(&callback_url).map_err(|_| AuthenticationError::OidcCallbackUrlInvalid)?; - - let response = AuthorizationResponse::parse_uri(&url) - .map_err(|_| AuthenticationError::OidcCallbackUrlInvalid)?; - - let code = match response { - AuthorizationResponse::Success(code) => code, - AuthorizationResponse::Error(err) => { - if err.error.error == AccessDenied { - // The user cancelled the login in the web view. - return Err(AuthenticationError::OidcCancelled); - } - return Err(AuthenticationError::OidcError { - message: err.error.error.to_string(), - }); - } - }; - - if code.state != authentication_data.state { - return Err(AuthenticationError::OidcCallbackUrlInvalid); - }; - - oidc.finish_authorization(code).await?; - - oidc.finish_login() - .await - .map_err(|e| AuthenticationError::OidcError { message: e.to_string() })?; + client.login_with_oidc_callback(authentication_data, callback_url).await?; drop(client_guard); @@ -493,135 +434,6 @@ impl AuthenticationService { Ok(builder) } - - /// Handle any necessary configuration in order for login via OIDC to - /// succeed. This includes performing dynamic client registration against - /// the homeserver's issuer or restoring a previous registration if one has - /// been stored. - async fn configure_oidc( - &self, - oidc: &Oidc, - issuer: String, - configuration: &OidcConfiguration, - ) -> Result<(), AuthenticationError> { - if oidc.client_credentials().is_some() { - tracing::info!("OIDC is already configured."); - return Ok(()); - }; - - let oidc_metadata: VerifiedClientMetadata = configuration.try_into()?; - let registrations_file = Path::new(&configuration.dynamic_registrations_file); - - if self.load_client_registration( - oidc, - issuer.clone(), - oidc_metadata.clone(), - registrations_file, - ) { - tracing::info!("OIDC configuration loaded from disk."); - return Ok(()); - } - - tracing::info!("Registering this client for OIDC."); - let registration_response = - oidc.register_client(&issuer, oidc_metadata.clone(), None).await?; - - // The format of the credentials changes according to the client metadata that - // was sent. Public clients only get a client ID. - let credentials = - ClientCredentials::None { client_id: registration_response.client_id.clone() }; - oidc.restore_registered_client(issuer, oidc_metadata, credentials); - - tracing::info!("Persisting OIDC registration data."); - self.store_client_registration(oidc, registrations_file)?; - - Ok(()) - } - - /// Stores the current OIDC dynamic client registration so it can be re-used - /// if we ever log in via the same issuer again. - fn store_client_registration( - &self, - oidc: &Oidc, - registrations_file: &Path, - ) -> Result<(), AuthenticationError> { - let issuer = Url::parse(oidc.issuer().ok_or(AuthenticationError::OidcNotSupported)?) - .map_err(|_| AuthenticationError::OidcError { - message: String::from("Failed to parse issuer URL."), - })?; - let client_id = oidc - .client_credentials() - .ok_or(AuthenticationError::OidcError { - message: String::from("Missing client registration."), - })? - .client_id() - .to_owned(); - - let metadata = oidc.client_metadata().ok_or(AuthenticationError::OidcError { - message: String::from("Missing client metadata."), - })?; - - let registrations = OidcRegistrations::new( - registrations_file, - metadata.clone(), - self.oidc_static_registrations(), - )?; - registrations.set_and_write_client_id(ClientId(client_id), issuer)?; - - Ok(()) - } - - /// Attempts to load an existing OIDC dynamic client registration for the - /// currently configured issuer. - fn load_client_registration( - &self, - oidc: &Oidc, - issuer: String, - oidc_metadata: VerifiedClientMetadata, - registrations_file: &Path, - ) -> bool { - let Ok(issuer_url) = Url::parse(&issuer) else { - tracing::error!("Failed to parse {issuer:?}"); - return false; - }; - let Some(registrations) = OidcRegistrations::new( - registrations_file, - oidc_metadata.clone(), - self.oidc_static_registrations(), - ) - .ok() else { - return false; - }; - let Some(client_id) = registrations.client_id(&issuer_url) else { - return false; - }; - - oidc.restore_registered_client( - issuer, - oidc_metadata, - ClientCredentials::None { client_id: client_id.0 }, - ); - - true - } - - fn oidc_static_registrations(&self) -> HashMap { - let registrations = self - .oidc_configuration - .as_ref() - .map(|c| c.static_registrations.clone()) - .unwrap_or_default(); - registrations - .iter() - .filter_map(|(issuer, client_id)| { - let Ok(issuer) = Url::parse(issuer) else { - tracing::error!("Failed to parse {:?}", issuer); - return None; - }; - Some((issuer, ClientId(client_id.clone()))) - }) - .collect() - } } impl TryInto for &OidcConfiguration { diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 872260993..3559075b6 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -1,6 +1,7 @@ use std::{ collections::HashMap, mem::ManuallyDrop, + path::Path, sync::{Arc, RwLock}, }; @@ -8,15 +9,19 @@ use anyhow::{anyhow, Context as _}; use matrix_sdk::{ media::{MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequest, MediaThumbnailSize}, oidc::{ + registrations::{ClientId, OidcRegistrations}, requests::account_management::AccountManagementActionFull, types::{ client_credentials::ClientCredentials, + errors::ClientErrorCode::AccessDenied, registration::{ ClientMetadata, ClientMetadataVerificationError, VerifiedClientMetadata, }, + requests::Prompt, }, - OidcSession, + AuthorizationResponse, Oidc, OidcSession, }, + reqwest::StatusCode, ruma::{ api::client::{ media::get_content_thumbnail::v3::Method, @@ -58,6 +63,7 @@ use url::Url; use super::{room::Room, session_verification::SessionVerificationController, RUNTIME}; use crate::{ + authentication_service::{AuthenticationError, OidcAuthenticationData, OidcConfiguration}, client, encryption::Encryption, notification::NotificationClient, @@ -350,6 +356,82 @@ impl Client { } impl Client { + /// Requests the URL needed for login in a web view using OIDC. Once the web + /// view has succeeded, call `login_with_oidc_callback` with the callback it + /// returns. + pub(crate) async fn url_for_oidc_login( + &self, + oidc_configuration: &OidcConfiguration, + ) -> Result, AuthenticationError> { + let oidc = self.inner.oidc(); + + let issuer = match oidc.fetch_authentication_issuer().await { + Ok(issuer) => issuer, + Err(error) => { + if error + .as_client_api_error() + .is_some_and(|err| err.status_code == StatusCode::NOT_FOUND) + { + return Err(AuthenticationError::OidcNotSupported); + } else { + return Err(AuthenticationError::ServerUnreachable(error)); + } + } + }; + + let redirect_url = Url::parse(&oidc_configuration.redirect_uri) + .map_err(|_e| AuthenticationError::OidcMetadataInvalid)?; + + self.configure_oidc(&oidc, issuer, oidc_configuration).await?; + + let mut data_builder = oidc.login(redirect_url, None)?; + // TODO: Add a check for the Consent prompt when MAS is updated. + data_builder = data_builder.prompt(vec![Prompt::Consent]); + let data = data_builder.build().await?; + + Ok(Arc::new(OidcAuthenticationData { url: data.url, state: data.state })) + } + + /// Completes the OIDC login process. + pub(crate) async fn login_with_oidc_callback( + &self, + authentication_data: Arc, + callback_url: String, + ) -> Result<(), AuthenticationError> { + let oidc = self.inner.oidc(); + + let url = + Url::parse(&callback_url).map_err(|_| AuthenticationError::OidcCallbackUrlInvalid)?; + + let response = AuthorizationResponse::parse_uri(&url) + .map_err(|_| AuthenticationError::OidcCallbackUrlInvalid)?; + + let code = match response { + AuthorizationResponse::Success(code) => code, + AuthorizationResponse::Error(err) => { + if err.error.error == AccessDenied { + // The user cancelled the login in the web view. + return Err(AuthenticationError::OidcCancelled); + } + return Err(AuthenticationError::OidcError { + message: err.error.error.to_string(), + }); + } + }; + + if code.state != authentication_data.state { + return Err(AuthenticationError::OidcCallbackUrlInvalid); + }; + + oidc.finish_authorization(code).await?; + + oidc.finish_login() + .await + .map_err(|e| AuthenticationError::OidcError { message: e.to_string() })?; + + Ok(()) + } + /// Restores the client from an `AuthSession`. pub(crate) async fn restore_session_inner( &self, @@ -375,6 +457,126 @@ impl Client { .any(|login_type| matches!(login_type, get_login_types::v3::LoginType::Password(_))); Ok(supports_password) } + + /// Handle any necessary configuration in order for login via OIDC to + /// succeed. This includes performing dynamic client registration against + /// the homeserver's issuer or restoring a previous registration if one has + /// been stored. + async fn configure_oidc( + &self, + oidc: &Oidc, + issuer: String, + configuration: &OidcConfiguration, + ) -> Result<(), AuthenticationError> { + if oidc.client_credentials().is_some() { + tracing::info!("OIDC is already configured."); + return Ok(()); + }; + + let oidc_metadata: VerifiedClientMetadata = configuration.try_into()?; + let registrations_file = Path::new(&configuration.dynamic_registrations_file); + let static_registrations = configuration + .static_registrations + .iter() + .filter_map(|(issuer, client_id)| { + let Ok(issuer) = Url::parse(issuer) else { + tracing::error!("Failed to parse {:?}", issuer); + return None; + }; + Some((issuer, ClientId(client_id.clone()))) + }) + .collect::>(); + + if self.load_client_registration( + oidc, + issuer.clone(), + oidc_metadata.clone(), + registrations_file, + static_registrations.clone(), + ) { + tracing::info!("OIDC configuration loaded from disk."); + return Ok(()); + } + + tracing::info!("Registering this client for OIDC."); + let registration_response = + oidc.register_client(&issuer, oidc_metadata.clone(), None).await?; + + // The format of the credentials changes according to the client metadata that + // was sent. Public clients only get a client ID. + let credentials = + ClientCredentials::None { client_id: registration_response.client_id.clone() }; + oidc.restore_registered_client(issuer, oidc_metadata, credentials); + + tracing::info!("Persisting OIDC registration data."); + self.store_client_registration(oidc, registrations_file, static_registrations)?; + + Ok(()) + } + + /// Stores the current OIDC dynamic client registration so it can be re-used + /// if we ever log in via the same issuer again. + fn store_client_registration( + &self, + oidc: &Oidc, + registrations_file: &Path, + static_registrations: HashMap, + ) -> Result<(), AuthenticationError> { + let issuer = Url::parse(oidc.issuer().ok_or(AuthenticationError::OidcNotSupported)?) + .map_err(|_| AuthenticationError::OidcError { + message: String::from("Failed to parse issuer URL."), + })?; + let client_id = oidc + .client_credentials() + .ok_or(AuthenticationError::OidcError { + message: String::from("Missing client registration."), + })? + .client_id() + .to_owned(); + + let metadata = oidc.client_metadata().ok_or(AuthenticationError::OidcError { + message: String::from("Missing client metadata."), + })?; + + let registrations = + OidcRegistrations::new(registrations_file, metadata.clone(), static_registrations)?; + registrations.set_and_write_client_id(ClientId(client_id), issuer)?; + + Ok(()) + } + + /// Attempts to load an existing OIDC dynamic client registration for the + /// currently configured issuer. + fn load_client_registration( + &self, + oidc: &Oidc, + issuer: String, + oidc_metadata: VerifiedClientMetadata, + registrations_file: &Path, + static_registrations: HashMap, + ) -> bool { + let Ok(issuer_url) = Url::parse(&issuer) else { + tracing::error!("Failed to parse {issuer:?}"); + return false; + }; + let Some(registrations) = + OidcRegistrations::new(registrations_file, oidc_metadata.clone(), static_registrations) + .ok() + else { + return false; + }; + let Some(client_id) = registrations.client_id(&issuer_url) else { + return false; + }; + + oidc.restore_registered_client( + issuer, + oidc_metadata, + ClientCredentials::None { client_id: client_id.0 }, + ); + + true + } } #[uniffi::export(async_runtime = "tokio")]