use std::{
collections::HashMap,
fmt::{self, Debug},
sync::Arc,
};
use matrix_sdk::{
oidc::{
registrations::OidcRegistrationsError,
types::{
iana::oauth::OAuthClientAuthenticationMethod,
oidc::ApplicationType,
registration::{ClientMetadata, Localized, VerifiedClientMetadata},
requests::GrantType,
},
OidcError as SdkOidcError,
},
Error,
};
use url::Url;
use crate::client::{Client, OidcPrompt, SlidingSyncVersion};
#[derive(uniffi::Object)]
pub struct HomeserverLoginDetails {
pub(crate) url: String,
pub(crate) sliding_sync_version: SlidingSyncVersion,
pub(crate) supports_oidc_login: bool,
pub(crate) supported_oidc_prompts: Vec<OidcPrompt>,
pub(crate) supports_password_login: bool,
}
#[matrix_sdk_ffi_macros::export]
impl HomeserverLoginDetails {
pub fn url(&self) -> String {
self.url.clone()
}
pub fn sliding_sync_version(&self) -> SlidingSyncVersion {
self.sliding_sync_version.clone()
}
pub fn supports_oidc_login(&self) -> bool {
self.supports_oidc_login
}
pub fn supported_oidc_prompts(&self) -> Vec<OidcPrompt> {
self.supported_oidc_prompts.clone()
}
pub fn supports_password_login(&self) -> bool {
self.supports_password_login
}
}
#[derive(uniffi::Object)]
pub struct SsoHandler {
pub(crate) client: Arc<Client>,
pub(crate) url: String,
}
#[matrix_sdk_ffi_macros::export]
impl SsoHandler {
pub fn url(&self) -> String {
self.url.clone()
}
pub async fn finish(&self, callback_url: String) -> Result<(), SsoError> {
let auth = self.client.inner.matrix_auth();
let url = Url::parse(&callback_url).map_err(|_| SsoError::CallbackUrlInvalid)?;
let builder =
auth.login_with_sso_callback(url).map_err(|_| SsoError::CallbackUrlInvalid)?;
builder.await.map_err(|_| SsoError::LoginWithTokenFailed)?;
Ok(())
}
}
impl Debug for SsoHandler {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
fmt.debug_struct("SsoHandler").field("url", &self.url).finish_non_exhaustive()
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum SsoError {
#[error("The supplied callback URL used to complete SSO is invalid.")]
CallbackUrlInvalid,
#[error("Logging in with the token from the supplied callback URL failed.")]
LoginWithTokenFailed,
#[error("An error occurred: {message}")]
Generic { message: String },
}
#[derive(uniffi::Record)]
pub struct OidcConfiguration {
pub client_name: Option<String>,
pub redirect_uri: String,
pub client_uri: Option<String>,
pub logo_uri: Option<String>,
pub tos_uri: Option<String>,
pub policy_uri: Option<String>,
pub contacts: Option<Vec<String>>,
pub static_registrations: HashMap<String, String>,
pub dynamic_registrations_file: String,
}
impl TryInto<VerifiedClientMetadata> for &OidcConfiguration {
type Error = OidcError;
fn try_into(self) -> Result<VerifiedClientMetadata, Self::Error> {
let redirect_uri =
Url::parse(&self.redirect_uri).map_err(|_| OidcError::CallbackUrlInvalid)?;
let client_name = self.client_name.as_ref().map(|n| Localized::new(n.to_owned(), []));
let client_uri = self.client_uri.localized_url()?;
let logo_uri = self.logo_uri.localized_url()?;
let policy_uri = self.policy_uri.localized_url()?;
let tos_uri = self.tos_uri.localized_url()?;
let contacts = self.contacts.clone();
ClientMetadata {
application_type: Some(ApplicationType::Native),
redirect_uris: Some(vec![redirect_uri]),
grant_types: Some(vec![
GrantType::RefreshToken,
GrantType::AuthorizationCode,
GrantType::DeviceCode,
]),
token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None),
client_name,
contacts,
client_uri,
logo_uri,
policy_uri,
tos_uri,
..Default::default()
}
.validate()
.map_err(|_| OidcError::MetadataInvalid)
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum OidcError {
#[error(
"The homeserver doesn't provide an authentication issuer in its well-known configuration."
)]
NotSupported,
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
MetadataInvalid,
#[error("Failed to use the supplied registrations file path.")]
RegistrationsPathInvalid,
#[error("The supplied callback URL used to complete OIDC is invalid.")]
CallbackUrlInvalid,
#[error("The OIDC login was cancelled by the user.")]
Cancelled,
#[error("An error occurred: {message}")]
Generic { message: String },
}
impl From<SdkOidcError> for OidcError {
fn from(e: SdkOidcError) -> OidcError {
match e {
SdkOidcError::MissingAuthenticationIssuer => OidcError::NotSupported,
SdkOidcError::MissingRedirectUri => OidcError::MetadataInvalid,
SdkOidcError::InvalidCallbackUrl => OidcError::CallbackUrlInvalid,
SdkOidcError::InvalidState => OidcError::CallbackUrlInvalid,
SdkOidcError::CancelledAuthorization => OidcError::Cancelled,
_ => OidcError::Generic { message: e.to_string() },
}
}
}
impl From<OidcRegistrationsError> for OidcError {
fn from(e: OidcRegistrationsError) -> OidcError {
match e {
OidcRegistrationsError::InvalidFilePath => OidcError::RegistrationsPathInvalid,
_ => OidcError::Generic { message: e.to_string() },
}
}
}
impl From<Error> for OidcError {
fn from(e: Error) -> OidcError {
match e {
Error::Oidc(e) => e.into(),
_ => OidcError::Generic { message: e.to_string() },
}
}
}
trait OptionExt {
fn localized_url(&self) -> Result<Option<Localized<Url>>, OidcError>;
}
impl OptionExt for Option<String> {
fn localized_url(&self) -> Result<Option<Localized<Url>>, OidcError> {
self.as_deref()
.map(|uri| -> Result<Localized<Url>, OidcError> {
Ok(Localized::new(Url::parse(uri).map_err(|_| OidcError::MetadataInvalid)?, []))
})
.transpose()
}
}