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 { client: &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 .base_client()
757 .activate(
758 meta,
759 room_load_settings,
760 #[cfg(feature = "e2e-encryption")]
761 None,
762 )
763 .await?;
764 #[cfg(feature = "e2e-encryption")]
765 self.deferred_enable_cross_process_refresh_lock().await;
766
767 self.client
768 .inner
769 .auth_ctx
770 .auth_data
771 .set(AuthData::OAuth(data))
772 .expect("Client authentication data was already set");
773
774 // Initialize the cross-process locking by saving our tokens' hash into
775 // the database, if we've enabled the cross-process lock.
776
777 #[cfg(feature = "e2e-encryption")]
778 if let Some(cross_process_lock) = self.ctx().cross_process_token_refresh_manager.get() {
779 cross_process_lock.restore_session(&tokens).await;
780
781 let mut guard = cross_process_lock
782 .spin_lock()
783 .await
784 .map_err(|err| crate::Error::OAuth(Box::new(err.into())))?;
785
786 // After we got the lock, it's possible that our session doesn't
787 // match the one read from the database, because of a race: another
788 // process has refreshed the tokens while we were waiting for the
789 // lock.
790 //
791 // In that case, if there's a mismatch, we reload the session and
792 // update the hash. Otherwise, we save our hash into the database.
793
794 if guard.hash_mismatch {
795 Box::pin(self.handle_session_hash_mismatch(&mut guard))
796 .await
797 .map_err(|err| crate::Error::OAuth(Box::new(err.into())))?;
798 } else {
799 guard
800 .save_in_memory_and_db(&tokens)
801 .await
802 .map_err(|err| crate::Error::OAuth(Box::new(err.into())))?;
803 // No need to call the save_session_callback here; it was the
804 // source of the session, so it's already in sync with what we
805 // had.
806 }
807 }
808
809 #[cfg(feature = "e2e-encryption")]
810 self.client.encryption().spawn_initialization_task(None).await;
811
812 Ok(())
813 }
814
815 #[cfg(feature = "e2e-encryption")]
816 async fn handle_session_hash_mismatch(
817 &self,
818 guard: &mut CrossProcessRefreshLockGuard,
819 ) -> Result<(), CrossProcessRefreshLockError> {
820 trace!("Handling hash mismatch.");
821
822 let callback = self
823 .client
824 .auth_ctx()
825 .reload_session_callback
826 .get()
827 .ok_or(CrossProcessRefreshLockError::MissingReloadSession)?;
828
829 match callback(self.client.clone()) {
830 Ok(tokens) => {
831 guard.handle_mismatch(&tokens).await?;
832
833 self.client.auth_ctx().set_session_tokens(tokens.clone());
834 // The app's callback acted as authoritative here, so we're not
835 // saving the data back into the app, as that would have no
836 // effect.
837 }
838 Err(err) => {
839 error!("when reloading OAuth 2.0 session tokens from callback: {err}");
840 }
841 }
842
843 Ok(())
844 }
845
846 /// The scopes to request for logging in and the corresponding device ID.
847 fn login_scopes(
848 device_id: Option<OwnedDeviceId>,
849 additional_scopes: Option<Vec<Scope>>,
850 ) -> (Vec<Scope>, OwnedDeviceId) {
851 /// Scope to grand full access to the client-server API.
852 const SCOPE_MATRIX_CLIENT_SERVER_API_FULL_ACCESS: &str =
853 "urn:matrix:org.matrix.msc2967.client:api:*";
854 /// Prefix of the scope to bind a device ID to an access token.
855 const SCOPE_MATRIX_DEVICE_ID_PREFIX: &str = "urn:matrix:org.matrix.msc2967.client:device:";
856
857 // Generate the device ID if it is not provided.
858 let device_id = device_id.unwrap_or_else(DeviceId::new);
859
860 let mut scopes = vec![
861 Scope::new(SCOPE_MATRIX_CLIENT_SERVER_API_FULL_ACCESS.to_owned()),
862 Scope::new(format!("{SCOPE_MATRIX_DEVICE_ID_PREFIX}{device_id}")),
863 ];
864
865 if let Some(extra_scopes) = additional_scopes {
866 scopes.extend(extra_scopes);
867 }
868
869 (scopes, device_id)
870 }
871
872 /// Log in via OAuth 2.0 with the Authorization Code flow.
873 ///
874 /// This method requires to open a URL in the end-user's browser where they
875 /// will be able to log into their account in the server's web UI and grant
876 /// access to their Matrix account.
877 ///
878 /// The [`OAuthAuthCodeUrlBuilder`] that is returned allows to customize a
879 /// few settings before calling `.build()` to obtain the URL to open in the
880 /// browser of the end-user.
881 ///
882 /// [`OAuth::finish_login()`] must be called once the user has been
883 /// redirected to the `redirect_uri`. [`OAuth::abort_login()`] should be
884 /// called instead if the authorization should be aborted before completion.
885 ///
886 /// # Arguments
887 ///
888 /// - `redirect_uri` - The URI where the end user will be redirected after
889 /// authorizing the login. It must be one of the redirect URIs sent in the
890 /// client metadata during registration.
891 ///
892 /// - `device_id` - The unique ID that will be associated with the session.
893 /// If not set, a random one will be generated. It can be an existing
894 /// device ID from a previous login call. Note that this should be done
895 /// only if the client also holds the corresponding encryption keys.
896 ///
897 /// - `registration_data` - The data to restore or register the client with
898 /// the server. If this is not provided, an error will occur unless
899 /// [`OAuth::register_client()`] or [`OAuth::restore_registered_client()`]
900 /// was called previously.
901 ///
902 /// - `additional_scopes` - Additional scopes to request from the
903 /// authorization server, e.g.
904 /// "urn:matrix:client:com.example.msc9999.foo". The scopes for API access
905 /// and the device ID according to the
906 /// [specification](https://spec.matrix.org/v1.15/client-server-api/#allocated-scope-tokens)
907 /// are always requested.
908 ///
909 /// # Example
910 ///
911 /// ```no_run
912 /// use matrix_sdk::{
913 /// authentication::oauth::registration::ClientMetadata,
914 /// ruma::serde::Raw,
915 /// };
916 /// use url::Url;
917 /// # use matrix_sdk::Client;
918 /// # let client: Client = unimplemented!();
919 /// # let redirect_uri = unimplemented!();
920 /// # async fn open_uri_and_wait_for_redirect(uri: Url) -> Url { unimplemented!() };
921 /// # fn client_metadata() -> Raw<ClientMetadata> { unimplemented!() };
922 /// # _ = async {
923 /// let oauth = client.oauth();
924 /// let client_metadata: Raw<ClientMetadata> = client_metadata();
925 /// let registration_data = client_metadata.into();
926 ///
927 /// let auth_data = oauth.login(redirect_uri, None, Some(registration_data), None)
928 /// .build()
929 /// .await?;
930 ///
931 /// // Open auth_data.url and wait for response at the redirect URI.
932 /// let redirected_to_uri: Url = open_uri_and_wait_for_redirect(auth_data.url).await;
933 ///
934 /// oauth.finish_login(redirected_to_uri.into()).await?;
935 ///
936 /// // The session tokens can be persisted from the
937 /// // `OAuth::full_session()` method.
938 ///
939 /// // You can now make requests to the Matrix API.
940 /// let _me = client.whoami().await?;
941 /// # anyhow::Ok(()) }
942 /// ```
943 pub fn login(
944 &self,
945 redirect_uri: Url,
946 device_id: Option<OwnedDeviceId>,
947 registration_data: Option<ClientRegistrationData>,
948 additional_scopes: Option<Vec<Scope>>,
949 ) -> OAuthAuthCodeUrlBuilder {
950 let (scopes, device_id) = Self::login_scopes(device_id, additional_scopes);
951
952 OAuthAuthCodeUrlBuilder::new(
953 self.clone(),
954 scopes.to_vec(),
955 device_id,
956 redirect_uri,
957 registration_data,
958 )
959 }
960
961 /// Finish the login process.
962 ///
963 /// This method should be called after the URL returned by
964 /// [`OAuthAuthCodeUrlBuilder::build()`] has been presented and the user has
965 /// been redirected to the redirect URI after completing the authorization.
966 ///
967 /// If the authorization needs to be cancelled before its completion,
968 /// [`OAuth::abort_login()`] should be used instead to clean up the local
969 /// data.
970 ///
971 /// # Arguments
972 ///
973 /// - `url_or_query` - The URI where the user was redirected, or just its
974 /// query part.
975 ///
976 /// Returns an error if the authorization failed, if a request fails, or if
977 /// the client was already logged in with a different session.
978 pub async fn finish_login(&self, url_or_query: UrlOrQuery) -> Result<()> {
979 let response = AuthorizationResponse::parse_url_or_query(&url_or_query)
980 .map_err(|error| OAuthError::from(OAuthAuthorizationCodeError::from(error)))?;
981
982 let auth_code = match response {
983 AuthorizationResponse::Success(code) => code,
984 AuthorizationResponse::Error(error) => {
985 self.abort_login(&error.state).await;
986 return Err(OAuthError::from(OAuthAuthorizationCodeError::from(error.error)).into());
987 }
988 };
989
990 let device_id = self.finish_authorization(auth_code).await?;
991 self.load_session(device_id).await
992 }
993
994 /// Load the session after login.
995 ///
996 /// Returns an error if the request to get the user ID fails, or if the
997 /// client was already logged in with a different session.
998 pub(crate) async fn load_session(&self, device_id: OwnedDeviceId) -> Result<()> {
999 // Get the user ID.
1000 let whoami_res = self.client.whoami().await.map_err(crate::Error::from)?;
1001
1002 let new_session = SessionMeta { user_id: whoami_res.user_id, device_id };
1003
1004 if let Some(current_session) = self.client.session_meta() {
1005 if new_session != *current_session {
1006 return Err(OAuthError::SessionMismatch.into());
1007 }
1008 } else {
1009 self.client
1010 .base_client()
1011 .activate(
1012 new_session,
1013 RoomLoadSettings::default(),
1014 #[cfg(feature = "e2e-encryption")]
1015 None,
1016 )
1017 .await?;
1018 // At this point the Olm machine has been set up.
1019
1020 // Enable the cross-process lock for refreshes, if needs be.
1021 #[cfg(feature = "e2e-encryption")]
1022 self.enable_cross_process_lock().await.map_err(OAuthError::from)?;
1023
1024 #[cfg(feature = "e2e-encryption")]
1025 self.client.encryption().spawn_initialization_task(None).await;
1026 }
1027
1028 Ok(())
1029 }
1030
1031 #[cfg(feature = "e2e-encryption")]
1032 pub(crate) async fn enable_cross_process_lock(
1033 &self,
1034 ) -> Result<(), CrossProcessRefreshLockError> {
1035 // Enable the cross-process lock for refreshes, if needs be.
1036 self.deferred_enable_cross_process_refresh_lock().await;
1037
1038 if let Some(cross_process_manager) = self.ctx().cross_process_token_refresh_manager.get()
1039 && let Some(tokens) = self.client.session_tokens()
1040 {
1041 let mut cross_process_guard = cross_process_manager.spin_lock().await?;
1042
1043 if cross_process_guard.hash_mismatch {
1044 // At this point, we're finishing a login while another process
1045 // had written something in the database. It's likely the
1046 // information in the database is just outdated and wasn't
1047 // properly updated, but display a warning, just in case this
1048 // happens frequently.
1049 warn!("unexpected cross-process hash mismatch when finishing login (see comment)");
1050 }
1051
1052 cross_process_guard.save_in_memory_and_db(&tokens).await?;
1053 }
1054
1055 Ok(())
1056 }
1057
1058 /// Finish the authorization process.
1059 ///
1060 /// This method should be called after the URL returned by
1061 /// [`OAuthAuthCodeUrlBuilder::build()`] has been presented and the user has
1062 /// been redirected to the redirect URI after a successful authorization.
1063 ///
1064 /// # Arguments
1065 ///
1066 /// - `auth_code` - The response received as part of the redirect URI when
1067 /// the authorization was successful.
1068 ///
1069 /// Returns the device ID used in the authorized scope if it succeeds.
1070 /// Returns an error if a request fails.
1071 async fn finish_authorization(
1072 &self,
1073 auth_code: AuthorizationCode,
1074 ) -> Result<OwnedDeviceId, OAuthError> {
1075 let data = self.data().ok_or(OAuthError::NotAuthenticated)?;
1076 let client_id = data.client_id.clone();
1077
1078 let validation_data = data
1079 .authorization_data
1080 .lock()
1081 .await
1082 .remove(&auth_code.state)
1083 .ok_or(OAuthAuthorizationCodeError::InvalidState)?;
1084
1085 let token_uri = TokenUrl::from_url(validation_data.server_metadata.token_endpoint.clone());
1086
1087 let response = OAuthClient::new(client_id)
1088 .set_token_uri(token_uri)
1089 .exchange_code(oauth2::AuthorizationCode::new(auth_code.code))
1090 .set_pkce_verifier(validation_data.pkce_verifier)
1091 .set_redirect_uri(Cow::Owned(validation_data.redirect_uri))
1092 .request_async(self.http_client())
1093 .await
1094 .map_err(OAuthAuthorizationCodeError::RequestToken)?;
1095
1096 self.client.auth_ctx().set_session_tokens(SessionTokens {
1097 access_token: response.access_token().secret().clone(),
1098 refresh_token: response.refresh_token().map(RefreshToken::secret).cloned(),
1099 });
1100
1101 Ok(validation_data.device_id)
1102 }
1103
1104 /// Abort the login process.
1105 ///
1106 /// This method should be called if a login should be aborted before it is
1107 /// completed.
1108 ///
1109 /// If the login has been completed, [`OAuth::finish_login()`] should be
1110 /// used instead.
1111 ///
1112 /// # Arguments
1113 ///
1114 /// - `state` - The state provided in [`OAuthAuthorizationData`] after
1115 /// building the authorization URL.
1116 pub async fn abort_login(&self, state: &CsrfToken) {
1117 if let Some(data) = self.data() {
1118 data.authorization_data.lock().await.remove(state);
1119 }
1120 }
1121
1122 /// Request codes from the authorization server for logging in with another
1123 /// device.
1124 #[cfg(feature = "e2e-encryption")]
1125 async fn request_device_authorization(
1126 &self,
1127 server_metadata: &AuthorizationServerMetadata,
1128 device_id: Option<OwnedDeviceId>,
1129 ) -> Result<oauth2::StandardDeviceAuthorizationResponse, qrcode::DeviceAuthorizationOAuthError>
1130 {
1131 let (scopes, _) = Self::login_scopes(device_id, None);
1132
1133 let client_id = self.client_id().ok_or(OAuthError::NotRegistered)?.clone();
1134
1135 let device_authorization_url = server_metadata
1136 .device_authorization_endpoint
1137 .clone()
1138 .map(oauth2::DeviceAuthorizationUrl::from_url)
1139 .ok_or(qrcode::DeviceAuthorizationOAuthError::NoDeviceAuthorizationEndpoint)?;
1140
1141 let response = OAuthClient::new(client_id)
1142 .set_device_authorization_url(device_authorization_url)
1143 .exchange_device_code()
1144 .add_scopes(scopes)
1145 .request_async(self.http_client())
1146 .await?;
1147
1148 Ok(response)
1149 }
1150
1151 /// Exchange the device code against an access token.
1152 #[cfg(feature = "e2e-encryption")]
1153 async fn exchange_device_code(
1154 &self,
1155 server_metadata: &AuthorizationServerMetadata,
1156 device_authorization_response: &oauth2::StandardDeviceAuthorizationResponse,
1157 ) -> Result<(), qrcode::DeviceAuthorizationOAuthError> {
1158 use oauth2::TokenResponse;
1159
1160 let client_id = self.client_id().ok_or(OAuthError::NotRegistered)?.clone();
1161
1162 let token_uri = TokenUrl::from_url(server_metadata.token_endpoint.clone());
1163
1164 let response = OAuthClient::new(client_id)
1165 .set_token_uri(token_uri)
1166 .exchange_device_access_token(device_authorization_response)
1167 .request_async(self.http_client(), matrix_sdk_common::sleep::sleep, None)
1168 .await?;
1169
1170 self.client.auth_ctx().set_session_tokens(SessionTokens {
1171 access_token: response.access_token().secret().to_owned(),
1172 refresh_token: response.refresh_token().map(|t| t.secret().to_owned()),
1173 });
1174
1175 Ok(())
1176 }
1177
1178 async fn refresh_access_token_inner(
1179 self,
1180 refresh_token: String,
1181 token_endpoint: Url,
1182 client_id: ClientId,
1183 #[cfg(feature = "e2e-encryption")] cross_process_lock: Option<CrossProcessRefreshLockGuard>,
1184 ) -> Result<(), OAuthError> {
1185 trace!(
1186 "Token refresh: attempting to refresh with refresh_token {}",
1187 hash_str(&refresh_token)
1188 );
1189
1190 let token = RefreshToken::new(refresh_token.clone());
1191 let token_uri = TokenUrl::from_url(token_endpoint);
1192
1193 let response = OAuthClient::new(client_id)
1194 .set_token_uri(token_uri)
1195 .exchange_refresh_token(&token)
1196 .request_async(self.http_client())
1197 .await
1198 .map_err(OAuthError::RefreshToken)?;
1199
1200 let new_access_token = response.access_token().secret().clone();
1201 let new_refresh_token = response.refresh_token().map(RefreshToken::secret).cloned();
1202
1203 trace!(
1204 "Token refresh: new refresh_token: {} / access_token: {}",
1205 new_refresh_token.as_deref().map(hash_str).unwrap_or_else(|| "<none>".to_owned()),
1206 hash_str(&new_access_token)
1207 );
1208
1209 let tokens = SessionTokens {
1210 access_token: new_access_token,
1211 refresh_token: new_refresh_token.or(Some(refresh_token)),
1212 };
1213
1214 #[cfg(feature = "e2e-encryption")]
1215 let tokens_clone = tokens.clone();
1216
1217 self.client.auth_ctx().set_session_tokens(tokens);
1218
1219 // Call the save_session_callback if set, while the optional lock is
1220 // being held.
1221 if let Some(save_session_callback) = self.client.auth_ctx().save_session_callback.get() {
1222 // Satisfies the save_session_callback invariant: set_session_tokens
1223 // has been called just above.
1224 tracing::debug!("call save_session_callback");
1225 if let Err(err) = save_session_callback(self.client.clone()) {
1226 error!("when saving session after refresh: {err}");
1227 }
1228 }
1229
1230 #[cfg(feature = "e2e-encryption")]
1231 if let Some(mut lock) = cross_process_lock {
1232 lock.save_in_memory_and_db(&tokens_clone).await?;
1233 }
1234
1235 tracing::debug!("broadcast session changed");
1236 _ = self.client.auth_ctx().session_change_sender.send(SessionChange::TokensRefreshed);
1237
1238 Ok(())
1239 }
1240
1241 /// Refresh the access token.
1242 ///
1243 /// This should be called when the access token has expired. It should not
1244 /// be needed to call this manually if the [`Client`] was constructed with
1245 /// [`ClientBuilder::handle_refresh_tokens()`].
1246 ///
1247 /// This method is protected behind a lock, so calling this method several
1248 /// times at once will only call the endpoint once and all subsequent calls
1249 /// will wait for the result of the first call.
1250 ///
1251 /// [`ClientBuilder::handle_refresh_tokens()`]: crate::ClientBuilder::handle_refresh_tokens()
1252 #[instrument(skip_all)]
1253 pub async fn refresh_access_token(&self) -> Result<(), RefreshTokenError> {
1254 macro_rules! fail {
1255 ($lock:expr, $err:expr) => {
1256 let error = $err;
1257 *$lock = Err(error.clone());
1258 return Err(error);
1259 };
1260 }
1261
1262 let client = &self.client;
1263
1264 let refresh_status_lock = client.auth_ctx().refresh_token_lock.clone().try_lock_owned();
1265
1266 let Ok(mut refresh_status_guard) = refresh_status_lock else {
1267 debug!("another refresh is happening, waiting for result.");
1268 // There's already a request to refresh happening in the same
1269 // process. Wait for it to finish.
1270 let res = client.auth_ctx().refresh_token_lock.lock().await.clone();
1271 debug!("other refresh is a {}", if res.is_ok() { "success" } else { "failure " });
1272 return res;
1273 };
1274
1275 debug!("no other refresh happening in background, starting.");
1276
1277 // Fetch the authorization server metadata _before_ taking the
1278 // cross-process lock, checking the session hash, or reading the refresh
1279 // token. This request can stall for a long time when the OS suspends
1280 // the process (e.g. iOS background suspension), and while suspended the
1281 // lock lease lapses, which lets another process refresh and rotate the
1282 // token. Doing it first means the lock and the hash check happen after
1283 // the stall, so such a rotation is caught below as a hash mismatch
1284 // instead of being exchanged while stale, which the server rejects with
1285 // `invalid_grant` and signs the user out.
1286 let server_metadata = match self.server_metadata().await {
1287 Ok(metadata) => metadata,
1288 Err(err) => {
1289 warn!("couldn't get authorization server metadata: {err:?}");
1290 fail!(refresh_status_guard, RefreshTokenError::OAuth(Arc::new(err.into())));
1291 }
1292 };
1293
1294 let Some(client_id) = self.client_id().cloned() else {
1295 warn!("invalid state: missing client ID");
1296 fail!(
1297 refresh_status_guard,
1298 RefreshTokenError::OAuth(Arc::new(OAuthError::NotAuthenticated))
1299 );
1300 };
1301
1302 #[cfg(feature = "e2e-encryption")]
1303 let cross_process_guard =
1304 if let Some(manager) = self.ctx().cross_process_token_refresh_manager.get() {
1305 let mut cross_process_guard = match manager
1306 .spin_lock()
1307 .await
1308 .map_err(|err| RefreshTokenError::OAuth(Arc::new(err.into())))
1309 {
1310 Ok(guard) => guard,
1311 Err(err) => {
1312 warn!("couldn't acquire cross-process lock (timeout)");
1313 fail!(refresh_status_guard, err);
1314 }
1315 };
1316
1317 if cross_process_guard.hash_mismatch {
1318 Box::pin(self.handle_session_hash_mismatch(&mut cross_process_guard))
1319 .await
1320 .map_err(|err| RefreshTokenError::OAuth(Arc::new(err.into())))?;
1321 // Optimistic exit: assume that the underlying process did
1322 // update fast enough. In the worst case, we'll do another
1323 // refresh Soon™.
1324 tracing::info!("other process handled refresh for us, assuming success");
1325 *refresh_status_guard = Ok(());
1326 return Ok(());
1327 }
1328
1329 Some(cross_process_guard)
1330 } else {
1331 None
1332 };
1333
1334 // Read the refresh token only now, after the hash check above, so we
1335 // always exchange the token that is current in the store, never one
1336 // that another process rotated out from under us while we were
1337 // suspended.
1338 let Some(session_tokens) = self.client.session_tokens() else {
1339 warn!("invalid state: missing session tokens");
1340 fail!(refresh_status_guard, RefreshTokenError::RefreshTokenRequired);
1341 };
1342
1343 let Some(refresh_token) = session_tokens.refresh_token else {
1344 warn!("invalid state: missing session tokens");
1345 fail!(refresh_status_guard, RefreshTokenError::RefreshTokenRequired);
1346 };
1347
1348 // Do not interrupt refresh access token requests and processing, by
1349 // detaching the request sending and response processing. Make sure to
1350 // keep the `refresh_status_guard` during the entire processing.
1351
1352 let this = self.clone();
1353
1354 spawn(async move {
1355 match this
1356 .refresh_access_token_inner(
1357 refresh_token,
1358 server_metadata.token_endpoint,
1359 client_id,
1360 #[cfg(feature = "e2e-encryption")]
1361 cross_process_guard,
1362 )
1363 .await
1364 {
1365 Ok(()) => {
1366 debug!("success refreshing a token");
1367 *refresh_status_guard = Ok(());
1368 Ok(())
1369 }
1370
1371 Err(err) => {
1372 let err = RefreshTokenError::OAuth(Arc::new(err));
1373 warn!("error refreshing an OAuth 2.0 token: {err}");
1374 fail!(refresh_status_guard, err);
1375 }
1376 }
1377 })
1378 .await
1379 .expect("joining")
1380 }
1381
1382 /// Log out from the currently authenticated session.
1383 pub async fn logout(&self) -> Result<(), OAuthError> {
1384 let client_id = self.client_id().ok_or(OAuthError::NotAuthenticated)?.clone();
1385
1386 let server_metadata = self.server_metadata().await?;
1387 let revocation_url = RevocationUrl::from_url(server_metadata.revocation_endpoint);
1388
1389 let tokens = self.client.session_tokens().ok_or(OAuthError::NotAuthenticated)?;
1390
1391 // Revoke the access token, it should revoke both tokens.
1392 OAuthClient::new(client_id)
1393 .set_revocation_url(revocation_url)
1394 .revoke_token(StandardRevocableToken::AccessToken(AccessToken::new(
1395 tokens.access_token,
1396 )))
1397 .map_err(OAuthTokenRevocationError::Url)?
1398 .request_async(self.http_client())
1399 .await
1400 .map_err(OAuthTokenRevocationError::Revoke)?;
1401
1402 #[cfg(feature = "e2e-encryption")]
1403 if let Some(manager) = self.ctx().cross_process_token_refresh_manager.get() {
1404 manager.on_logout().await?;
1405 }
1406
1407 Ok(())
1408 }
1409}
1410
1411/// Builder for QR login futures.
1412#[cfg(feature = "e2e-encryption")]
1413#[derive(Debug)]
1414pub struct LoginWithQrCodeBuilder<'a> {
1415 /// The underlying Matrix API client.
1416 client: &'a Client,
1417
1418 /// The data to restore or register the client with the server.
1419 registration_data: Option<&'a ClientRegistrationData>,
1420}
1421
1422#[cfg(feature = "e2e-encryption")]
1423impl<'a> LoginWithQrCodeBuilder<'a> {
1424 /// This method allows you to log in with a scanned QR code.
1425 ///
1426 /// The existing device needs to display the QR code which this device can
1427 /// scan and call this method to log in.
1428 ///
1429 /// A successful login using this method will automatically mark the device
1430 /// as verified and transfer all end-to-end encryption related secrets, like
1431 /// the private cross-signing keys and the backup key from the existing
1432 /// device to the new device.
1433 ///
1434 /// For the reverse flow where this device generates the QR code for the
1435 /// existing device to scan, use [`LoginWithQrCodeBuilder::generate`].
1436 ///
1437 /// # Arguments
1438 ///
1439 /// - `data` - The data scanned from a QR code.
1440 ///
1441 /// # Example
1442 ///
1443 /// ```no_run
1444 /// use anyhow::bail;
1445 /// use futures_util::StreamExt;
1446 /// use matrix_sdk::{
1447 /// authentication::oauth::{
1448 /// registration::ClientMetadata,
1449 /// qrcode::{LoginProgress, Msc4108IntentData, QrCodeData, QrCodeIntentData, QrProgress},
1450 /// },
1451 /// ruma::serde::Raw,
1452 /// Client,
1453 /// };
1454 /// # fn client_metadata() -> Raw<ClientMetadata> { unimplemented!() }
1455 /// # _ = async {
1456 /// # let bytes = unimplemented!();
1457 /// // You'll need to use a different library to scan and extract the raw bytes from the QR
1458 /// // code.
1459 /// let qr_code_data = QrCodeData::from_bytes(bytes)?;
1460 ///
1461 /// // Fetch the homeserver out of the parsed QR code data.
1462 /// let QrCodeIntentData::Msc4108 { data: Msc4108IntentData::Reciprocate { server_name }, ..} = qr_code_data.intent_data() else {
1463 /// bail!("The QR code is invalid, we did not receive a homeserver in the QR code.");
1464 /// };
1465 ///
1466 /// // Build the client as usual.
1467 /// let client = Client::builder()
1468 /// .server_name_or_homeserver_url(server_name)
1469 /// .handle_refresh_tokens()
1470 /// .build()
1471 /// .await?;
1472 ///
1473 /// let oauth = client.oauth();
1474 /// let client_metadata: Raw<ClientMetadata> = client_metadata();
1475 /// let registration_data = client_metadata.into();
1476 ///
1477 /// // Subscribing to the progress is necessary since we need to input the check
1478 /// // code on the existing device.
1479 /// let login = oauth.login_with_qr_code(Some(®istration_data)).scan(&qr_code_data);
1480 /// let mut progress = login.subscribe_to_progress();
1481 ///
1482 /// // Create a task which will show us the progress and tell us the check
1483 /// // code to input in the existing device.
1484 /// let task = tokio::spawn(async move {
1485 /// while let Some(state) = progress.next().await {
1486 /// match state {
1487 /// LoginProgress::Starting | LoginProgress::SyncingSecrets => (),
1488 /// LoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
1489 /// println!("Please enter the following code into the other device {check_code:02}");
1490 /// },
1491 /// LoginProgress::WaitingForToken { user_code } => {
1492 /// println!("Please use your other device to confirm the log in {user_code}")
1493 /// },
1494 /// LoginProgress::Done => break,
1495 /// }
1496 /// }
1497 /// });
1498 ///
1499 /// // Now run the future to complete the login.
1500 /// login.await?;
1501 /// task.abort();
1502 ///
1503 /// println!("Successfully logged in: {:?} {:?}", client.user_id(), client.device_id());
1504 /// # anyhow::Ok(()) };
1505 /// ```
1506 pub fn scan(self, data: &'a QrCodeData) -> LoginWithQrCode<'a> {
1507 LoginWithQrCode::new(self.client, data, self.registration_data)
1508 }
1509
1510 /// This method allows you to log in by generating a QR code.
1511 ///
1512 /// This device needs to call this method to generate and display the QR
1513 /// code which the existing device can scan and grant the log in.
1514 ///
1515 /// A successful login using this method will automatically mark the device
1516 /// as verified and transfer all end-to-end encryption related secrets, like
1517 /// the private cross-signing keys and the backup key from the existing
1518 /// device to the new device.
1519 ///
1520 /// For the reverse flow where the existing device generates the QR code for
1521 /// this device to scan, use [`LoginWithQrCodeBuilder::scan`].
1522 ///
1523 /// # Example
1524 ///
1525 /// ```no_run
1526 /// use anyhow::bail;
1527 /// use futures_util::StreamExt;
1528 /// use matrix_sdk::{
1529 /// authentication::oauth::{
1530 /// registration::ClientMetadata,
1531 /// qrcode::{GeneratedQrProgress, LoginProgress, QrCodeData},
1532 /// },
1533 /// ruma::serde::Raw,
1534 /// Client,
1535 /// };
1536 /// use std::{error::Error, io::stdin};
1537 /// # fn client_metadata() -> Raw<ClientMetadata> { unimplemented!() }
1538 /// # _ = async {
1539 /// // Build the client as usual.
1540 /// let client = Client::builder()
1541 /// .server_name_or_homeserver_url("matrix.org")
1542 /// .handle_refresh_tokens()
1543 /// .build()
1544 /// .await?;
1545 ///
1546 /// let oauth = client.oauth();
1547 /// let client_metadata: Raw<ClientMetadata> = client_metadata();
1548 /// let registration_data = client_metadata.into();
1549 ///
1550 /// // Subscribing to the progress is necessary since we need to display the
1551 /// // QR code and prompt for the check code.
1552 /// let login = oauth.login_with_qr_code(Some(®istration_data)).generate();
1553 /// let mut progress = login.subscribe_to_progress();
1554 ///
1555 /// // Create a task which will show us the progress and allows us to display
1556 /// // the QR code and prompt for the check code.
1557 /// let task = tokio::spawn(async move {
1558 /// while let Some(state) = progress.next().await {
1559 /// match state {
1560 /// LoginProgress::Starting | LoginProgress::SyncingSecrets => (),
1561 /// LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady(qr)) => {
1562 /// println!("Please use your other device to scan the QR code {:?}", qr)
1563 /// }
1564 /// LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned(cctx)) => {
1565 /// println!("Please enter the code displayed on your other device");
1566 /// let mut s = String::new();
1567 /// stdin().read_line(&mut s)?;
1568 /// let check_code = s.trim().parse::<u8>()?;
1569 /// cctx.send(check_code).await?
1570 /// }
1571 /// LoginProgress::WaitingForToken { user_code } => {
1572 /// println!("Please use your other device to confirm the log in {user_code}")
1573 /// },
1574 /// LoginProgress::Done => break,
1575 /// }
1576 /// }
1577 /// Ok::<(), Box<dyn Error + Send + Sync>>(())
1578 /// });
1579 ///
1580 /// // Now run the future to complete the login.
1581 /// login.await?;
1582 /// task.abort();
1583 ///
1584 /// println!("Successfully logged in: {:?} {:?}", client.user_id(), client.device_id());
1585 /// # anyhow::Ok(()) };
1586 /// ```
1587 pub fn generate(self) -> LoginWithGeneratedQrCode<'a> {
1588 LoginWithGeneratedQrCode::new(self.client, self.registration_data)
1589 }
1590}
1591
1592/// Builder for QR login grant handlers.
1593#[cfg(feature = "e2e-encryption")]
1594#[derive(Debug)]
1595pub struct GrantLoginWithQrCodeBuilder<'a> {
1596 /// The underlying Matrix API client.
1597 client: &'a Client,
1598 /// The duration to wait for the homeserver to create the new device after
1599 /// consenting the login before giving up.
1600 device_creation_timeout: Duration,
1601}
1602
1603#[cfg(feature = "e2e-encryption")]
1604impl<'a> GrantLoginWithQrCodeBuilder<'a> {
1605 /// Create a new builder with the default device creation timeout.
1606 fn new(client: &'a Client) -> Self {
1607 Self { client, device_creation_timeout: Duration::from_secs(10) }
1608 }
1609
1610 /// Set the device creation timeout.
1611 ///
1612 /// # Arguments
1613 ///
1614 /// - `device_creation_timeout` - The duration to wait for the homeserver to
1615 /// create the new device after consenting the login before giving up.
1616 pub fn device_creation_timeout(mut self, device_creation_timeout: Duration) -> Self {
1617 self.device_creation_timeout = device_creation_timeout;
1618 self
1619 }
1620
1621 /// This method allows you to grant login to a new device by scanning a QR
1622 /// code generated by the new device.
1623 ///
1624 /// The new device needs to display the QR code which this device can scan
1625 /// and call this method to grant the login.
1626 ///
1627 /// A successful login grant using this method will automatically mark the
1628 /// new device as verified and transfer all end-to-end encryption related
1629 /// secrets, like the private cross-signing keys and the backup key from
1630 /// this device device to the new device.
1631 ///
1632 /// For the reverse flow where this device generates the QR code for the new
1633 /// device to scan, use [`GrantLoginWithQrCodeBuilder::generate`].
1634 ///
1635 /// # Arguments
1636 ///
1637 /// - `data` - The data scanned from a QR code.
1638 ///
1639 /// # Example
1640 ///
1641 /// ```no_run
1642 /// use anyhow::bail;
1643 /// use futures_util::StreamExt;
1644 /// use matrix_sdk::{
1645 /// Client, authentication::oauth::{
1646 /// qrcode::{GrantLoginProgress, QrCodeData, QrProgress},
1647 /// }
1648 /// };
1649 /// use std::{error::Error, io::stdin};
1650 /// # _ = async {
1651 /// # let bytes = unimplemented!();
1652 /// // You'll need to use a different library to scan and extract the raw bytes from the QR
1653 /// // code.
1654 /// let qr_code_data = QrCodeData::from_bytes(bytes)?;
1655 ///
1656 /// // Build the client as usual.
1657 /// let client = Client::builder()
1658 /// .server_name_or_homeserver_url("matrix.org")
1659 /// .handle_refresh_tokens()
1660 /// .build()
1661 /// .await?;
1662 ///
1663 /// let oauth = client.oauth();
1664 ///
1665 /// // Subscribing to the progress is necessary to capture
1666 /// // the checkcode in order to display it to the other device and to obtain the verification URL to
1667 /// // open it in a browser so the user can consent to the new login.
1668 /// let mut grant = oauth.grant_login_with_qr_code().scan(&qr_code_data);
1669 /// let mut progress = grant.subscribe_to_progress();
1670 ///
1671 /// // Create a task which will show us the progress and allows us to receive
1672 /// // and feed back data.
1673 /// let task = tokio::spawn(async move {
1674 /// while let Some(state) = progress.next().await {
1675 /// match state {
1676 /// GrantLoginProgress::Starting | GrantLoginProgress::SyncingSecrets => (),
1677 /// GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
1678 /// println!("Please enter the checkcode on your other device: {:?}", check_code);
1679 /// }
1680 /// GrantLoginProgress::WaitingForAuth { verification_uri, continuation_sender } => {
1681 /// println!("Please open {verification_uri} to confirm the new login");
1682 ///
1683 /// // Once the new login has been confirmed in the browser, we can let the
1684 /// // client continue with the process.
1685 /// continuation_sender.confirm().await?;
1686 /// },
1687 /// GrantLoginProgress::Done => break,
1688 /// }
1689 /// }
1690 /// Ok::<(), Box<dyn Error + Send + Sync>>(())
1691 /// });
1692 ///
1693 /// // Now run the future to grant the login.
1694 /// grant.await?;
1695 /// task.abort();
1696 ///
1697 /// println!("Successfully granted login");
1698 /// # anyhow::Ok(()) };
1699 /// ```
1700 pub fn scan(self, data: &'a QrCodeData) -> GrantLoginWithScannedQrCode<'a> {
1701 GrantLoginWithScannedQrCode::new(self.client, data, self.device_creation_timeout)
1702 }
1703
1704 /// This method allows you to grant login to a new device by generating a QR
1705 /// code on this device to be scanned by the new device.
1706 ///
1707 /// This device needs to call this method to generate and display the QR
1708 /// code which the new device can scan to initiate the grant process.
1709 ///
1710 /// A successful login grant using this method will automatically mark the
1711 /// new device as verified and transfer all end-to-end encryption related
1712 /// secrets, like the private cross-signing keys and the backup key from
1713 /// this device device to the new device.
1714 ///
1715 /// For the reverse flow where the new device generates the QR code for this
1716 /// device to scan, use [`GrantLoginWithQrCodeBuilder::scan`].
1717 ///
1718 /// # Example
1719 ///
1720 /// ```no_run
1721 /// use anyhow::bail;
1722 /// use futures_util::StreamExt;
1723 /// use matrix_sdk::{
1724 /// Client, authentication::oauth::{
1725 /// qrcode::{GeneratedQrProgress, GrantLoginProgress}
1726 /// }
1727 /// };
1728 /// use std::{error::Error, io::stdin};
1729 /// # _ = async {
1730 /// // Build the client as usual.
1731 /// let client = Client::builder()
1732 /// .server_name_or_homeserver_url("matrix.org")
1733 /// .handle_refresh_tokens()
1734 /// .build()
1735 /// .await?;
1736 ///
1737 /// let oauth = client.oauth();
1738 ///
1739 /// // Subscribing to the progress is necessary since we need to capture the
1740 /// // QR code, feed the checkcode back in and obtain the verification URL to
1741 /// // open it in a browser so the user can consent to the new login.
1742 /// let mut grant = oauth.grant_login_with_qr_code().generate();
1743 /// let mut progress = grant.subscribe_to_progress();
1744 ///
1745 /// // Create a task which will show us the progress and allows us to receive
1746 /// // and feed back data.
1747 /// let task = tokio::spawn(async move {
1748 /// while let Some(state) = progress.next().await {
1749 /// match state {
1750 /// GrantLoginProgress::Starting | GrantLoginProgress::SyncingSecrets => (),
1751 /// GrantLoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady(qr_code_data)) => {
1752 /// println!("Please scan the QR code on your other device: {:?}", qr_code_data);
1753 /// }
1754 /// GrantLoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned(checkcode_sender)) => {
1755 /// println!("Please enter the code displayed on your other device");
1756 /// let mut s = String::new();
1757 /// stdin().read_line(&mut s)?;
1758 /// let check_code = s.trim().parse::<u8>()?;
1759 /// checkcode_sender.send(check_code).await?;
1760 /// }
1761 /// GrantLoginProgress::WaitingForAuth { verification_uri, continuation_sender } => {
1762 /// println!("Please open {verification_uri} to confirm the new login");
1763 ///
1764 /// // Once the new login has been confirmed in the browser, we can let the
1765 /// // client continue with the process.
1766 /// continuation_sender.confirm().await?;
1767 /// },
1768 /// GrantLoginProgress::Done => break,
1769 /// }
1770 /// }
1771 /// Ok::<(), Box<dyn Error + Send + Sync>>(())
1772 /// });
1773 ///
1774 /// // Now run the future to grant the login.
1775 /// grant.await?;
1776 /// task.abort();
1777 ///
1778 /// println!("Successfully granted login");
1779 /// # anyhow::Ok(()) };
1780 /// ```
1781 pub fn generate(self) -> GrantLoginWithGeneratedQrCode<'a> {
1782 GrantLoginWithGeneratedQrCode::new(self.client, self.device_creation_timeout)
1783 }
1784}
1785/// A full session for the OAuth 2.0 API.
1786#[derive(Debug, Clone)]
1787pub struct OAuthSession {
1788 /// The client ID obtained after registration.
1789 pub client_id: ClientId,
1790
1791 /// The user session.
1792 pub user: UserSession,
1793}
1794
1795/// A user session for the OAuth 2.0 API.
1796#[derive(Debug, Clone, Serialize, Deserialize)]
1797pub struct UserSession {
1798 /// The Matrix user session info.
1799 #[serde(flatten)]
1800 pub meta: SessionMeta,
1801
1802 /// The tokens used for authentication.
1803 #[serde(flatten)]
1804 pub tokens: SessionTokens,
1805}
1806
1807/// The data necessary to validate a response from the Token endpoint in the
1808/// Authorization Code flow.
1809#[derive(Debug)]
1810struct AuthorizationValidationData {
1811 /// The metadata of the server,
1812 server_metadata: AuthorizationServerMetadata,
1813
1814 /// The device ID used in the scope.
1815 device_id: OwnedDeviceId,
1816
1817 /// The URI where the end-user will be redirected after authorization.
1818 redirect_uri: RedirectUrl,
1819
1820 /// A string to correlate the authorization request to the token request.
1821 pkce_verifier: PkceCodeVerifier,
1822}
1823
1824/// The data returned by the server in the redirect URI after a successful
1825/// authorization.
1826#[derive(Debug, Clone)]
1827enum AuthorizationResponse {
1828 /// A successful response.
1829 Success(AuthorizationCode),
1830
1831 /// An error response.
1832 Error(AuthorizationError),
1833}
1834
1835impl AuthorizationResponse {
1836 /// Deserialize an `AuthorizationResponse` from a [`UrlOrQuery`].
1837 ///
1838 /// Returns an error if the URL or query doesn't have the expected format.
1839 fn parse_url_or_query(url_or_query: &UrlOrQuery) -> Result<Self, RedirectUriQueryParseError> {
1840 let query = url_or_query.query().ok_or(RedirectUriQueryParseError::MissingQuery)?;
1841 Self::parse_query(query)
1842 }
1843
1844 /// Deserialize an `AuthorizationResponse` from the query part of a URI.
1845 ///
1846 /// Returns an error if the query doesn't have the expected format.
1847 fn parse_query(query: &str) -> Result<Self, RedirectUriQueryParseError> {
1848 // For some reason deserializing the enum with `serde(untagged)` doesn't
1849 // work, so let's try both variants separately.
1850 if let Ok(code) = serde_html_form::from_str(query) {
1851 return Ok(AuthorizationResponse::Success(code));
1852 }
1853 if let Ok(error) = serde_html_form::from_str(query) {
1854 return Ok(AuthorizationResponse::Error(error));
1855 }
1856
1857 Err(RedirectUriQueryParseError::UnknownFormat)
1858 }
1859}
1860
1861/// The data returned by the server in the redirect URI after a successful
1862/// authorization.
1863#[derive(Debug, Clone, Deserialize)]
1864struct AuthorizationCode {
1865 /// The code to use to retrieve the access token.
1866 code: String,
1867 /// The unique identifier for this transaction.
1868 state: CsrfToken,
1869}
1870
1871/// The data returned by the server in the redirect URI after an authorization
1872/// error.
1873#[derive(Debug, Clone, Deserialize)]
1874struct AuthorizationError {
1875 /// The error.
1876 #[serde(flatten)]
1877 error: StandardErrorResponse<error::AuthorizationCodeErrorResponseType>,
1878 /// The unique identifier for this transaction.
1879 state: CsrfToken,
1880}
1881
1882fn hash_str(x: &str) -> String {
1883 hex::encode(sha2::Sha256::new().chain_update(x).finalize())
1884}
1885
1886/// Data to register or restore a client.
1887#[derive(Debug, Clone)]
1888pub struct ClientRegistrationData {
1889 /// The metadata to use to register the client when using dynamic client
1890 /// registration.
1891 pub metadata: Raw<ClientMetadata>,
1892
1893 /// Static registrations for servers that don't support dynamic registration
1894 /// but provide a client ID out-of-band.
1895 ///
1896 /// The keys of the map should be the URLs of the homeservers, but keys
1897 /// using `issuer` URLs are also supported.
1898 pub static_registrations: Option<HashMap<Url, ClientId>>,
1899}
1900
1901impl ClientRegistrationData {
1902 /// Construct a [`ClientRegistrationData`] with the given metadata and no
1903 /// static registrations.
1904 pub fn new(metadata: Raw<ClientMetadata>) -> Self {
1905 Self { metadata, static_registrations: None }
1906 }
1907}
1908
1909impl From<Raw<ClientMetadata>> for ClientRegistrationData {
1910 fn from(value: Raw<ClientMetadata>) -> Self {
1911 Self::new(value)
1912 }
1913}