atagen

matrix-rust-sdk

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