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