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