matrix_sdk_ffi/
authentication.rs

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