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