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 pub fn url(&self) -> String {
34 self.url.clone()
35 }
36
37 pub fn sliding_sync_version(&self) -> SlidingSyncVersion {
39 self.sliding_sync_version.clone()
40 }
41
42 pub fn supports_oidc_login(&self) -> bool {
44 self.supports_oidc_login
45 }
46
47 pub fn supported_oidc_prompts(&self) -> Vec<OidcPrompt> {
50 self.supported_oidc_prompts.clone()
51 }
52
53 pub fn supports_password_login(&self) -> bool {
55 self.supports_password_login
56 }
57}
58
59#[derive(uniffi::Object)]
61pub struct SsoHandler {
62 pub(crate) client: Arc<Client>,
64
65 pub(crate) url: String,
67}
68
69#[matrix_sdk_ffi_macros::export]
70impl SsoHandler {
71 pub fn url(&self) -> String {
75 self.url.clone()
76 }
77
78 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#[derive(uniffi::Record)]
109pub struct OidcConfiguration {
110 pub client_name: Option<String>,
112 pub redirect_uri: String,
115 pub client_uri: String,
117 pub logo_uri: Option<String>,
119 pub tos_uri: Option<String>,
121 pub policy_uri: Option<String>,
123 pub contacts: Option<Vec<String>>,
125
126 pub static_registrations: HashMap<String, String>,
129
130 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 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
251trait OptionExt {
254 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 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}