matrix_sdk_ffi/
authentication.rs1use std::{
2 collections::HashMap,
3 fmt::{self, Debug},
4 sync::Arc,
5};
6
7use matrix_sdk::{
8 authentication::oidc::{
9 registrations::OidcRegistrationsError,
10 types::{
11 iana::oauth::OAuthClientAuthenticationMethod,
12 oidc::ApplicationType,
13 registration::{ClientMetadata, Localized, VerifiedClientMetadata},
14 requests::GrantType,
15 },
16 OidcError as SdkOidcError,
17 },
18 Error,
19};
20use url::Url;
21
22use crate::client::{Client, OidcPrompt, SlidingSyncVersion};
23
24#[derive(uniffi::Object)]
25pub struct HomeserverLoginDetails {
26 pub(crate) url: String,
27 pub(crate) sliding_sync_version: SlidingSyncVersion,
28 pub(crate) supports_oidc_login: bool,
29 pub(crate) supported_oidc_prompts: Vec<OidcPrompt>,
30 pub(crate) supports_password_login: bool,
31}
32
33#[matrix_sdk_ffi_macros::export]
34impl HomeserverLoginDetails {
35 pub fn url(&self) -> String {
37 self.url.clone()
38 }
39
40 pub fn sliding_sync_version(&self) -> SlidingSyncVersion {
42 self.sliding_sync_version.clone()
43 }
44
45 pub fn supports_oidc_login(&self) -> bool {
47 self.supports_oidc_login
48 }
49
50 pub fn supported_oidc_prompts(&self) -> Vec<OidcPrompt> {
53 self.supported_oidc_prompts.clone()
54 }
55
56 pub fn supports_password_login(&self) -> bool {
58 self.supports_password_login
59 }
60}
61
62#[derive(uniffi::Object)]
64pub struct SsoHandler {
65 pub(crate) client: Arc<Client>,
67
68 pub(crate) url: String,
70}
71
72#[matrix_sdk_ffi_macros::export]
73impl SsoHandler {
74 pub fn url(&self) -> String {
78 self.url.clone()
79 }
80
81 pub async fn finish(&self, callback_url: String) -> Result<(), SsoError> {
83 let auth = self.client.inner.matrix_auth();
84 let url = Url::parse(&callback_url).map_err(|_| SsoError::CallbackUrlInvalid)?;
85 let builder =
86 auth.login_with_sso_callback(url).map_err(|_| SsoError::CallbackUrlInvalid)?;
87 builder.await.map_err(|_| SsoError::LoginWithTokenFailed)?;
88 Ok(())
89 }
90}
91
92impl Debug for SsoHandler {
93 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
94 fmt.debug_struct("SsoHandler").field("url", &self.url).finish_non_exhaustive()
95 }
96}
97
98#[derive(Debug, thiserror::Error, uniffi::Error)]
99#[uniffi(flat_error)]
100pub enum SsoError {
101 #[error("The supplied callback URL used to complete SSO is invalid.")]
102 CallbackUrlInvalid,
103 #[error("Logging in with the token from the supplied callback URL failed.")]
104 LoginWithTokenFailed,
105
106 #[error("An error occurred: {message}")]
107 Generic { message: String },
108}
109
110#[derive(uniffi::Record)]
112pub struct OidcConfiguration {
113 pub client_name: Option<String>,
115 pub redirect_uri: String,
118 pub client_uri: Option<String>,
120 pub logo_uri: Option<String>,
122 pub tos_uri: Option<String>,
124 pub policy_uri: Option<String>,
126 pub contacts: Option<Vec<String>>,
128
129 pub static_registrations: HashMap<String, String>,
132
133 pub dynamic_registrations_file: String,
137}
138
139impl TryInto<VerifiedClientMetadata> for &OidcConfiguration {
140 type Error = OidcError;
141
142 fn try_into(self) -> Result<VerifiedClientMetadata, Self::Error> {
143 let redirect_uri =
144 Url::parse(&self.redirect_uri).map_err(|_| OidcError::CallbackUrlInvalid)?;
145 let client_name = self.client_name.as_ref().map(|n| Localized::new(n.to_owned(), []));
146 let client_uri = self.client_uri.localized_url()?;
147 let logo_uri = self.logo_uri.localized_url()?;
148 let policy_uri = self.policy_uri.localized_url()?;
149 let tos_uri = self.tos_uri.localized_url()?;
150 let contacts = self.contacts.clone();
151
152 ClientMetadata {
153 application_type: Some(ApplicationType::Native),
154 redirect_uris: Some(vec![redirect_uri]),
155 grant_types: Some(vec![
156 GrantType::RefreshToken,
157 GrantType::AuthorizationCode,
158 GrantType::DeviceCode,
159 ]),
160 token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None),
162 client_name,
164 contacts,
165 client_uri,
166 logo_uri,
167 policy_uri,
168 tos_uri,
169 ..Default::default()
170 }
171 .validate()
172 .map_err(|_| OidcError::MetadataInvalid)
173 }
174}
175
176#[derive(Debug, thiserror::Error, uniffi::Error)]
177#[uniffi(flat_error)]
178pub enum OidcError {
179 #[error(
180 "The homeserver doesn't provide an authentication issuer in its well-known configuration."
181 )]
182 NotSupported,
183 #[error("Unable to use OIDC as the supplied client metadata is invalid.")]
184 MetadataInvalid,
185 #[error("Failed to use the supplied registrations file path.")]
186 RegistrationsPathInvalid,
187 #[error("The supplied callback URL used to complete OIDC is invalid.")]
188 CallbackUrlInvalid,
189 #[error("The OIDC login was cancelled by the user.")]
190 Cancelled,
191
192 #[error("An error occurred: {message}")]
193 Generic { message: String },
194}
195
196impl From<SdkOidcError> for OidcError {
197 fn from(e: SdkOidcError) -> OidcError {
198 match e {
199 SdkOidcError::Discovery(error) if error.is_not_supported() => OidcError::NotSupported,
200 SdkOidcError::MissingRedirectUri => OidcError::MetadataInvalid,
201 SdkOidcError::InvalidCallbackUrl => OidcError::CallbackUrlInvalid,
202 SdkOidcError::InvalidState => OidcError::CallbackUrlInvalid,
203 SdkOidcError::CancelledAuthorization => OidcError::Cancelled,
204 _ => OidcError::Generic { message: e.to_string() },
205 }
206 }
207}
208
209impl From<OidcRegistrationsError> for OidcError {
210 fn from(e: OidcRegistrationsError) -> OidcError {
211 match e {
212 OidcRegistrationsError::InvalidFilePath => OidcError::RegistrationsPathInvalid,
213 _ => OidcError::Generic { message: e.to_string() },
214 }
215 }
216}
217
218impl From<Error> for OidcError {
219 fn from(e: Error) -> OidcError {
220 match e {
221 Error::Oidc(e) => e.into(),
222 _ => OidcError::Generic { message: e.to_string() },
223 }
224 }
225}
226
227trait OptionExt {
230 fn localized_url(&self) -> Result<Option<Localized<Url>>, OidcError>;
233}
234
235impl OptionExt for Option<String> {
236 fn localized_url(&self) -> Result<Option<Localized<Url>>, OidcError> {
237 self.as_deref()
238 .map(|uri| -> Result<Localized<Url>, OidcError> {
239 Ok(Localized::new(Url::parse(uri).map_err(|_| OidcError::MetadataInvalid)?, []))
240 })
241 .transpose()
242 }
243}