matrix_sdk/authentication/oauth/mod.rs
1// Copyright 2022 Kévin Commaille
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 the specific language governing permissions and
13// limitations under the License.
14
15//! High-level OAuth 2.0 API.
16//!
17//! The OAuth 2.0 interactions with the Matrix API are currently a
18//! work-in-progress and are defined by [MSC3861] and its sub-proposals. And
19//! more documentation is available at [areweoidcyet.com].
20//!
21//! This authentication API is available with [`Client::oauth()`].
22//!
23//! # Homeserver support
24//!
25//! After building the client, you can check that the homeserver supports
26//! logging in via OAuth 2.0 when [`OAuth::server_metadata()`] succeeds.
27//!
28//! # Registration
29//!
30//! Clients must register with the homeserver before being able to interact with
31//! an OAuth 2.0 server.
32//!
33//! The registration consists in providing client metadata to the authorization
34//! server, to declare the interactions that the client supports with the
35//! homeserver. This step is important because the client cannot use a feature
36//! that is not declared during registration. In return, the server assigns an
37//! ID and eventually credentials to the client, which will allow to identify
38//! the client when authorization requests are made.
39//!
40//! Note that only public clients are supported by this API, i.e. clients
41//! without credentials.
42//!
43//! The registration step can be done automatically by providing a
44//! [`ClientRegistrationData`] to the login method.
45//!
46//! If the server supports dynamic registration, registration can be performed
47//! manually by using [`OAuth::register_client()`]. If dynamic registration is
48//! not available, the homeserver should document how to obtain a client ID. The
49//! client ID can then be provided with [`OAuth::restore_registered_client()`].
50//!
51//! # Login
52//!
53//! Currently, two login methods are supported by this API.
54//!
55//! ## Login with the Authorization Code flow
56//!
57//! The use of the Authorization Code flow is defined in [MSC2964] and [RFC
58//! 6749][rfc6749-auth-code].
59//!
60//! This method requires to open a URL in the end-user's browser where
61//! they will be able to log into their account in the server's web UI and grant
62//! access to their Matrix account.
63//!
64//! [`OAuth::login()`] constructs an [`OAuthAuthCodeUrlBuilder`] that can be
65//! configured, and then calling [`OAuthAuthCodeUrlBuilder::build()`] will
66//! provide the URL to present to the user in a web browser.
67//!
68//! After authenticating with the server, the user will be redirected to the
69//! provided redirect URI, with a code in the query that will allow to finish
70//! the login process by calling [`OAuth::finish_login()`].
71//!
72//! If the login needs to be cancelled before its completion,
73//! [`OAuth::abort_login()`] should be called to clean up the local data.
74//!
75//! ## Login by scanning a QR Code
76//!
77//! Logging in via a QR code is defined in [MSC4108]. It uses the Device
78//! authorization flow specified in [RFC 8628].
79//!
80//! This method requires to have another logged-in Matrix device that can
81//! display a QR Code.
82//!
83//! This login method is only available if the `e2e-encryption` cargo feature is
84//! enabled. It is not available on WASM.
85//!
86//! After scanning the QR Code, [`OAuth::login_with_qr_code()`] can be called
87//! with the QR Code's data. Then the different steps of the process need to be
88//! followed with [`LoginWithQrCode::subscribe_to_progress()`].
89//!
90//! A successful login using this method will automatically mark the device as
91//! verified and transfer all end-to-end encryption related secrets, like the
92//! private cross-signing keys and the backup key from the existing device to
93//! the new device.
94//!
95//! # Persisting/restoring a session
96//!
97//! The full session to persist can be obtained with [`OAuth::full_session()`].
98//! The different parts can also be retrieved with [`Client::session_meta()`],
99//! [`Client::session_tokens()`] and [`OAuth::client_id()`].
100//!
101//! To restore a previous session, use [`OAuth::restore_session()`].
102//!
103//! # Refresh tokens
104//!
105//! The use of refresh tokens with OAuth 2.0 servers is more common than in the
106//! Matrix specification. For this reason, it is recommended to configure the
107//! client with [`ClientBuilder::handle_refresh_tokens()`], to handle refreshing
108//! tokens automatically.
109//!
110//! Applications should then listen to session tokens changes after logging in
111//! with [`Client::subscribe_to_session_changes()`] to persist them on every
112//! change. If they are not persisted properly, the end-user will need to login
113//! again.
114//!
115//! # Unknown token error
116//!
117//! A request to the Matrix API can return an [`Error`] with an
118//! [`ErrorKind::UnknownToken`].
119//!
120//! The first step is to try to refresh the token with
121//! [`OAuth::refresh_access_token()`]. This step is done automatically if the
122//! client was built with [`ClientBuilder::handle_refresh_tokens()`].
123//!
124//! If refreshing the access token fails, the next step is to try to request a
125//! new login authorization with [`OAuth::login()`], using the device ID from
126//! the session.
127//!
128//! If this fails again, the client should assume to be logged out, and all
129//! local data should be erased.
130//!
131//! # Account management.
132//!
133//! The server might advertise a URL that allows the user to manage their
134//! account. It can be used to replace most of the Matrix APIs requiring
135//! User-Interactive Authentication.
136//!
137//! The account management URL is available as `account_management_uri` on
138//! [`AuthorizationServerMetadata`]. To build a full account management URL that
139//! includes the action that the user wants to perform, use
140//! [`AuthorizationServerMetadata::account_management_url_with_action()`].
141//!
142//! # Logout
143//!
144//! To log the [`Client`] out of the session, simply call [`OAuth::logout()`].
145//!
146//! # Examples
147//!
148//! Most methods have examples, there is also an example CLI application that
149//! supports all the actions described here, in [`examples/oauth_cli`].
150//!
151//! [MSC3861]: https://github.com/matrix-org/matrix-spec-proposals/pull/3861
152//! [areweoidcyet.com]: https://areweoidcyet.com/
153//! [MSC2964]: https://github.com/matrix-org/matrix-spec-proposals/pull/2964
154//! [rfc6749-auth-code]: https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
155//! [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
156//! [RFC 8628]: https://datatracker.ietf.org/doc/html/rfc8628
157//! [`ClientBuilder::handle_refresh_tokens()`]: crate::ClientBuilder::handle_refresh_tokens()
158//! [`Error`]: ruma::api::error::Error
159//! [`ErrorKind::UnknownToken`]: ruma::api::error::ErrorKind::UnknownToken
160//! [`examples/oauth_cli`]: https://github.com/matrix-org/matrix-rust-sdk/tree/main/examples/oauth_cli
161
162#[cfg(feature = "e2e-encryption")]
163use std::sync::OnceLock;
164#[cfg(feature = "e2e-encryption")]
165use std::time::Duration;
166use std::{borrow::Cow, collections::HashMap, fmt, sync::Arc};
167
168use as_variant::as_variant;
169#[cfg(feature = "e2e-encryption")]
170use error::CrossProcessRefreshLockError;
171use error::{
172 OAuthAuthorizationCodeError, OAuthClientRegistrationError, OAuthDiscoveryError,
173 OAuthTokenRevocationError, RedirectUriQueryParseError,
174};
175#[cfg(feature = "e2e-encryption")]
176use matrix_sdk_base::crypto::types::qr_login::QrCodeData;
177use matrix_sdk_base::{SessionMeta, store::RoomLoadSettings, ttl::TtlValue};
178#[cfg(feature = "e2e-encryption")]
179use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
180use oauth2::{
181 AccessToken, PkceCodeVerifier, RedirectUrl, RefreshToken, RevocationUrl, Scope,
182 StandardErrorResponse, StandardRevocableToken, TokenResponse, TokenUrl,
183 basic::BasicClient as OAuthClient,
184};
185pub use oauth2::{ClientId, CsrfToken};
186use oauth2_reqwest::ReqwestClient;
187use ruma::{
188 DeviceId, OwnedDeviceId,
189 api::client::discovery::get_authorization_server_metadata::{
190 self, v1::AuthorizationServerMetadata,
191 },
192 serde::Raw,
193};
194use serde::{Deserialize, Serialize};
195use sha2::Digest as _;
196use tokio::sync::Mutex;
197use tracing::{debug, error, instrument, trace, warn};
198use url::Url;
199
200mod auth_code_builder;
201#[cfg(feature = "e2e-encryption")]
202mod cross_process;
203pub mod error;
204mod http_client;
205#[cfg(feature = "e2e-encryption")]
206pub mod qrcode;
207pub mod registration;
208#[cfg(all(test, not(target_family = "wasm")))]
209mod tests;
210
211#[cfg(feature = "e2e-encryption")]
212use self::cross_process::{CrossProcessRefreshLockGuard, CrossProcessRefreshManager};
213#[cfg(feature = "e2e-encryption")]
214use self::qrcode::{
215 GrantLoginWithGeneratedQrCode, GrantLoginWithScannedQrCode, LoginWithGeneratedQrCode,
216 LoginWithQrCode,
217};
218pub use self::{
219 auth_code_builder::{OAuthAuthCodeUrlBuilder, OAuthAuthorizationData},
220 error::OAuthError,
221};
222use self::{
223 http_client::OAuthHttpClient,
224 registration::{ClientMetadata, ClientRegistrationResponse, register_client},
225};
226use super::{AuthData, SessionTokens};
227use crate::{
228 Client, RefreshTokenError, Result,
229 client::{SessionChange, caches::CachedValue},
230 executor::spawn,
231 utils::UrlOrQuery,
232};
233
234pub(crate) struct OAuthCtx {
235 /// Lock and state when multiple processes may refresh an OAuth 2.0 session.
236 #[cfg(feature = "e2e-encryption")]
237 cross_process_token_refresh_manager: OnceLock<CrossProcessRefreshManager>,
238
239 /// Deferred cross-process lock initializer.
240 ///
241 /// Note: only required because we're using the crypto store that might not
242 /// be present before reloading a session.
243 #[cfg(feature = "e2e-encryption")]
244 deferred_cross_process_lock_init: Mutex<Option<String>>,
245
246 /// Whether to allow HTTP issuer URLs.
247 insecure_discover: bool,
248}
249
250impl OAuthCtx {
251 pub(crate) fn new(insecure_discover: bool) -> Self {
252 Self {
253 insecure_discover,
254 #[cfg(feature = "e2e-encryption")]
255 cross_process_token_refresh_manager: Default::default(),
256 #[cfg(feature = "e2e-encryption")]
257 deferred_cross_process_lock_init: Default::default(),
258 }
259 }
260}
261
262pub(crate) struct OAuthAuthData {
263 pub(crate) client_id: ClientId,
264 /// The data necessary to validate authorization responses.
265 authorization_data: Mutex<HashMap<CsrfToken, AuthorizationValidationData>>,
266}
267
268#[cfg(not(tarpaulin_include))]
269impl fmt::Debug for OAuthAuthData {
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 f.debug_struct("OAuthAuthData").finish_non_exhaustive()
272 }
273}
274
275/// A high-level authentication API to interact with an OAuth 2.0 authorization
276/// server.
277#[derive(Debug, Clone)]
278pub struct OAuth {
279 /// The underlying Matrix API client.
280 client: Client,
281 /// The HTTP client used for making OAuth 2.0 request.
282 http_client: OAuthHttpClient,
283}
284
285impl OAuth {
286 pub(crate) fn new(client: Client) -> Self {
287 let http_client = OAuthHttpClient {
288 inner: ReqwestClient::from(client.inner.http_client.inner.clone()),
289 #[cfg(test)]
290 insecure_rewrite_https_to_http: false,
291 };
292 Self { client, http_client }
293 }
294
295 /// Rewrite HTTPS requests to use HTTP instead.
296 ///
297 /// This is a workaround to bypass some checks that require an HTTPS URL,
298 /// but we can only mock HTTP URLs.
299 #[cfg(test)]
300 pub(crate) fn insecure_rewrite_https_to_http(mut self) -> Self {
301 self.http_client.insecure_rewrite_https_to_http = true;
302 self
303 }
304
305 fn ctx(&self) -> &OAuthCtx {
306 &self.client.auth_ctx().oauth
307 }
308
309 fn http_client(&self) -> &OAuthHttpClient {
310 &self.http_client
311 }
312
313 /// Enable a cross-process store lock on the state store, to coordinate
314 /// refreshes across different processes.
315 #[cfg(feature = "e2e-encryption")]
316 pub async fn enable_cross_process_refresh_lock(
317 &self,
318 lock_value: String,
319 ) -> Result<(), OAuthError> {
320 // FIXME: it must be deferred only because we're using the crypto store
321 // and it's initialized only in `set_or_reload_session`, not if we use a
322 // dedicated store.
323 let mut lock = self.ctx().deferred_cross_process_lock_init.lock().await;
324 if lock.is_some() {
325 return Err(CrossProcessRefreshLockError::DuplicatedLock.into());
326 }
327 *lock = Some(lock_value);
328
329 Ok(())
330 }
331
332 /// Performs a deferred cross-process refresh-lock, if needs be, after an
333 /// olm machine has been initialized.
334 ///
335 /// Must be called after [`BaseClient::set_or_reload_session`].
336 #[cfg(feature = "e2e-encryption")]
337 async fn deferred_enable_cross_process_refresh_lock(&self) {
338 let deferred_init_lock = self.ctx().deferred_cross_process_lock_init.lock().await;
339
340 // Don't `take()` the value, so that subsequent calls to
341 // `enable_cross_process_refresh_lock` will keep on failing if we've
342 // enabled the lock at least once.
343 let Some(lock_value) = deferred_init_lock.as_ref() else {
344 return;
345 };
346
347 // FIXME: We shouldn't be using the crypto store for that! see also https://github.com/matrix-org/matrix-rust-sdk/issues/2472
348 let olm_machine_lock = self.client.olm_machine().await;
349 let olm_machine =
350 olm_machine_lock.as_ref().expect("there has to be an olm machine, hopefully?");
351 let store = olm_machine.store();
352 let lock = store.create_store_lock(
353 "oidc_session_refresh_lock".to_owned(),
354 CrossProcessLockConfig::multi_process(lock_value.to_owned()),
355 );
356
357 let manager = CrossProcessRefreshManager::new(store.clone(), lock);
358
359 // This method is guarded with the `deferred_cross_process_lock_init`
360 // lock held, so this `set` can't be an error.
361 let _ = self.ctx().cross_process_token_refresh_manager.set(manager);
362 }
363
364 /// The OAuth 2.0 authentication data.
365 ///
366 /// Returns `None` if the client was not registered or if the registration
367 /// was not restored with [`OAuth::restore_registered_client()`] or
368 /// [`OAuth::restore_session()`].
369 fn data(&self) -> Option<&OAuthAuthData> {
370 let data = self.client.auth_ctx().auth_data.get()?;
371 as_variant!(data, AuthData::OAuth)
372 }
373
374 /// Check if the homeserver supports the [MSC4388] variant of the rendezvous
375 /// server.
376 ///
377 /// Returns `Ok(true)` if the rendezvous discovery endpoint returns a 200 OK
378 /// HTTP response, `Ok(false)` if the endpoint returns a 404 NOT_FOUND or
379 /// 403 FORBIDDEN HTTP response, otherwise an error is returned.
380 ///
381 /// [MSC4388]: https://github.com/matrix-org/matrix-spec-proposals/pull/4388
382 #[cfg(feature = "e2e-encryption")]
383 pub async fn msc_4388_rendezvous_server_supported(&self) -> Result<bool, crate::HttpError> {
384 use http::StatusCode;
385 use ruma::api::client::rendezvous::discover_rendezvous;
386
387 match self.client.send(discover_rendezvous::unstable::Request::new()).await {
388 Ok(response) => Ok(response.create_available),
389 Err(e) => {
390 if e.as_client_api_error().is_some_and(|err| {
391 matches!(err.status_code, StatusCode::NOT_FOUND | StatusCode::FORBIDDEN)
392 }) {
393 Ok(false)
394 } else {
395 Err(e)
396 }
397 }
398 }
399 }
400
401 /// Log in this device using a QR code.
402 ///
403 /// # Arguments
404 ///
405 /// - `registration_data` - The data to restore or register the client with
406 /// the server. If this is not provided, an error will occur unless
407 /// [`OAuth::register_client()`] or [`OAuth::restore_registered_client()`]
408 /// was called previously.
409 #[cfg(feature = "e2e-encryption")]
410 pub fn login_with_qr_code<'a>(
411 &'a self,
412 registration_data: Option<&'a ClientRegistrationData>,
413 ) -> LoginWithQrCodeBuilder<'a> {
414 LoginWithQrCodeBuilder::new(&self.client, registration_data)
415 }
416
417 /// Grant login to a new device using a QR code.
418 #[cfg(feature = "e2e-encryption")]
419 pub fn grant_login_with_qr_code<'a>(&'a self) -> GrantLoginWithQrCodeBuilder<'a> {
420 GrantLoginWithQrCodeBuilder::new(&self.client)
421 }
422
423 /// Restore or register the OAuth 2.0 client for the server with the given
424 /// metadata, with the given optional [`ClientRegistrationData`].
425 ///
426 /// If we already have a client ID, this is a noop.
427 ///
428 /// Returns an error if there was a problem using the registration method.
429 async fn use_registration_data(
430 &self,
431 server_metadata: &AuthorizationServerMetadata,
432 data: Option<&ClientRegistrationData>,
433 ) -> std::result::Result<(), OAuthError> {
434 if self.client_id().is_some() {
435 tracing::info!("OAuth 2.0 is already configured.");
436 return Ok(());
437 }
438
439 let Some(data) = data else {
440 return Err(OAuthError::NotRegistered);
441 };
442
443 if let Some(static_registrations) = &data.static_registrations {
444 let client_id = static_registrations
445 .get(&self.client.homeserver())
446 .or_else(|| static_registrations.get(&server_metadata.issuer));
447
448 if let Some(client_id) = client_id {
449 self.restore_registered_client(client_id.clone());
450 return Ok(());
451 }
452 }
453
454 self.register_client_inner(server_metadata, &data.metadata).await?;
455
456 Ok(())
457 }
458
459 /// Get the cached OAuth 2.0 authorization server metadata of the
460 /// homeserver.
461 ///
462 /// This method will cache the metadata for a while. If the cache is not
463 /// populated it will request the server metadata, like a call to
464 /// [`OAuth::server_metadata()`], and cache the response before returning
465 /// it.
466 ///
467 /// The cache can be forced to be refreshed by calling
468 /// [`OAuth::server_metadata()`] instead.
469 ///
470 /// In most cases during the authentication process, it is better to always
471 /// fetch the metadata from the server. This is provided for convenience for
472 /// cases where the client doesn't want to incur the extra time necessary to
473 /// make the request.
474 ///
475 /// Returns an error if a problem occurred when fetching or validating the
476 /// metadata.
477 pub async fn cached_server_metadata(
478 &self,
479 ) -> Result<AuthorizationServerMetadata, OAuthDiscoveryError> {
480 let server_metadata_cache = &self.client.inner.caches.server_metadata;
481
482 if let CachedValue::Cached(metadata) = server_metadata_cache.value() {
483 if metadata.has_expired() {
484 debug!("spawning task to refresh OAuth 2.0 server metadata cache");
485
486 let oauth = self.clone();
487 self.client.task_monitor().spawn_finite_task(
488 "refresh OAuth 2.0 server metadata cache",
489 async move {
490 if let Err(error) = oauth.server_metadata().await {
491 warn!("failed to refresh OAuth 2.0 server metadata cache: {error}");
492 }
493 },
494 );
495 }
496
497 return Ok(metadata.into_data());
498 }
499
500 self.server_metadata().await
501 }
502
503 /// Fetch the OAuth 2.0 authorization server metadata of the homeserver.
504 ///
505 /// This will always request the latest server metadata from the homeserver.
506 ///
507 /// To avoid making a request each time, you can use
508 /// [`OAuth::cached_server_metadata()`].
509 ///
510 /// Returns an error if a problem occurred when fetching or validating the
511 /// metadata.
512 pub async fn server_metadata(
513 &self,
514 ) -> Result<AuthorizationServerMetadata, OAuthDiscoveryError> {
515 let server_metadata_cache = &self.client.inner.caches.server_metadata;
516
517 let mut server_metadata_guard = match server_metadata_cache.refresh_lock.try_lock() {
518 Ok(guard) => guard,
519 Err(_) => {
520 // There is already a refresh in progress, wait for it to
521 // finish.
522 let guard = server_metadata_cache.refresh_lock.lock().await;
523
524 // Reuse the data if the request was successful.
525 if matches!(*guard, Ok(()))
526 && let CachedValue::Cached(value) = server_metadata_cache.value()
527 {
528 return Ok(value.into_data());
529 }
530
531 // The previous request failed, make another request.
532 guard
533 }
534 };
535
536 match self.server_metadata_inner().await {
537 Ok(metadata) => {
538 // Always refresh the cache.
539 self.client.inner.caches.server_metadata.set_value(TtlValue::new(metadata.clone()));
540 *server_metadata_guard = Ok(());
541 Ok(metadata)
542 }
543 Err(error) => {
544 *server_metadata_guard = Err(());
545 Err(error)
546 }
547 }
548 }
549
550 async fn server_metadata_inner(
551 &self,
552 ) -> Result<AuthorizationServerMetadata, OAuthDiscoveryError> {
553 let response =
554 self.client.send(get_authorization_server_metadata::v1::Request::new()).await.map_err(
555 |error| {
556 // If the server doesn't support the endpoint.
557 if error.is_endpoint_not_implemented() {
558 OAuthDiscoveryError::NotSupported
559 } else {
560 error.into()
561 }
562 },
563 )?;
564
565 let metadata = response.metadata.deserialize()?;
566
567 if self.ctx().insecure_discover {
568 metadata.insecure_validate_urls()?;
569 } else {
570 metadata.validate_urls()?;
571 }
572
573 Ok(metadata)
574 }
575
576 /// The OAuth 2.0 unique identifier of this client obtained after
577 /// registration.
578 ///
579 /// Returns `None` if the client was not registered or if the registration
580 /// was not restored with [`OAuth::restore_registered_client()`] or
581 /// [`OAuth::restore_session()`].
582 pub fn client_id(&self) -> Option<&ClientId> {
583 self.data().map(|data| &data.client_id)
584 }
585
586 /// The OAuth 2.0 user session of this client.
587 ///
588 /// Returns `None` if the client was not logged in.
589 pub fn user_session(&self) -> Option<UserSession> {
590 let meta = self.client.session_meta()?.to_owned();
591 let tokens = self.client.session_tokens()?;
592 Some(UserSession { meta, tokens })
593 }
594
595 /// The full OAuth 2.0 session of this client.
596 ///
597 /// Returns `None` if the client was not logged in with the OAuth 2.0 API.
598 pub fn full_session(&self) -> Option<OAuthSession> {
599 let user = self.user_session()?;
600 let data = self.data()?;
601 Some(OAuthSession { client_id: data.client_id.clone(), user })
602 }
603
604 /// Register a client with the OAuth 2.0 server.
605 ///
606 /// This should be called before any authorization request with an
607 /// authorization server that supports dynamic client registration. If the
608 /// client registered with the server manually, it should use
609 /// [`OAuth::restore_registered_client()`].
610 ///
611 /// Note that this method only supports public clients, i.e. clients without
612 /// a secret.
613 ///
614 /// # Arguments
615 ///
616 /// - `client_metadata` - The serialized client metadata to register.
617 ///
618 /// # Panic
619 ///
620 /// Panics if the authentication data was already set.
621 ///
622 /// # Example
623 ///
624 /// ```no_run
625 /// use matrix_sdk::{Client, ServerName};
626 /// # use matrix_sdk::authentication::oauth::ClientId;
627 /// # use matrix_sdk::authentication::oauth::registration::ClientMetadata;
628 /// # use ruma::serde::Raw;
629 /// # let client_metadata = unimplemented!();
630 /// # fn persist_client_registration (_: url::Url, _: &ClientId) {}
631 /// # _ = async {
632 /// let server_name = ServerName::parse("myhomeserver.org")?;
633 /// let client = Client::builder().server_name(&server_name).build().await?;
634 /// let oauth = client.oauth();
635 ///
636 /// if let Err(error) = oauth.server_metadata().await {
637 /// if error.is_not_supported() {
638 /// println!("OAuth 2.0 is not supported");
639 /// }
640 ///
641 /// return Err(error.into());
642 /// }
643 ///
644 /// let response = oauth
645 /// .register_client(&client_metadata)
646 /// .await?;
647 ///
648 /// println!(
649 /// "Registered with client_id: {}",
650 /// response.client_id.as_str()
651 /// );
652 ///
653 /// // The API only supports clients without secrets.
654 /// let client_id = response.client_id;
655 ///
656 /// persist_client_registration(client.homeserver(), &client_id);
657 /// # anyhow::Ok(()) };
658 /// ```
659 pub async fn register_client(
660 &self,
661 client_metadata: &Raw<ClientMetadata>,
662 ) -> Result<ClientRegistrationResponse, OAuthError> {
663 let server_metadata = self.server_metadata().await?;
664 Ok(self.register_client_inner(&server_metadata, client_metadata).await?)
665 }
666
667 async fn register_client_inner(
668 &self,
669 server_metadata: &AuthorizationServerMetadata,
670 client_metadata: &Raw<ClientMetadata>,
671 ) -> Result<ClientRegistrationResponse, OAuthClientRegistrationError> {
672 let registration_endpoint = server_metadata
673 .registration_endpoint
674 .as_ref()
675 .ok_or(OAuthClientRegistrationError::NotSupported)?;
676
677 let registration_response =
678 register_client(self.http_client(), registration_endpoint, client_metadata).await?;
679
680 // The format of the credentials changes according to the client
681 // metadata that was sent. Public clients only get a client ID.
682 self.restore_registered_client(registration_response.client_id.clone());
683
684 Ok(registration_response)
685 }
686
687 /// Set the data of a client that is registered with an OAuth 2.0
688 /// authorization server.
689 ///
690 /// This should be called when logging in with a server that is already
691 /// known by the client.
692 ///
693 /// Note that this method only supports public clients, i.e. clients with no
694 /// credentials.
695 ///
696 /// # Arguments
697 ///
698 /// - `client_id` - The unique identifier to authenticate the client with
699 /// the server, obtained after registration.
700 ///
701 /// # Panic
702 ///
703 /// Panics if authentication data was already set.
704 pub fn restore_registered_client(&self, client_id: ClientId) {
705 let data = OAuthAuthData { client_id, authorization_data: Default::default() };
706
707 self.client
708 .auth_ctx()
709 .auth_data
710 .set(AuthData::OAuth(data))
711 .expect("Client authentication data was already set");
712 }
713
714 /// Restore a previously logged in session.
715 ///
716 /// This can be used to restore the client to a logged in state, including
717 /// loading the sync state and the encryption keys from the store, if one
718 /// was set up.
719 ///
720 /// # Persisting the store
721 ///
722 /// Restoring only reattaches the client to its stored state; it does not
723 /// recreate that state. The same persistent store used during the original
724 /// login (for example via [`ClientBuilder::sqlite_store()`]) must be
725 /// configured on the [`ClientBuilder`] when the session is restored,
726 /// otherwise the encryption keys and room state will not be available. When
727 /// the `e2e-encryption` feature is enabled, restoring on top of an
728 /// in-memory store will leave the client unable to send or receive
729 /// encrypted messages. See the [`persist_session`] example for a full
730 /// walk-through.
731 ///
732 /// [`ClientBuilder`]: crate::ClientBuilder
733 /// [`ClientBuilder::sqlite_store()`]: crate::ClientBuilder::sqlite_store
734 /// [`persist_session`]: https://github.com/matrix-org/matrix-rust-sdk/tree/main/examples/persist_session
735 ///
736 /// # Arguments
737 ///
738 /// - `session` - The session to restore.
739 /// - `room_load_settings` — Specify how many rooms must be restored; use
740 /// `::default()` if you don't know which value to pick.
741 ///
742 /// # Panic
743 ///
744 /// Panics if authentication data was already set.
745 pub async fn restore_session(
746 &self,
747 session: OAuthSession,
748 room_load_settings: RoomLoadSettings,
749 ) -> Result<()> {
750 let OAuthSession { client_id, user: UserSession { meta, tokens } } = session;
751
752 let data = OAuthAuthData { client_id, authorization_data: Default::default() };
753
754 self.client.auth_ctx().set_session_tokens(tokens.clone());
755 self.client
756 .activate(
757 meta,
758 room_load_settings,
759 #[cfg(feature = "e2e-encryption")]
760 None,
761 )
762 .await?;
763 #[cfg(feature = "e2e-encryption")]
764 self.deferred_enable_cross_process_refresh_lock().await;
765
766 self.client
767 .inner
768 .auth_ctx
769 .auth_data
770 .set(AuthData::OAuth(data))
771 .expect("Client authentication data was already set");
772
773 // Initialize the cross-process locking by saving our tokens' hash into
774 // the database, if we've enabled the cross-process lock.
775
776 #[cfg(feature = "e2e-encryption")]
777 if let Some(cross_process_lock) = self.ctx().cross_process_token_refresh_manager.get() {
778 cross_process_lock.restore_session(&tokens).await;
779
780 let mut guard = cross_process_lock
781 .spin_lock()
782 .await
783 .map_err(|err| crate::Error::OAuth(Box::new(err.into())))?;
784
785 // After we got the lock, it's possible that our session doesn't
786 // match the one read from the database, because of a race: another
787 // process has refreshed the tokens while we were waiting for the
788 // lock.
789 //
790 // In that case, if there's a mismatch, we reload the session and
791 // update the hash. Otherwise, we save our hash into the database.
792
793 if guard.hash_mismatch {
794 Box::pin(self.handle_session_hash_mismatch(&mut guard))
795 .await
796 .map_err(|err| crate::Error::OAuth(Box::new(err.into())))?;
797 } else {
798 guard
799 .save_in_memory_and_db(&tokens)
800 .await
801 .map_err(|err| crate::Error::OAuth(Box::new(err.into())))?;
802 // No need to call the save_session_callback here; it was the
803 // source of the session, so it's already in sync with what we
804 // had.
805 }
806 }
807
808 #[cfg(feature = "e2e-encryption")]
809 self.client.encryption().spawn_initialization_task(None).await;
810
811 Ok(())
812 }
813
814 #[cfg(feature = "e2e-encryption")]
815 async fn handle_session_hash_mismatch(
816 &self,
817 guard: &mut CrossProcessRefreshLockGuard,
818 ) -> Result<(), CrossProcessRefreshLockError> {
819 trace!("Handling hash mismatch.");
820
821 let callback = self
822 .client
823 .auth_ctx()
824 .reload_session_callback
825 .get()
826 .ok_or(CrossProcessRefreshLockError::MissingReloadSession)?;
827
828 match callback(self.client.clone()) {
829 Ok(tokens) => {
830 guard.handle_mismatch(&tokens).await?;
831
832 self.client.auth_ctx().set_session_tokens(tokens.clone());
833 // The app's callback acted as authoritative here, so we're not
834 // saving the data back into the app, as that would have no
835 // effect.
836 }
837 Err(err) => {
838 error!("when reloading OAuth 2.0 session tokens from callback: {err}");
839 }
840 }
841
842 Ok(())
843 }
844
845 /// The scopes to request for logging in and the corresponding device ID.
846 fn login_scopes(
847 device_id: Option<OwnedDeviceId>,
848 additional_scopes: Option<Vec<Scope>>,
849 ) -> (Vec<Scope>, OwnedDeviceId) {
850 /// Scope to grand full access to the client-server API.
851 const SCOPE_MATRIX_CLIENT_SERVER_API_FULL_ACCESS: &str = "urn:matrix:client:api:*";
852 /// Prefix of the scope to bind a device ID to an access token.
853 const SCOPE_MATRIX_DEVICE_ID_PREFIX: &str = "urn:matrix:client:device:";
854
855 // Generate the device ID if it is not provided.
856 let device_id = device_id.unwrap_or_else(DeviceId::new);
857
858 let mut scopes = vec![
859 Scope::new(SCOPE_MATRIX_CLIENT_SERVER_API_FULL_ACCESS.to_owned()),
860 Scope::new(format!("{SCOPE_MATRIX_DEVICE_ID_PREFIX}{device_id}")),
861 ];
862
863 if let Some(extra_scopes) = additional_scopes {
864 scopes.extend(extra_scopes);
865 }
866
867 (scopes, device_id)
868 }
869
870 /// Log in via OAuth 2.0 with the Authorization Code flow.
871 ///
872 /// This method requires to open a URL in the end-user's browser where they
873 /// will be able to log into their account in the server's web UI and grant
874 /// access to their Matrix account.
875 ///
876 /// The [`OAuthAuthCodeUrlBuilder`] that is returned allows to customize a
877 /// few settings before calling `.build()` to obtain the URL to open in the
878 /// browser of the end-user.
879 ///
880 /// [`OAuth::finish_login()`] must be called once the user has been
881 /// redirected to the `redirect_uri`. [`OAuth::abort_login()`] should be
882 /// called instead if the authorization should be aborted before completion.
883 ///
884 /// # Arguments
885 ///
886 /// - `redirect_uri` - The URI where the end user will be redirected after
887 /// authorizing the login. It must be one of the redirect URIs sent in the
888 /// client metadata during registration.
889 ///
890 /// - `device_id` - The unique ID that will be associated with the session.
891 /// If not set, a random one will be generated. It can be an existing
892 /// device ID from a previous login call. Note that this should be done
893 /// only if the client also holds the corresponding encryption keys.
894 ///
895 /// - `registration_data` - The data to restore or register the client with
896 /// the server. If this is not provided, an error will occur unless
897 /// [`OAuth::register_client()`] or [`OAuth::restore_registered_client()`]
898 /// was called previously.
899 ///
900 /// - `additional_scopes` - Additional scopes to request from the
901 /// authorization server, e.g.
902 /// "urn:matrix:client:com.example.msc9999.foo". The scopes for API access
903 /// and the device ID according to the
904 /// [specification](https://spec.matrix.org/v1.15/client-server-api/#allocated-scope-tokens)
905 /// are always requested.
906 ///
907 /// # Example
908 ///
909 /// ```no_run
910 /// use matrix_sdk::{
911 /// authentication::oauth::registration::ClientMetadata,
912 /// ruma::serde::Raw,
913 /// };
914 /// use url::Url;
915 /// # use matrix_sdk::Client;
916 /// # let client: Client = unimplemented!();
917 /// # let redirect_uri = unimplemented!();
918 /// # async fn open_uri_and_wait_for_redirect(uri: Url) -> Url { unimplemented!() };
919 /// # fn client_metadata() -> Raw<ClientMetadata> { unimplemented!() };
920 /// # _ = async {
921 /// let oauth = client.oauth();
922 /// let client_metadata: Raw<ClientMetadata> = client_metadata();
923 /// let registration_data = client_metadata.into();
924 ///
925 /// let auth_data = oauth.login(redirect_uri, None, Some(registration_data), None)
926 /// .build()
927 /// .await?;
928 ///
929 /// // Open auth_data.url and wait for response at the redirect URI.
930 /// let redirected_to_uri: Url = open_uri_and_wait_for_redirect(auth_data.url).await;
931 ///
932 /// oauth.finish_login(redirected_to_uri.into()).await?;
933 ///
934 /// // The session tokens can be persisted from the
935 /// // `OAuth::full_session()` method.
936 ///
937 /// // You can now make requests to the Matrix API.
938 /// let _me = client.whoami().await?;
939 /// # anyhow::Ok(()) }
940 /// ```
941 pub fn login(
942 &self,
943 redirect_uri: Url,
944 device_id: Option<OwnedDeviceId>,
945 registration_data: Option<ClientRegistrationData>,
946 additional_scopes: Option<Vec<Scope>>,
947 ) -> OAuthAuthCodeUrlBuilder {
948 let (scopes, device_id) = Self::login_scopes(device_id, additional_scopes);
949
950 OAuthAuthCodeUrlBuilder::new(
951 self.clone(),
952 scopes.to_vec(),
953 device_id,
954 redirect_uri,
955 registration_data,
956 )
957 }
958
959 /// Finish the login process.
960 ///
961 /// This method should be called after the URL returned by
962 /// [`OAuthAuthCodeUrlBuilder::build()`] has been presented and the user has
963 /// been redirected to the redirect URI after completing the authorization.
964 ///
965 /// If the authorization needs to be cancelled before its completion,
966 /// [`OAuth::abort_login()`] should be used instead to clean up the local
967 /// data.
968 ///
969 /// # Arguments
970 ///
971 /// - `url_or_query` - The URI where the user was redirected, or just its
972 /// query part.
973 ///
974 /// Returns an error if the authorization failed, if a request fails, or if
975 /// the client was already logged in with a different session.
976 pub async fn finish_login(&self, url_or_query: UrlOrQuery) -> Result<()> {
977 let response = AuthorizationResponse::parse_url_or_query(&url_or_query)
978 .map_err(|error| OAuthError::from(OAuthAuthorizationCodeError::from(error)))?;
979
980 let auth_code = match response {
981 AuthorizationResponse::Success(code) => code,
982 AuthorizationResponse::Error(error) => {
983 self.abort_login(&error.state).await;
984 return Err(OAuthError::from(OAuthAuthorizationCodeError::from(error.error)).into());
985 }
986 };
987
988 let device_id = self.finish_authorization(auth_code).await?;
989 self.load_session(device_id).await
990 }
991
992 /// Load the session after login.
993 ///
994 /// Returns an error if the request to get the user ID fails, or if the
995 /// client was already logged in with a different session.
996 pub(crate) async fn load_session(&self, device_id: OwnedDeviceId) -> Result<()> {
997 // Get the user ID.
998 let whoami_res = self.client.whoami().await.map_err(crate::Error::from)?;
999
1000 let new_session = SessionMeta { user_id: whoami_res.user_id, device_id };
1001
1002 if let Some(current_session) = self.client.session_meta() {
1003 if new_session != *current_session {
1004 return Err(OAuthError::SessionMismatch.into());
1005 }
1006 } else {
1007 self.client
1008 .activate(
1009 new_session,
1010 RoomLoadSettings::default(),
1011 #[cfg(feature = "e2e-encryption")]
1012 None,
1013 )
1014 .await?;
1015 // At this point the Olm machine has been set up.
1016
1017 // Enable the cross-process lock for refreshes, if needs be.
1018 #[cfg(feature = "e2e-encryption")]
1019 self.enable_cross_process_lock().await.map_err(OAuthError::from)?;
1020
1021 #[cfg(feature = "e2e-encryption")]
1022 self.client.encryption().spawn_initialization_task(None).await;
1023 }
1024
1025 Ok(())
1026 }
1027
1028 #[cfg(feature = "e2e-encryption")]
1029 pub(crate) async fn enable_cross_process_lock(
1030 &self,
1031 ) -> Result<(), CrossProcessRefreshLockError> {
1032 // Enable the cross-process lock for refreshes, if needs be.
1033 self.deferred_enable_cross_process_refresh_lock().await;
1034
1035 if let Some(cross_process_manager) = self.ctx().cross_process_token_refresh_manager.get()
1036 && let Some(tokens) = self.client.session_tokens()
1037 {
1038 let mut cross_process_guard = cross_process_manager.spin_lock().await?;
1039
1040 if cross_process_guard.hash_mismatch {
1041 // At this point, we're finishing a login while another process
1042 // had written something in the database. It's likely the
1043 // information in the database is just outdated and wasn't
1044 // properly updated, but display a warning, just in case this
1045 // happens frequently.
1046 warn!("unexpected cross-process hash mismatch when finishing login (see comment)");
1047 }
1048
1049 cross_process_guard.save_in_memory_and_db(&tokens).await?;
1050 }
1051
1052 Ok(())
1053 }
1054
1055 /// Finish the authorization process.
1056 ///
1057 /// This method should be called after the URL returned by
1058 /// [`OAuthAuthCodeUrlBuilder::build()`] has been presented and the user has
1059 /// been redirected to the redirect URI after a successful authorization.
1060 ///
1061 /// # Arguments
1062 ///
1063 /// - `auth_code` - The response received as part of the redirect URI when
1064 /// the authorization was successful.
1065 ///
1066 /// Returns the device ID used in the authorized scope if it succeeds.
1067 /// Returns an error if a request fails.
1068 async fn finish_authorization(
1069 &self,
1070 auth_code: AuthorizationCode,
1071 ) -> Result<OwnedDeviceId, OAuthError> {
1072 let data = self.data().ok_or(OAuthError::NotAuthenticated)?;
1073 let client_id = data.client_id.clone();
1074
1075 let validation_data = data
1076 .authorization_data
1077 .lock()
1078 .await
1079 .remove(&auth_code.state)
1080 .ok_or(OAuthAuthorizationCodeError::InvalidState)?;
1081
1082 let token_uri = TokenUrl::from_url(validation_data.server_metadata.token_endpoint.clone());
1083
1084 let response = OAuthClient::new(client_id)
1085 .set_token_uri(token_uri)
1086 .exchange_code(oauth2::AuthorizationCode::new(auth_code.code))
1087 .set_pkce_verifier(validation_data.pkce_verifier)
1088 .set_redirect_uri(Cow::Owned(validation_data.redirect_uri))
1089 .request_async(self.http_client())
1090 .await
1091 .map_err(OAuthAuthorizationCodeError::RequestToken)?;
1092
1093 self.client.auth_ctx().set_session_tokens(SessionTokens {
1094 access_token: response.access_token().secret().clone(),
1095 refresh_token: response.refresh_token().map(RefreshToken::secret).cloned(),
1096 });
1097
1098 Ok(validation_data.device_id)
1099 }
1100
1101 /// Abort the login process.
1102 ///
1103 /// This method should be called if a login should be aborted before it is
1104 /// completed.
1105 ///
1106 /// If the login has been completed, [`OAuth::finish_login()`] should be
1107 /// used instead.
1108 ///
1109 /// # Arguments
1110 ///
1111 /// - `state` - The state provided in [`OAuthAuthorizationData`] after
1112 /// building the authorization URL.
1113 pub async fn abort_login(&self, state: &CsrfToken) {
1114 if let Some(data) = self.data() {
1115 data.authorization_data.lock().await.remove(state);
1116 }
1117 }
1118
1119 /// Request codes from the authorization server for logging in with another
1120 /// device.
1121 #[cfg(feature = "e2e-encryption")]
1122 async fn request_device_authorization(
1123 &self,
1124 server_metadata: &AuthorizationServerMetadata,
1125 device_id: Option<OwnedDeviceId>,
1126 ) -> Result<oauth2::StandardDeviceAuthorizationResponse, qrcode::DeviceAuthorizationOAuthError>
1127 {
1128 let (scopes, _) = Self::login_scopes(device_id, None);
1129
1130 let client_id = self.client_id().ok_or(OAuthError::NotRegistered)?.clone();
1131
1132 let device_authorization_url = server_metadata
1133 .device_authorization_endpoint
1134 .clone()
1135 .map(oauth2::DeviceAuthorizationUrl::from_url)
1136 .ok_or(qrcode::DeviceAuthorizationOAuthError::NoDeviceAuthorizationEndpoint)?;
1137
1138 let response = OAuthClient::new(client_id)
1139 .set_device_authorization_url(device_authorization_url)
1140 .exchange_device_code()
1141 .add_scopes(scopes)
1142 .request_async(self.http_client())
1143 .await?;
1144
1145 Ok(response)
1146 }
1147
1148 /// Exchange the device code against an access token.
1149 #[cfg(feature = "e2e-encryption")]
1150 async fn exchange_device_code(
1151 &self,
1152 server_metadata: &AuthorizationServerMetadata,
1153 device_authorization_response: &oauth2::StandardDeviceAuthorizationResponse,
1154 ) -> Result<(), qrcode::DeviceAuthorizationOAuthError> {
1155 use oauth2::TokenResponse;
1156
1157 let client_id = self.client_id().ok_or(OAuthError::NotRegistered)?.clone();
1158
1159 let token_uri = TokenUrl::from_url(server_metadata.token_endpoint.clone());
1160
1161 let response = OAuthClient::new(client_id)
1162 .set_token_uri(token_uri)
1163 .exchange_device_access_token(device_authorization_response)
1164 .request_async(self.http_client(), matrix_sdk_common::sleep::sleep, None)
1165 .await?;
1166
1167 self.client.auth_ctx().set_session_tokens(SessionTokens {
1168 access_token: response.access_token().secret().to_owned(),
1169 refresh_token: response.refresh_token().map(|t| t.secret().to_owned()),
1170 });
1171
1172 Ok(())
1173 }
1174
1175 async fn refresh_access_token_inner(
1176 self,
1177 refresh_token: String,
1178 token_endpoint: Url,
1179 client_id: ClientId,
1180 #[cfg(feature = "e2e-encryption")] cross_process_lock: Option<CrossProcessRefreshLockGuard>,
1181 ) -> Result<(), OAuthError> {
1182 trace!(
1183 "Token refresh: attempting to refresh with refresh_token {}",
1184 hash_str(&refresh_token)
1185 );
1186
1187 let token = RefreshToken::new(refresh_token.clone());
1188 let token_uri = TokenUrl::from_url(token_endpoint);
1189
1190 let response = OAuthClient::new(client_id)
1191 .set_token_uri(token_uri)
1192 .exchange_refresh_token(&token)
1193 .request_async(self.http_client())
1194 .await
1195 .map_err(OAuthError::RefreshToken)?;
1196
1197 let new_access_token = response.access_token().secret().clone();
1198 let new_refresh_token = response.refresh_token().map(RefreshToken::secret).cloned();
1199
1200 trace!(
1201 "Token refresh: new refresh_token: {} / access_token: {}",
1202 new_refresh_token.as_deref().map(hash_str).unwrap_or_else(|| "<none>".to_owned()),
1203 hash_str(&new_access_token)
1204 );
1205
1206 let tokens = SessionTokens {
1207 access_token: new_access_token,
1208 refresh_token: new_refresh_token.or(Some(refresh_token)),
1209 };
1210
1211 #[cfg(feature = "e2e-encryption")]
1212 let tokens_clone = tokens.clone();
1213
1214 self.client.auth_ctx().set_session_tokens(tokens);
1215
1216 // Call the save_session_callback if set, while the optional lock is
1217 // being held.
1218 if let Some(save_session_callback) = self.client.auth_ctx().save_session_callback.get() {
1219 // Satisfies the save_session_callback invariant: set_session_tokens
1220 // has been called just above.
1221 tracing::debug!("call save_session_callback");
1222 if let Err(err) = save_session_callback(self.client.clone()) {
1223 error!("when saving session after refresh: {err}");
1224 }
1225 }
1226
1227 #[cfg(feature = "e2e-encryption")]
1228 if let Some(mut lock) = cross_process_lock {
1229 lock.save_in_memory_and_db(&tokens_clone).await?;
1230 }
1231
1232 tracing::debug!("broadcast session changed");
1233 _ = self.client.auth_ctx().session_change_sender.send(SessionChange::TokensRefreshed);
1234
1235 Ok(())
1236 }
1237
1238 /// Refresh the access token.
1239 ///
1240 /// This should be called when the access token has expired. It should not
1241 /// be needed to call this manually if the [`Client`] was constructed with
1242 /// [`ClientBuilder::handle_refresh_tokens()`].
1243 ///
1244 /// This method is protected behind a lock, so calling this method several
1245 /// times at once will only call the endpoint once and all subsequent calls
1246 /// will wait for the result of the first call.
1247 ///
1248 /// [`ClientBuilder::handle_refresh_tokens()`]: crate::ClientBuilder::handle_refresh_tokens()
1249 #[instrument(skip_all)]
1250 pub async fn refresh_access_token(&self) -> Result<(), RefreshTokenError> {
1251 macro_rules! fail {
1252 ($lock:expr, $err:expr) => {
1253 let error = $err;
1254 *$lock = Err(error.clone());
1255 return Err(error);
1256 };
1257 }
1258
1259 let client = &self.client;
1260
1261 let refresh_status_lock = client.auth_ctx().refresh_token_lock.clone().try_lock_owned();
1262
1263 let Ok(mut refresh_status_guard) = refresh_status_lock else {
1264 debug!("another refresh is happening, waiting for result.");
1265 // There's already a request to refresh happening in the same
1266 // process. Wait for it to finish.
1267 let res = client.auth_ctx().refresh_token_lock.lock().await.clone();
1268 debug!("other refresh is a {}", if res.is_ok() { "success" } else { "failure " });
1269 return res;
1270 };
1271
1272 debug!("no other refresh happening in background, starting.");
1273
1274 // Fetch the authorization server metadata _before_ taking the
1275 // cross-process lock, checking the session hash, or reading the refresh
1276 // token. This request can stall for a long time when the OS suspends
1277 // the process (e.g. iOS background suspension), and while suspended the
1278 // lock lease lapses, which lets another process refresh and rotate the
1279 // token. Doing it first means the lock and the hash check happen after
1280 // the stall, so such a rotation is caught below as a hash mismatch
1281 // instead of being exchanged while stale, which the server rejects with
1282 // `invalid_grant` and signs the user out.
1283 let server_metadata = match self.server_metadata().await {
1284 Ok(metadata) => metadata,
1285 Err(err) => {
1286 warn!("couldn't get authorization server metadata: {err:?}");
1287 fail!(refresh_status_guard, RefreshTokenError::OAuth(Arc::new(err.into())));
1288 }
1289 };
1290
1291 let Some(client_id) = self.client_id().cloned() else {
1292 warn!("invalid state: missing client ID");
1293 fail!(
1294 refresh_status_guard,
1295 RefreshTokenError::OAuth(Arc::new(OAuthError::NotAuthenticated))
1296 );
1297 };
1298
1299 #[cfg(feature = "e2e-encryption")]
1300 let cross_process_guard =
1301 if let Some(manager) = self.ctx().cross_process_token_refresh_manager.get() {
1302 let mut cross_process_guard = match manager
1303 .spin_lock()
1304 .await
1305 .map_err(|err| RefreshTokenError::OAuth(Arc::new(err.into())))
1306 {
1307 Ok(guard) => guard,
1308 Err(err) => {
1309 warn!("couldn't acquire cross-process lock (timeout)");
1310 fail!(refresh_status_guard, err);
1311 }
1312 };
1313
1314 if cross_process_guard.hash_mismatch {
1315 Box::pin(self.handle_session_hash_mismatch(&mut cross_process_guard))
1316 .await
1317 .map_err(|err| RefreshTokenError::OAuth(Arc::new(err.into())))?;
1318 // Optimistic exit: assume that the underlying process did
1319 // update fast enough. In the worst case, we'll do another
1320 // refresh Soon™.
1321 tracing::info!("other process handled refresh for us, assuming success");
1322 *refresh_status_guard = Ok(());
1323 return Ok(());
1324 }
1325
1326 Some(cross_process_guard)
1327 } else {
1328 None
1329 };
1330
1331 // Read the refresh token only now, after the hash check above, so we
1332 // always exchange the token that is current in the store, never one
1333 // that another process rotated out from under us while we were
1334 // suspended.
1335 let Some(session_tokens) = self.client.session_tokens() else {
1336 warn!("invalid state: missing session tokens");
1337 fail!(refresh_status_guard, RefreshTokenError::RefreshTokenRequired);
1338 };
1339
1340 let Some(refresh_token) = session_tokens.refresh_token else {
1341 warn!("invalid state: missing session tokens");
1342 fail!(refresh_status_guard, RefreshTokenError::RefreshTokenRequired);
1343 };
1344
1345 // Do not interrupt refresh access token requests and processing, by
1346 // detaching the request sending and response processing. Make sure to
1347 // keep the `refresh_status_guard` during the entire processing.
1348
1349 let this = self.clone();
1350
1351 spawn(async move {
1352 match this
1353 .refresh_access_token_inner(
1354 refresh_token,
1355 server_metadata.token_endpoint,
1356 client_id,
1357 #[cfg(feature = "e2e-encryption")]
1358 cross_process_guard,
1359 )
1360 .await
1361 {
1362 Ok(()) => {
1363 debug!("success refreshing a token");
1364 *refresh_status_guard = Ok(());
1365 Ok(())
1366 }
1367
1368 Err(err) => {
1369 let err = RefreshTokenError::OAuth(Arc::new(err));
1370 warn!("error refreshing an OAuth 2.0 token: {err}");
1371 fail!(refresh_status_guard, err);
1372 }
1373 }
1374 })
1375 .await
1376 .expect("joining")
1377 }
1378
1379 /// Log out from the currently authenticated session.
1380 pub async fn logout(&self) -> Result<(), OAuthError> {
1381 let client_id = self.client_id().ok_or(OAuthError::NotAuthenticated)?.clone();
1382
1383 let server_metadata = self.server_metadata().await?;
1384 let revocation_url = RevocationUrl::from_url(server_metadata.revocation_endpoint);
1385
1386 let tokens = self.client.session_tokens().ok_or(OAuthError::NotAuthenticated)?;
1387
1388 // Revoke the access token, it should revoke both tokens.
1389 OAuthClient::new(client_id)
1390 .set_revocation_url(revocation_url)
1391 .revoke_token(StandardRevocableToken::AccessToken(AccessToken::new(
1392 tokens.access_token,
1393 )))
1394 .map_err(OAuthTokenRevocationError::Url)?
1395 .request_async(self.http_client())
1396 .await
1397 .map_err(OAuthTokenRevocationError::Revoke)?;
1398
1399 #[cfg(feature = "e2e-encryption")]
1400 if let Some(manager) = self.ctx().cross_process_token_refresh_manager.get() {
1401 manager.on_logout().await?;
1402 }
1403
1404 Ok(())
1405 }
1406}
1407
1408/// Builder for QR login futures.
1409#[cfg(feature = "e2e-encryption")]
1410#[derive(Debug)]
1411pub struct LoginWithQrCodeBuilder<'a> {
1412 /// The underlying Matrix API client.
1413 client: &'a Client,
1414
1415 /// The data to restore or register the client with the server.
1416 registration_data: Option<&'a ClientRegistrationData>,
1417}
1418
1419#[cfg(feature = "e2e-encryption")]
1420impl<'a> LoginWithQrCodeBuilder<'a> {
1421 /// Create a new builder.
1422 fn new(client: &'a Client, registration_data: Option<&'a ClientRegistrationData>) -> Self {
1423 Self { client, registration_data }
1424 }
1425
1426 /// This method allows you to log in with a scanned QR code.
1427 ///
1428 /// The existing device needs to display the QR code which this device can
1429 /// scan and call this method to log in.
1430 ///
1431 /// A successful login using this method will automatically mark the device
1432 /// as verified and transfer all end-to-end encryption related secrets, like
1433 /// the private cross-signing keys and the backup key from the existing
1434 /// device to the new device.
1435 ///
1436 /// For the reverse flow where this device generates the QR code for the
1437 /// existing device to scan, use [`LoginWithQrCodeBuilder::generate`].
1438 ///
1439 /// # Arguments
1440 ///
1441 /// - `data` - The data scanned from a QR code.
1442 ///
1443 /// # Example
1444 ///
1445 /// ```no_run
1446 /// use anyhow::bail;
1447 /// use futures_util::StreamExt;
1448 /// use matrix_sdk::{
1449 /// authentication::oauth::{
1450 /// registration::ClientMetadata,
1451 /// qrcode::{LoginProgress, Msc4108IntentData, QrCodeData, QrCodeIntentData, QrProgress},
1452 /// },
1453 /// ruma::serde::Raw,
1454 /// Client,
1455 /// };
1456 /// # fn client_metadata() -> Raw<ClientMetadata> { unimplemented!() }
1457 /// # _ = async {
1458 /// # let bytes = unimplemented!();
1459 /// // You'll need to use a different library to scan and extract the raw bytes from the QR
1460 /// // code.
1461 /// let qr_code_data = QrCodeData::from_bytes(bytes)?;
1462 ///
1463 /// // Fetch the homeserver out of the parsed QR code data.
1464 /// let QrCodeIntentData::Msc4108 { data: Msc4108IntentData::Reciprocate { server_name }, ..} = qr_code_data.intent_data() else {
1465 /// bail!("The QR code is invalid, we did not receive a homeserver in the QR code.");
1466 /// };
1467 ///
1468 /// // Build the client as usual.
1469 /// let client = Client::builder()
1470 /// .server_name_or_homeserver_url(server_name)
1471 /// .handle_refresh_tokens()
1472 /// .build()
1473 /// .await?;
1474 ///
1475 /// let oauth = client.oauth();
1476 /// let client_metadata: Raw<ClientMetadata> = client_metadata();
1477 /// let registration_data = client_metadata.into();
1478 ///
1479 /// // Subscribing to the progress is necessary since we need to input the check
1480 /// // code on the existing device.
1481 /// let login = oauth.login_with_qr_code(Some(®istration_data)).scan(&qr_code_data);
1482 /// let mut progress = login.subscribe_to_progress();
1483 ///
1484 /// // Create a task which will show us the progress and tell us the check
1485 /// // code to input in the existing device.
1486 /// let task = tokio::spawn(async move {
1487 /// while let Some(state) = progress.next().await {
1488 /// match state {
1489 /// LoginProgress::Starting | LoginProgress::SyncingSecrets => (),
1490 /// LoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
1491 /// println!("Please enter the following code into the other device {check_code:02}");
1492 /// },
1493 /// LoginProgress::WaitingForToken { user_code } => {
1494 /// println!("Please use your other device to confirm the log in {user_code}")
1495 /// },
1496 /// LoginProgress::Done => break,
1497 /// }
1498 /// }
1499 /// });
1500 ///
1501 /// // Now run the future to complete the login.
1502 /// login.await?;
1503 /// task.abort();
1504 ///
1505 /// println!("Successfully logged in: {:?} {:?}", client.user_id(), client.device_id());
1506 /// # anyhow::Ok(()) };
1507 /// ```
1508 pub fn scan(self, data: &QrCodeData) -> LoginWithQrCode {
1509 LoginWithQrCode::new(self.client, data, self.registration_data)
1510 }
1511
1512 /// This method allows you to log in by generating a QR code.
1513 ///
1514 /// This device needs to call this method to generate and display the QR
1515 /// code which the existing device can scan and grant the log in.
1516 ///
1517 /// A successful login using this method will automatically mark the device
1518 /// as verified and transfer all end-to-end encryption related secrets, like
1519 /// the private cross-signing keys and the backup key from the existing
1520 /// device to the new device.
1521 ///
1522 /// For the reverse flow where the existing device generates the QR code for
1523 /// this device to scan, use [`LoginWithQrCodeBuilder::scan`].
1524 ///
1525 /// # Example
1526 ///
1527 /// ```no_run
1528 /// use anyhow::bail;
1529 /// use futures_util::StreamExt;
1530 /// use matrix_sdk::{
1531 /// authentication::oauth::{
1532 /// registration::ClientMetadata,
1533 /// qrcode::{GeneratedQrProgress, LoginProgress, QrCodeData},
1534 /// },
1535 /// ruma::serde::Raw,
1536 /// Client,
1537 /// };
1538 /// use std::{error::Error, io::stdin};
1539 /// # fn client_metadata() -> Raw<ClientMetadata> { unimplemented!() }
1540 /// # _ = async {
1541 /// // Build the client as usual.
1542 /// let client = Client::builder()
1543 /// .server_name_or_homeserver_url("matrix.org")
1544 /// .handle_refresh_tokens()
1545 /// .build()
1546 /// .await?;
1547 ///
1548 /// let oauth = client.oauth();
1549 /// let client_metadata: Raw<ClientMetadata> = client_metadata();
1550 /// let registration_data = client_metadata.into();
1551 ///
1552 /// // Subscribing to the progress is necessary since we need to display the
1553 /// // QR code and prompt for the check code.
1554 /// let login = oauth.login_with_qr_code(Some(®istration_data)).generate();
1555 /// let mut progress = login.subscribe_to_progress();
1556 ///
1557 /// // Create a task which will show us the progress and allows us to display
1558 /// // the QR code and prompt for the check code.
1559 /// let task = tokio::spawn(async move {
1560 /// while let Some(state) = progress.next().await {
1561 /// match state {
1562 /// LoginProgress::Starting | LoginProgress::SyncingSecrets => (),
1563 /// LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady(qr)) => {
1564 /// println!("Please use your other device to scan the QR code {:?}", qr)
1565 /// }
1566 /// LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned(cctx)) => {
1567 /// println!("Please enter the code displayed on your other device");
1568 /// let mut s = String::new();
1569 /// stdin().read_line(&mut s)?;
1570 /// let check_code = s.trim().parse::<u8>()?;
1571 /// cctx.send(check_code).await?
1572 /// }
1573 /// LoginProgress::WaitingForToken { user_code } => {
1574 /// println!("Please use your other device to confirm the log in {user_code}")
1575 /// },
1576 /// LoginProgress::Done => break,
1577 /// }
1578 /// }
1579 /// Ok::<(), Box<dyn Error + Send + Sync>>(())
1580 /// });
1581 ///
1582 /// // Now run the future to complete the login.
1583 /// login.await?;
1584 /// task.abort();
1585 ///
1586 /// println!("Successfully logged in: {:?} {:?}", client.user_id(), client.device_id());
1587 /// # anyhow::Ok(()) };
1588 /// ```
1589 pub fn generate(self) -> LoginWithGeneratedQrCode {
1590 LoginWithGeneratedQrCode::new(self.client, self.registration_data)
1591 }
1592}
1593
1594/// Builder for QR login grant handlers.
1595#[cfg(feature = "e2e-encryption")]
1596#[derive(Debug)]
1597pub struct GrantLoginWithQrCodeBuilder<'a> {
1598 /// The underlying Matrix API client.
1599 client: &'a Client,
1600 /// The duration to wait for the homeserver to create the new device after
1601 /// consenting the login before giving up.
1602 device_creation_timeout: Duration,
1603}
1604
1605#[cfg(feature = "e2e-encryption")]
1606impl<'a> GrantLoginWithQrCodeBuilder<'a> {
1607 /// Create a new builder with the default device creation timeout.
1608 fn new(client: &'a Client) -> Self {
1609 Self { client, device_creation_timeout: Duration::from_secs(10) }
1610 }
1611
1612 /// Set the device creation timeout.
1613 ///
1614 /// # Arguments
1615 ///
1616 /// - `device_creation_timeout` - The duration to wait for the homeserver to
1617 /// create the new device after consenting the login before giving up.
1618 pub fn device_creation_timeout(mut self, device_creation_timeout: Duration) -> Self {
1619 self.device_creation_timeout = device_creation_timeout;
1620 self
1621 }
1622
1623 /// This method allows you to grant login to a new device by scanning a QR
1624 /// code generated by the new device.
1625 ///
1626 /// The new device needs to display the QR code which this device can scan
1627 /// and call this method to grant the login.
1628 ///
1629 /// A successful login grant using this method will automatically mark the
1630 /// new device as verified and transfer all end-to-end encryption related
1631 /// secrets, like the private cross-signing keys and the backup key from
1632 /// this device device to the new device.
1633 ///
1634 /// For the reverse flow where this device generates the QR code for the new
1635 /// device to scan, use [`GrantLoginWithQrCodeBuilder::generate`].
1636 ///
1637 /// # Arguments
1638 ///
1639 /// - `data` - The data scanned from a QR code.
1640 ///
1641 /// # Example
1642 ///
1643 /// ```no_run
1644 /// use anyhow::bail;
1645 /// use futures_util::StreamExt;
1646 /// use matrix_sdk::{
1647 /// Client, authentication::oauth::{
1648 /// qrcode::{GrantLoginProgress, QrCodeData, QrProgress},
1649 /// }
1650 /// };
1651 /// use std::{error::Error, io::stdin};
1652 /// # _ = async {
1653 /// # let bytes = unimplemented!();
1654 /// // You'll need to use a different library to scan and extract the raw bytes from the QR
1655 /// // code.
1656 /// let qr_code_data = QrCodeData::from_bytes(bytes)?;
1657 ///
1658 /// // Build the client as usual.
1659 /// let client = Client::builder()
1660 /// .server_name_or_homeserver_url("matrix.org")
1661 /// .handle_refresh_tokens()
1662 /// .build()
1663 /// .await?;
1664 ///
1665 /// let oauth = client.oauth();
1666 ///
1667 /// // Subscribing to the progress is necessary to capture
1668 /// // the checkcode in order to display it to the other device and to obtain the verification URL to
1669 /// // open it in a browser so the user can consent to the new login.
1670 /// let mut grant = oauth.grant_login_with_qr_code().scan(&qr_code_data);
1671 /// let mut progress = grant.subscribe_to_progress();
1672 ///
1673 /// // Create a task which will show us the progress and allows us to receive
1674 /// // and feed back data.
1675 /// let task = tokio::spawn(async move {
1676 /// while let Some(state) = progress.next().await {
1677 /// match state {
1678 /// GrantLoginProgress::Starting | GrantLoginProgress::SyncingSecrets => (),
1679 /// GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
1680 /// println!("Please enter the checkcode on your other device: {:?}", check_code);
1681 /// }
1682 /// GrantLoginProgress::WaitingForAuth { verification_uri, continuation_sender } => {
1683 /// println!("Please open {verification_uri} to confirm the new login");
1684 ///
1685 /// // Once the new login has been confirmed in the browser, we can let the
1686 /// // client continue with the process.
1687 /// continuation_sender.confirm().await?;
1688 /// },
1689 /// GrantLoginProgress::Done => break,
1690 /// }
1691 /// }
1692 /// Ok::<(), Box<dyn Error + Send + Sync>>(())
1693 /// });
1694 ///
1695 /// // Now run the future to grant the login.
1696 /// grant.await?;
1697 /// task.abort();
1698 ///
1699 /// println!("Successfully granted login");
1700 /// # anyhow::Ok(()) };
1701 /// ```
1702 pub fn scan(self, data: &QrCodeData) -> GrantLoginWithScannedQrCode {
1703 GrantLoginWithScannedQrCode::new(self.client, data, self.device_creation_timeout)
1704 }
1705
1706 /// This method allows you to grant login to a new device by generating a QR
1707 /// code on this device to be scanned by the new device.
1708 ///
1709 /// This device needs to call this method to generate and display the QR
1710 /// code which the new device can scan to initiate the grant process.
1711 ///
1712 /// A successful login grant using this method will automatically mark the
1713 /// new device as verified and transfer all end-to-end encryption related
1714 /// secrets, like the private cross-signing keys and the backup key from
1715 /// this device device to the new device.
1716 ///
1717 /// For the reverse flow where the new device generates the QR code for this
1718 /// device to scan, use [`GrantLoginWithQrCodeBuilder::scan`].
1719 ///
1720 /// # Example
1721 ///
1722 /// ```no_run
1723 /// use anyhow::bail;
1724 /// use futures_util::StreamExt;
1725 /// use matrix_sdk::{
1726 /// Client, authentication::oauth::{
1727 /// qrcode::{GeneratedQrProgress, GrantLoginProgress}
1728 /// }
1729 /// };
1730 /// use std::{error::Error, io::stdin};
1731 /// # _ = async {
1732 /// // Build the client as usual.
1733 /// let client = Client::builder()
1734 /// .server_name_or_homeserver_url("matrix.org")
1735 /// .handle_refresh_tokens()
1736 /// .build()
1737 /// .await?;
1738 ///
1739 /// let oauth = client.oauth();
1740 ///
1741 /// // Subscribing to the progress is necessary since we need to capture the
1742 /// // QR code, feed the checkcode back in and obtain the verification URL to
1743 /// // open it in a browser so the user can consent to the new login.
1744 /// let mut grant = oauth.grant_login_with_qr_code().generate();
1745 /// let mut progress = grant.subscribe_to_progress();
1746 ///
1747 /// // Create a task which will show us the progress and allows us to receive
1748 /// // and feed back data.
1749 /// let task = tokio::spawn(async move {
1750 /// while let Some(state) = progress.next().await {
1751 /// match state {
1752 /// GrantLoginProgress::Starting | GrantLoginProgress::SyncingSecrets => (),
1753 /// GrantLoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady(qr_code_data)) => {
1754 /// println!("Please scan the QR code on your other device: {:?}", qr_code_data);
1755 /// }
1756 /// GrantLoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned(checkcode_sender)) => {
1757 /// println!("Please enter the code displayed on your other device");
1758 /// let mut s = String::new();
1759 /// stdin().read_line(&mut s)?;
1760 /// let check_code = s.trim().parse::<u8>()?;
1761 /// checkcode_sender.send(check_code).await?;
1762 /// }
1763 /// GrantLoginProgress::WaitingForAuth { verification_uri, continuation_sender } => {
1764 /// println!("Please open {verification_uri} to confirm the new login");
1765 ///
1766 /// // Once the new login has been confirmed in the browser, we can let the
1767 /// // client continue with the process.
1768 /// continuation_sender.confirm().await?;
1769 /// },
1770 /// GrantLoginProgress::Done => break,
1771 /// }
1772 /// }
1773 /// Ok::<(), Box<dyn Error + Send + Sync>>(())
1774 /// });
1775 ///
1776 /// // Now run the future to grant the login.
1777 /// grant.await?;
1778 /// task.abort();
1779 ///
1780 /// println!("Successfully granted login");
1781 /// # anyhow::Ok(()) };
1782 /// ```
1783 pub fn generate(self) -> GrantLoginWithGeneratedQrCode {
1784 GrantLoginWithGeneratedQrCode::new(self.client, self.device_creation_timeout)
1785 }
1786}
1787/// A full session for the OAuth 2.0 API.
1788#[derive(Debug, Clone)]
1789pub struct OAuthSession {
1790 /// The client ID obtained after registration.
1791 pub client_id: ClientId,
1792
1793 /// The user session.
1794 pub user: UserSession,
1795}
1796
1797/// A user session for the OAuth 2.0 API.
1798#[derive(Debug, Clone, Serialize, Deserialize)]
1799pub struct UserSession {
1800 /// The Matrix user session info.
1801 #[serde(flatten)]
1802 pub meta: SessionMeta,
1803
1804 /// The tokens used for authentication.
1805 #[serde(flatten)]
1806 pub tokens: SessionTokens,
1807}
1808
1809/// The data necessary to validate a response from the Token endpoint in the
1810/// Authorization Code flow.
1811#[derive(Debug)]
1812struct AuthorizationValidationData {
1813 /// The metadata of the server,
1814 server_metadata: AuthorizationServerMetadata,
1815
1816 /// The device ID used in the scope.
1817 device_id: OwnedDeviceId,
1818
1819 /// The URI where the end-user will be redirected after authorization.
1820 redirect_uri: RedirectUrl,
1821
1822 /// A string to correlate the authorization request to the token request.
1823 pkce_verifier: PkceCodeVerifier,
1824}
1825
1826/// The data returned by the server in the redirect URI after a successful
1827/// authorization.
1828#[derive(Debug, Clone)]
1829enum AuthorizationResponse {
1830 /// A successful response.
1831 Success(AuthorizationCode),
1832
1833 /// An error response.
1834 Error(AuthorizationError),
1835}
1836
1837impl AuthorizationResponse {
1838 /// Deserialize an `AuthorizationResponse` from a [`UrlOrQuery`].
1839 ///
1840 /// Returns an error if the URL or query doesn't have the expected format.
1841 fn parse_url_or_query(url_or_query: &UrlOrQuery) -> Result<Self, RedirectUriQueryParseError> {
1842 let query = url_or_query.query().ok_or(RedirectUriQueryParseError::MissingQuery)?;
1843 Self::parse_query(query)
1844 }
1845
1846 /// Deserialize an `AuthorizationResponse` from the query part of a URI.
1847 ///
1848 /// Returns an error if the query doesn't have the expected format.
1849 fn parse_query(query: &str) -> Result<Self, RedirectUriQueryParseError> {
1850 // For some reason deserializing the enum with `serde(untagged)` doesn't
1851 // work, so let's try both variants separately.
1852 if let Ok(code) = serde_html_form::from_str(query) {
1853 return Ok(AuthorizationResponse::Success(code));
1854 }
1855 if let Ok(error) = serde_html_form::from_str(query) {
1856 return Ok(AuthorizationResponse::Error(error));
1857 }
1858
1859 Err(RedirectUriQueryParseError::UnknownFormat)
1860 }
1861}
1862
1863/// The data returned by the server in the redirect URI after a successful
1864/// authorization.
1865#[derive(Debug, Clone, Deserialize)]
1866struct AuthorizationCode {
1867 /// The code to use to retrieve the access token.
1868 code: String,
1869 /// The unique identifier for this transaction.
1870 state: CsrfToken,
1871}
1872
1873/// The data returned by the server in the redirect URI after an authorization
1874/// error.
1875#[derive(Debug, Clone, Deserialize)]
1876struct AuthorizationError {
1877 /// The error.
1878 #[serde(flatten)]
1879 error: StandardErrorResponse<error::AuthorizationCodeErrorResponseType>,
1880 /// The unique identifier for this transaction.
1881 state: CsrfToken,
1882}
1883
1884fn hash_str(x: &str) -> String {
1885 hex::encode(sha2::Sha256::new().chain_update(x).finalize())
1886}
1887
1888/// Data to register or restore a client.
1889#[derive(Debug, Clone)]
1890pub struct ClientRegistrationData {
1891 /// The metadata to use to register the client when using dynamic client
1892 /// registration.
1893 pub metadata: Raw<ClientMetadata>,
1894
1895 /// Static registrations for servers that don't support dynamic registration
1896 /// but provide a client ID out-of-band.
1897 ///
1898 /// The keys of the map should be the URLs of the homeservers, but keys
1899 /// using `issuer` URLs are also supported.
1900 pub static_registrations: Option<HashMap<Url, ClientId>>,
1901}
1902
1903impl ClientRegistrationData {
1904 /// Construct a [`ClientRegistrationData`] with the given metadata and no
1905 /// static registrations.
1906 pub fn new(metadata: Raw<ClientMetadata>) -> Self {
1907 Self { metadata, static_registrations: None }
1908 }
1909}
1910
1911impl From<Raw<ClientMetadata>> for ClientRegistrationData {
1912 fn from(value: Raw<ClientMetadata>) -> Self {
1913 Self::new(value)
1914 }
1915}