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