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