diff --git a/bindings/matrix-sdk-ffi/src/authentication_service.rs b/bindings/matrix-sdk-ffi/src/authentication_service.rs index e57ee1506..6594e5a37 100644 --- a/bindings/matrix-sdk-ffi/src/authentication_service.rs +++ b/bindings/matrix-sdk-ffi/src/authentication_service.rs @@ -1,10 +1,6 @@ -use std::{ - collections::HashMap, - sync::{Arc, RwLock as StdRwLock}, -}; +use std::collections::HashMap; use matrix_sdk::{ - encryption::BackupDownloadStrategy, oidc::{ registrations::OidcRegistrationsError, types::{ @@ -13,49 +9,18 @@ use matrix_sdk::{ registration::{ClientMetadata, Localized, VerifiedClientMetadata}, requests::GrantType, }, - OidcAuthorizationData, OidcError, + OidcError, }, ClientBuildError as MatrixClientBuildError, HttpError, RumaApiError, }; use ruma::api::error::{DeserializationError, FromHttpResponseError}; -use tokio::sync::RwLock as AsyncRwLock; use url::Url; -use zeroize::Zeroize; -use super::{client::Client, client_builder::ClientBuilder}; -use crate::{ - client::ClientSessionDelegate, - client_builder::{CertificateBytes, ClientBuildError}, - error::ClientError, -}; - -#[derive(uniffi::Object)] -pub struct AuthenticationService { - session_path: String, - passphrase: Option, - user_agent: Option, - client: AsyncRwLock>, - homeserver_details: StdRwLock>>, - oidc_configuration: Option, - custom_sliding_sync_proxy: Option, - cross_process_refresh_lock_id: Option, - session_delegate: Option>, - additional_root_certificates: Vec, - proxy: Option, -} - -impl Drop for AuthenticationService { - fn drop(&mut self) { - self.passphrase.zeroize(); - } -} +use crate::client_builder::ClientBuildError; #[derive(Debug, thiserror::Error, uniffi::Error)] #[uniffi(flat_error)] pub enum AuthenticationError { - #[error("A successful call to configure_homeserver must be made first.")] - ClientMissing, - #[error("The supplied server name is invalid.")] InvalidServerName, #[error(transparent)] @@ -67,9 +32,6 @@ pub enum AuthenticationError { #[error("The homeserver doesn't provide a trusted sliding sync proxy in its well-known configuration.")] SlidingSyncNotAvailable, - #[error("Login was successful but is missing a valid Session to configure the file store.")] - SessionMissing, - #[error( "The homeserver doesn't provide an authentication issuer in its well-known configuration." )] @@ -173,10 +135,10 @@ pub struct OidcConfiguration { #[derive(uniffi::Object)] pub struct HomeserverLoginDetails { - url: String, - sliding_sync_proxy: Option, - supports_oidc_login: bool, - supports_password_login: bool, + pub(crate) url: String, + pub(crate) sliding_sync_proxy: Option, + pub(crate) supports_oidc_login: bool, + pub(crate) supports_password_login: bool, } #[uniffi::export] @@ -203,219 +165,6 @@ impl HomeserverLoginDetails { } } -#[uniffi::export(async_runtime = "tokio")] -impl AuthenticationService { - /// Creates a new service to authenticate a user with. - /// - /// # Arguments - /// - /// * `session_path` - A path to the directory where the session data will - /// be stored. A new directory **must** be given for each subsequent - /// session as the database isn't designed to be shared. - /// - /// * `passphrase` - An optional passphrase to use to encrypt the session - /// data. - /// - /// * `user_agent` - An optional user agent to use when making requests. - /// - /// * `additional_root_certificates` - Additional root certificates to trust - /// when making requests when built with rustls. - /// - /// * `proxy` - An optional HTTP(S) proxy URL to use when making requests. - /// - /// * `oidc_configuration` - Configuration data about the app to use during - /// OIDC authentication. This is required if OIDC authentication is to be - /// used. - /// - /// * `custom_sliding_sync_proxy` - An optional sliding sync proxy URL that - /// will override the proxy discovered from the homeserver's well-known. - /// - /// * `session_delegate` - A delegate that will handle token refresh etc. - /// when the cross-process lock is configured. - /// - /// * `cross_process_refresh_lock_id` - A process ID to use for - /// cross-process token refresh locks. - #[uniffi::constructor] - // TODO: This has too many arguments, even clippy agrees. Many of these methods are the same as - // for the `ClientBuilder`. We should let people pass in a `ClientBuilder` and possibly convert - // this to a builder pattern as well. - #[allow(clippy::too_many_arguments)] - pub fn new( - session_path: String, - passphrase: Option, - user_agent: Option, - additional_root_certificates: Vec>, - proxy: Option, - oidc_configuration: Option, - custom_sliding_sync_proxy: Option, - session_delegate: Option>, - cross_process_refresh_lock_id: Option, - ) -> Arc { - Arc::new(AuthenticationService { - session_path, - passphrase, - user_agent, - client: AsyncRwLock::new(None), - homeserver_details: StdRwLock::new(None), - oidc_configuration, - custom_sliding_sync_proxy, - session_delegate: session_delegate.map(Into::into), - cross_process_refresh_lock_id, - additional_root_certificates, - proxy, - }) - } - - pub fn homeserver_details(&self) -> Option> { - self.homeserver_details.read().unwrap().clone() - } - - /// Updates the service to authenticate with the homeserver for the - /// specified address. - pub async fn configure_homeserver( - &self, - server_name_or_homeserver_url: String, - ) -> Result<(), AuthenticationError> { - let builder = - self.new_client_builder()?.server_name_or_homeserver_url(server_name_or_homeserver_url); - - let client = builder.build_inner().await?; - - // Compute homeserver login details. - let details = { - let supports_oidc_login = - client.inner.oidc().fetch_authentication_issuer().await.is_ok(); - let supports_password_login = - client.supports_password_login().await.ok().unwrap_or(false); - let sliding_sync_proxy = - client.sliding_sync_proxy().map(|proxy_url| proxy_url.to_string()); - - HomeserverLoginDetails { - url: client.homeserver(), - sliding_sync_proxy, - supports_oidc_login, - supports_password_login, - } - }; - - *self.client.write().await = Some(client); - *self.homeserver_details.write().unwrap() = Some(Arc::new(details)); - - Ok(()) - } - - /// Performs a password login using the current homeserver. - pub async fn login( - &self, - username: String, - password: String, - initial_device_name: Option, - device_id: Option, - ) -> Result, AuthenticationError> { - let client_guard = self.client.read().await; - let Some(client) = client_guard.as_ref() else { - return Err(AuthenticationError::ClientMissing); - }; - - client.login(username, password, initial_device_name, device_id).await.map_err( - |e| match e { - ClientError::Generic { msg } => AuthenticationError::Generic { message: msg }, - }, - )?; - - drop(client_guard); - - // Now that the client is logged in we can take ownership away from the service - // to ensure there aren't two clients at any point later. - let Some(client) = self.client.write().await.take() else { - return Err(AuthenticationError::ClientMissing); - }; - - Ok(Arc::new(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 async fn url_for_oidc_login( - &self, - ) -> Result, AuthenticationError> { - let client_guard = self.client.read().await; - let Some(client) = client_guard.as_ref() else { - return Err(AuthenticationError::ClientMissing); - }; - - let Some(oidc_configuration) = &self.oidc_configuration else { - return Err(AuthenticationError::OidcMetadataMissing); - }; - - client.url_for_oidc_login(oidc_configuration).await - } - - /// Completes the OIDC login process. - pub async fn login_with_oidc_callback( - &self, - authentication_data: Arc, - callback_url: String, - ) -> Result, AuthenticationError> { - let client_guard = self.client.read().await; - let Some(client) = client_guard.as_ref() else { - return Err(AuthenticationError::ClientMissing); - }; - - client.login_with_oidc_callback(authentication_data, callback_url).await?; - - drop(client_guard); - - // Now that the client is logged in we can take ownership away from the service - // to ensure there aren't two clients at any point later. - let Some(client) = self.client.write().await.take() else { - return Err(AuthenticationError::ClientMissing); - }; - - Ok(Arc::new(client)) - } -} - -impl AuthenticationService { - /// Create a new client builder that is pre-configured with the parameters - /// passed to the service along with some other sensible defaults - fn new_client_builder(&self) -> Result, AuthenticationError> { - let mut builder = ClientBuilder::new() - .session_path(self.session_path.clone()) - .passphrase(self.passphrase.clone()) - .requires_sliding_sync() - .sliding_sync_proxy(self.custom_sliding_sync_proxy.clone()) - .auto_enable_cross_signing(true) - .backup_download_strategy(BackupDownloadStrategy::AfterDecryptionFailure) - .auto_enable_backups(true); - - if let Some(user_agent) = self.user_agent.clone() { - builder = builder.user_agent(user_agent); - } - - if let Some(proxy) = &self.proxy { - builder = builder.proxy(proxy.to_owned()) - } - - builder = builder.add_root_certificates(self.additional_root_certificates.clone()); - - if let Some(id) = &self.cross_process_refresh_lock_id { - let Some(ref session_delegate) = self.session_delegate else { - return Err(AuthenticationError::OidcError { - message: "cross-process refresh lock requires session delegate".to_owned(), - }); - }; - builder = builder - .enable_cross_process_refresh_lock_inner(id.clone(), session_delegate.clone()); - } else if let Some(ref session_delegate) = self.session_delegate { - builder = builder.set_session_delegate_inner(session_delegate.clone()); - } - - Ok(builder) - } -} - impl TryInto for &OidcConfiguration { type Error = AuthenticationError; diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 384f25c6f..e8c23b749 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -60,7 +60,7 @@ use url::Url; use super::{room::Room, session_verification::SessionVerificationController, RUNTIME}; use crate::{ - authentication_service::{AuthenticationError, OidcConfiguration}, + authentication_service::{AuthenticationError, HomeserverLoginDetails, OidcConfiguration}, client, encryption::Encryption, notification::NotificationClient, @@ -254,6 +254,20 @@ impl Client { #[uniffi::export(async_runtime = "tokio")] impl Client { + /// Information about login options for the client's homeserver. + pub async fn homeserver_login_details(&self) -> Arc { + let supports_oidc_login = self.inner.oidc().fetch_authentication_issuer().await.is_ok(); + let supports_password_login = self.supports_password_login().await.ok().unwrap_or(false); + let sliding_sync_proxy = self.sliding_sync_proxy().map(|proxy_url| proxy_url.to_string()); + + Arc::new(HomeserverLoginDetails { + url: self.homeserver(), + sliding_sync_proxy, + supports_oidc_login, + supports_password_login, + }) + } + /// Login using a username and password. pub async fn login( &self, @@ -273,6 +287,77 @@ impl Client { Ok(()) } + /// 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. If a failure occurs and a callback isn't available, make sure + /// to call `abort_oidc_login` to inform the client of this. + pub async fn url_for_oidc_login( + &self, + oidc_configuration: &OidcConfiguration, + ) -> Result, AuthenticationError> { + let oidc_metadata: VerifiedClientMetadata = oidc_configuration.try_into()?; + let registrations_file = Path::new(&oidc_configuration.dynamic_registrations_file); + let static_registrations = oidc_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::>(); + let registrations = OidcRegistrations::new( + registrations_file, + oidc_metadata.clone(), + static_registrations, + )?; + + let data = + self.inner.oidc().url_for_oidc_login(oidc_metadata, registrations).await.map_err( + // TODO: Introduce an OidcError in the FFI with a From implementation. + |e| match e { + OidcError::MissingAuthenticationIssuer => AuthenticationError::OidcNotSupported, + OidcError::MissingRedirectUri => AuthenticationError::OidcMetadataInvalid, + _ => AuthenticationError::OidcError { message: e.to_string() }, + }, + )?; + + Ok(Arc::new(data)) + } + + /// Aborts an existing OIDC login operation that might have been cancelled, + /// failed etc. + pub async fn abort_oidc_login(&self, authorization_data: Arc) { + self.inner.oidc().abort_authorization(&authorization_data.state).await; + } + + /// Completes the OIDC login process. + pub async fn login_with_oidc_callback( + &self, + authorization_data: Arc, + callback_url: String, + ) -> Result<(), AuthenticationError> { + let url = Url::parse(&callback_url).or(Err(AuthenticationError::OidcCallbackUrlInvalid))?; + + self.inner.oidc().login_with_oidc_callback(&authorization_data, url).await.map_err( + // TODO: Introduce an OidcError in the FFI with a From implementation. + |e| match e { + Error::Oidc(OidcError::InvalidCallbackUrl) => { + AuthenticationError::OidcCallbackUrlInvalid + } + Error::Oidc(OidcError::InvalidState) => AuthenticationError::OidcCallbackUrlInvalid, + Error::Oidc(OidcError::CancelledAuthorization) => { + AuthenticationError::OidcCancelled + } + _ => AuthenticationError::OidcError { message: e.to_string() }, + }, + )?; + + Ok(()) + } + pub async fn get_media_file( &self, media_source: Arc, @@ -353,75 +438,6 @@ 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_metadata: VerifiedClientMetadata = oidc_configuration.try_into()?; - let registrations_file = Path::new(&oidc_configuration.dynamic_registrations_file); - let static_registrations = oidc_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::>(); - let registrations = OidcRegistrations::new( - registrations_file, - oidc_metadata.clone(), - static_registrations, - )?; - - let data = - self.inner.oidc().url_for_oidc_login(oidc_metadata, registrations).await.map_err( - // TODO: Introduce an OidcError in the FFI with a From implementation. - |e| match e { - OidcError::MissingAuthenticationIssuer => AuthenticationError::OidcNotSupported, - OidcError::MissingRedirectUri => AuthenticationError::OidcMetadataInvalid, - _ => AuthenticationError::OidcError { message: e.to_string() }, - }, - )?; - - Ok(Arc::new(data)) - } - - #[allow(dead_code)] // Will be exposed when AuthenticationService is removed. - pub(crate) async fn abort_oidc_login(&self, authorization_data: Arc) { - self.inner.oidc().abort_authorization(&authorization_data.state).await; - } - - /// Completes the OIDC login process. - pub(crate) async fn login_with_oidc_callback( - &self, - authorization_data: Arc, - callback_url: String, - ) -> Result<(), AuthenticationError> { - let url = Url::parse(&callback_url).or(Err(AuthenticationError::OidcCallbackUrlInvalid))?; - - self.inner.oidc().login_with_oidc_callback(&authorization_data, url).await.map_err( - // TODO: Introduce an OidcError in the FFI with a From implementation. - |e| match e { - Error::Oidc(OidcError::InvalidCallbackUrl) => { - AuthenticationError::OidcCallbackUrlInvalid - } - Error::Oidc(OidcError::InvalidState) => AuthenticationError::OidcCallbackUrlInvalid, - Error::Oidc(OidcError::CancelledAuthorization) => { - AuthenticationError::OidcCancelled - } - _ => AuthenticationError::OidcError { message: e.to_string() }, - }, - )?; - - Ok(()) - } - /// Restores the client from an `AuthSession`. pub(crate) async fn restore_session_inner( &self, diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index cee0f8fb2..d7c0d3484 100644 --- a/bindings/matrix-sdk-ffi/src/client_builder.rs +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -487,15 +487,6 @@ impl ClientBuilder { Arc::new(builder) } - pub(crate) fn set_session_delegate_inner( - self: Arc, - session_delegate: Arc, - ) -> Arc { - let mut builder = unwrap_or_clone_arc(self); - builder.session_delegate = Some(session_delegate); - Arc::new(builder) - } - pub(crate) async fn build_inner(self: Arc) -> Result { let builder = unwrap_or_clone_arc(self); let mut inner_builder = builder.inner;