mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-14 19:16:02 -04:00
ffi: Remove the AuthenticationService
This commit is contained in:
@@ -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<String>,
|
||||
user_agent: Option<String>,
|
||||
client: AsyncRwLock<Option<Client>>,
|
||||
homeserver_details: StdRwLock<Option<Arc<HomeserverLoginDetails>>>,
|
||||
oidc_configuration: Option<OidcConfiguration>,
|
||||
custom_sliding_sync_proxy: Option<String>,
|
||||
cross_process_refresh_lock_id: Option<String>,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
additional_root_certificates: Vec<CertificateBytes>,
|
||||
proxy: Option<String>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
supports_oidc_login: bool,
|
||||
supports_password_login: bool,
|
||||
pub(crate) url: String,
|
||||
pub(crate) sliding_sync_proxy: Option<String>,
|
||||
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<String>,
|
||||
user_agent: Option<String>,
|
||||
additional_root_certificates: Vec<Vec<u8>>,
|
||||
proxy: Option<String>,
|
||||
oidc_configuration: Option<OidcConfiguration>,
|
||||
custom_sliding_sync_proxy: Option<String>,
|
||||
session_delegate: Option<Box<dyn ClientSessionDelegate>>,
|
||||
cross_process_refresh_lock_id: Option<String>,
|
||||
) -> Arc<Self> {
|
||||
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<Arc<HomeserverLoginDetails>> {
|
||||
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<String>,
|
||||
device_id: Option<String>,
|
||||
) -> Result<Arc<Client>, 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<Arc<OidcAuthorizationData>, 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<OidcAuthorizationData>,
|
||||
callback_url: String,
|
||||
) -> Result<Arc<Client>, 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<Arc<ClientBuilder>, 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<VerifiedClientMetadata> for &OidcConfiguration {
|
||||
type Error = AuthenticationError;
|
||||
|
||||
|
||||
@@ -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<HomeserverLoginDetails> {
|
||||
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<Arc<OidcAuthorizationData>, 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::<HashMap<_, _>>();
|
||||
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<OidcAuthorizationData>) {
|
||||
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<OidcAuthorizationData>,
|
||||
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<MediaSource>,
|
||||
@@ -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<Arc<OidcAuthorizationData>, 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::<HashMap<_, _>>();
|
||||
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<OidcAuthorizationData>) {
|
||||
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<OidcAuthorizationData>,
|
||||
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,
|
||||
|
||||
@@ -487,15 +487,6 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub(crate) fn set_session_delegate_inner(
|
||||
self: Arc<Self>,
|
||||
session_delegate: Arc<dyn ClientSessionDelegate>,
|
||||
) -> Arc<Self> {
|
||||
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<Self>) -> Result<Client, ClientBuildError> {
|
||||
let builder = unwrap_or_clone_arc(self);
|
||||
let mut inner_builder = builder.inner;
|
||||
|
||||
Reference in New Issue
Block a user