matrix_sdk_ffi/
authentication.rs

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