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