matrix_sdk_ffi/
authentication.rs1use 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 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 supports_sso_login(&self) -> bool {
49 self.supports_sso_login
50 }
51
52 pub fn supported_oidc_prompts(&self) -> Vec<OidcPrompt> {
55 self.supported_oidc_prompts.clone()
56 }
57
58 pub fn supports_password_login(&self) -> bool {
60 self.supports_password_login
61 }
62}
63
64#[derive(uniffi::Object)]
66pub struct SsoHandler {
67 pub(crate) client: Arc<Client>,
69
70 pub(crate) url: String,
72}
73
74#[matrix_sdk_ffi_macros::export]
75impl SsoHandler {
76 pub fn url(&self) -> String {
80 self.url.clone()
81 }
82
83 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#[derive(uniffi::Record)]
114pub struct OidcConfiguration {
115 pub client_name: Option<String>,
117 pub redirect_uri: String,
120 pub client_uri: String,
122 pub logo_uri: Option<String>,
124 pub tos_uri: Option<String>,
126 pub policy_uri: Option<String>,
128
129 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 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
237trait OptionExt {
240 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 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}