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