matrix_sdk_ffi/
authentication.rs1use std::{
16 collections::HashMap,
17 fmt::{self, Debug},
18 sync::Arc,
19};
20
21use matrix_sdk::{
22 Error,
23 authentication::oauth::{
24 ClientId, ClientRegistrationData, OAuthError as SdkOAuthError,
25 error::OAuthAuthorizationCodeError,
26 registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
27 },
28};
29use ruma::serde::Raw;
30use url::Url;
31
32use crate::client::{Client, OAuthPrompt, SlidingSyncVersion};
33
34#[derive(uniffi::Object)]
35pub 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]
45impl HomeserverLoginDetails {
46 pub fn url(&self) -> String {
48 self.url.clone()
49 }
50
51 pub fn sliding_sync_version(&self) -> SlidingSyncVersion {
53 self.sliding_sync_version.clone()
54 }
55
56 pub fn supports_oauth_login(&self) -> bool {
58 self.supports_oauth_login
59 }
60
61 pub fn supported_oauth_prompts(&self) -> Vec<OAuthPrompt> {
64 self.supported_oauth_prompts.clone()
65 }
66
67 pub fn supports_sso_login(&self) -> bool {
69 self.supports_sso_login
70 }
71
72 pub fn supports_password_login(&self) -> bool {
74 self.supports_password_login
75 }
76}
77
78#[derive(uniffi::Object)]
80pub struct SsoHandler {
81 pub(crate) client: Arc<Client>,
83
84 pub(crate) url: String,
86}
87
88#[matrix_sdk_ffi_macros::export]
89impl SsoHandler {
90 pub fn url(&self) -> String {
94 self.url.clone()
95 }
96
97 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
109impl 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)]
117pub 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#[derive(uniffi::Record)]
129pub struct OAuthConfiguration {
130 pub client_name: Option<String>,
132 pub redirect_uri: String,
135 pub client_uri: String,
137 pub logo_uri: Option<String>,
139 pub tos_uri: Option<String>,
141 pub policy_uri: Option<String>,
143
144 pub static_registrations: HashMap<String, String>,
150}
151
152impl 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 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)]
211pub 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
227impl 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
243impl 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
252trait OptionExt {
255 fn localized_url(&self) -> Result<Option<Localized<Url>>, OAuthError>;
258}
259
260impl 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
266trait StrExt {
267 fn localized_url(&self) -> Result<Localized<Url>, OAuthError>;
270}
271
272impl 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}