matrix_sdk_ffi/
authentication.rs

1use std::{
2    collections::HashMap,
3    fmt::{self, Debug},
4    sync::Arc,
5};
6
7use matrix_sdk::{
8    authentication::oidc::{
9        registrations::OidcRegistrationsError,
10        types::{
11            iana::oauth::OAuthClientAuthenticationMethod,
12            oidc::ApplicationType,
13            registration::{ClientMetadata, Localized, VerifiedClientMetadata},
14            requests::GrantType,
15        },
16        OidcError as SdkOidcError,
17    },
18    Error,
19};
20use url::Url;
21
22use crate::client::{Client, OidcPrompt, SlidingSyncVersion};
23
24#[derive(uniffi::Object)]
25pub struct HomeserverLoginDetails {
26    pub(crate) url: String,
27    pub(crate) sliding_sync_version: SlidingSyncVersion,
28    pub(crate) supports_oidc_login: bool,
29    pub(crate) supported_oidc_prompts: Vec<OidcPrompt>,
30    pub(crate) supports_password_login: bool,
31}
32
33#[matrix_sdk_ffi_macros::export]
34impl HomeserverLoginDetails {
35    /// The URL of the currently configured homeserver.
36    pub fn url(&self) -> String {
37        self.url.clone()
38    }
39
40    /// The sliding sync version.
41    pub fn sliding_sync_version(&self) -> SlidingSyncVersion {
42        self.sliding_sync_version.clone()
43    }
44
45    /// Whether the current homeserver supports login using OIDC.
46    pub fn supports_oidc_login(&self) -> bool {
47        self.supports_oidc_login
48    }
49
50    /// The prompts advertised by the authentication issuer for use in the login
51    /// URL.
52    pub fn supported_oidc_prompts(&self) -> Vec<OidcPrompt> {
53        self.supported_oidc_prompts.clone()
54    }
55
56    /// Whether the current homeserver supports the password login flow.
57    pub fn supports_password_login(&self) -> bool {
58        self.supports_password_login
59    }
60}
61
62/// An object encapsulating the SSO login flow
63#[derive(uniffi::Object)]
64pub struct SsoHandler {
65    /// The wrapped Client.
66    pub(crate) client: Arc<Client>,
67
68    /// The underlying URL for authentication.
69    pub(crate) url: String,
70}
71
72#[matrix_sdk_ffi_macros::export]
73impl SsoHandler {
74    /// Returns the URL for starting SSO authentication. The URL should be
75    /// opened in a web view. Once the web view succeeds, call `finish` with
76    /// the callback URL.
77    pub fn url(&self) -> String {
78        self.url.clone()
79    }
80
81    /// Completes the SSO login process.
82    pub async fn finish(&self, callback_url: String) -> Result<(), SsoError> {
83        let auth = self.client.inner.matrix_auth();
84        let url = Url::parse(&callback_url).map_err(|_| SsoError::CallbackUrlInvalid)?;
85        let builder =
86            auth.login_with_sso_callback(url).map_err(|_| SsoError::CallbackUrlInvalid)?;
87        builder.await.map_err(|_| SsoError::LoginWithTokenFailed)?;
88        Ok(())
89    }
90}
91
92impl Debug for SsoHandler {
93    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
94        fmt.debug_struct("SsoHandler").field("url", &self.url).finish_non_exhaustive()
95    }
96}
97
98#[derive(Debug, thiserror::Error, uniffi::Error)]
99#[uniffi(flat_error)]
100pub enum SsoError {
101    #[error("The supplied callback URL used to complete SSO is invalid.")]
102    CallbackUrlInvalid,
103    #[error("Logging in with the token from the supplied callback URL failed.")]
104    LoginWithTokenFailed,
105
106    #[error("An error occurred: {message}")]
107    Generic { message: String },
108}
109
110/// The configuration to use when authenticating with OIDC.
111#[derive(uniffi::Record)]
112pub struct OidcConfiguration {
113    /// The name of the client that will be shown during OIDC authentication.
114    pub client_name: Option<String>,
115    /// The redirect URI that will be used when OIDC authentication is
116    /// successful.
117    pub redirect_uri: String,
118    /// A URI that contains information about the client.
119    pub client_uri: Option<String>,
120    /// A URI that contains the client's logo.
121    pub logo_uri: Option<String>,
122    /// A URI that contains the client's terms of service.
123    pub tos_uri: Option<String>,
124    /// A URI that contains the client's privacy policy.
125    pub policy_uri: Option<String>,
126    /// An array of e-mail addresses of people responsible for this client.
127    pub contacts: Option<Vec<String>>,
128
129    /// Pre-configured registrations for use with issuers that don't support
130    /// dynamic client registration.
131    pub static_registrations: HashMap<String, String>,
132
133    /// A file path where any dynamic registrations should be stored.
134    ///
135    /// Suggested value: `{base_path}/oidc/registrations.json`
136    pub dynamic_registrations_file: String,
137}
138
139impl TryInto<VerifiedClientMetadata> for &OidcConfiguration {
140    type Error = OidcError;
141
142    fn try_into(self) -> Result<VerifiedClientMetadata, Self::Error> {
143        let redirect_uri =
144            Url::parse(&self.redirect_uri).map_err(|_| OidcError::CallbackUrlInvalid)?;
145        let client_name = self.client_name.as_ref().map(|n| Localized::new(n.to_owned(), []));
146        let client_uri = self.client_uri.localized_url()?;
147        let logo_uri = self.logo_uri.localized_url()?;
148        let policy_uri = self.policy_uri.localized_url()?;
149        let tos_uri = self.tos_uri.localized_url()?;
150        let contacts = self.contacts.clone();
151
152        ClientMetadata {
153            application_type: Some(ApplicationType::Native),
154            redirect_uris: Some(vec![redirect_uri]),
155            grant_types: Some(vec![
156                GrantType::RefreshToken,
157                GrantType::AuthorizationCode,
158                GrantType::DeviceCode,
159            ]),
160            // A native client shouldn't use authentication as the credentials could be intercepted.
161            token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None),
162            // The server should display the following fields when getting the user's consent.
163            client_name,
164            contacts,
165            client_uri,
166            logo_uri,
167            policy_uri,
168            tos_uri,
169            ..Default::default()
170        }
171        .validate()
172        .map_err(|_| OidcError::MetadataInvalid)
173    }
174}
175
176#[derive(Debug, thiserror::Error, uniffi::Error)]
177#[uniffi(flat_error)]
178pub enum OidcError {
179    #[error(
180        "The homeserver doesn't provide an authentication issuer in its well-known configuration."
181    )]
182    NotSupported,
183    #[error("Unable to use OIDC as the supplied client metadata is invalid.")]
184    MetadataInvalid,
185    #[error("Failed to use the supplied registrations file path.")]
186    RegistrationsPathInvalid,
187    #[error("The supplied callback URL used to complete OIDC is invalid.")]
188    CallbackUrlInvalid,
189    #[error("The OIDC login was cancelled by the user.")]
190    Cancelled,
191
192    #[error("An error occurred: {message}")]
193    Generic { message: String },
194}
195
196impl From<SdkOidcError> for OidcError {
197    fn from(e: SdkOidcError) -> OidcError {
198        match e {
199            SdkOidcError::Discovery(error) if error.is_not_supported() => OidcError::NotSupported,
200            SdkOidcError::MissingRedirectUri => OidcError::MetadataInvalid,
201            SdkOidcError::InvalidCallbackUrl => OidcError::CallbackUrlInvalid,
202            SdkOidcError::InvalidState => OidcError::CallbackUrlInvalid,
203            SdkOidcError::CancelledAuthorization => OidcError::Cancelled,
204            _ => OidcError::Generic { message: e.to_string() },
205        }
206    }
207}
208
209impl From<OidcRegistrationsError> for OidcError {
210    fn from(e: OidcRegistrationsError) -> OidcError {
211        match e {
212            OidcRegistrationsError::InvalidFilePath => OidcError::RegistrationsPathInvalid,
213            _ => OidcError::Generic { message: e.to_string() },
214        }
215    }
216}
217
218impl From<Error> for OidcError {
219    fn from(e: Error) -> OidcError {
220        match e {
221            Error::Oidc(e) => e.into(),
222            _ => OidcError::Generic { message: e.to_string() },
223        }
224    }
225}
226
227/* Helpers */
228
229trait OptionExt {
230    /// Convenience method to convert a string to a URL and returns it as a
231    /// Localized URL. No localization is actually performed.
232    fn localized_url(&self) -> Result<Option<Localized<Url>>, OidcError>;
233}
234
235impl OptionExt for Option<String> {
236    fn localized_url(&self) -> Result<Option<Localized<Url>>, OidcError> {
237        self.as_deref()
238            .map(|uri| -> Result<Localized<Url>, OidcError> {
239                Ok(Localized::new(Url::parse(uri).map_err(|_| OidcError::MetadataInvalid)?, []))
240            })
241            .transpose()
242    }
243}