From 900ba33a4e21eb207ae6b2b3a420c9e9713893d3 Mon Sep 17 00:00:00 2001 From: lafleur Date: Mon, 3 Nov 2025 03:07:23 +0100 Subject: [PATCH] OIDC: impl client_registrar over db (sync impl) --- Cargo.lock | 108 ++++++++++- src/api/client/oidc/authorize.rs | 17 +- src/api/client/oidc/register.rs | 8 +- src/database/maps.rs | 8 + src/service/oidc/mod.rs | 316 ++++++++++++++++++++++--------- src/service/oidc/registrar.rs | 201 -------------------- src/service/users/mod.rs | 4 +- 7 files changed, 361 insertions(+), 301 deletions(-) delete mode 100644 src/service/oidc/registrar.rs diff --git a/Cargo.lock b/Cargo.lock index 245f15c99..fdd0ddcd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,15 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.21" @@ -788,10 +797,9 @@ version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "num-traits", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1193,6 +1201,7 @@ dependencies = [ "oxide-auth", "percent-encoding", "rand 0.8.5", + "serde", "thiserror 2.0.17", "tracing", "url", @@ -4078,6 +4087,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "roff" version = "0.2.2" @@ -5958,6 +5989,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.1.3" @@ -5970,6 +6036,44 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings 0.3.1", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/src/api/client/oidc/authorize.rs b/src/api/client/oidc/authorize.rs index 302cf07a2..45f80ecce 100644 --- a/src/api/client/oidc/authorize.rs +++ b/src/api/client/oidc/authorize.rs @@ -47,7 +47,7 @@ pub(crate) async fn authorize( tracing::debug!("submitting OIDC authorisation for token : {token:#?}"); // Get the user id from the token and add it to the query. - let (owner_id, _) = services.oidc.user_and_device_from_token(token)?; + let (owner_id, _) = services.oidc.user_and_device_from_token(token).await?; let mut query_with_user_id = query.clone(); query_with_user_id.username = Some(owner_id.localpart().to_string()); @@ -91,11 +91,16 @@ pub(crate) async fn authorize_consent( .map_err(|err| err!(Request(InvalidUsername("invalid username {owner_id:?}: {err}"))) )?; - let Some(mc) = services.oidc.client_from_client_id(&query.client_id) else { + let Some(matrix_client) = services.oidc + .client_from_client_id(&query.client_id) + .await? else { return Err(err!(Request(Unknown("no client has registered client_id {:?}", query.client_id)))); }; - let scope = query.scope.parse().map_err(|err| - err!(Request(Unknown("could not parse scope {:?}: {}", query.scope, err))))?; + let scope = query.scope + .parse() + .map_err(|err| + err!(Request(Unknown("could not parse scope {:?}: {}", query.scope, err))) + )?; let device_id = services.oidc.device_id_from_scope(scope)?; // Check that the device is registered in the owner devices list. // Note that this is _not_ the OIDC client registration. @@ -110,9 +115,9 @@ pub(crate) async fn authorize_consent( services.oidc.register_device( &query.client_id, (&owner_id, &device_id), - mc.name.as_deref(), + matrix_client.name.as_deref(), client_ip, - )?; + ).await?; } services diff --git a/src/api/client/oidc/register.rs b/src/api/client/oidc/register.rs index 4d5fafaf4..a82179c0f 100644 --- a/src/api/client/oidc/register.rs +++ b/src/api/client/oidc/register.rs @@ -3,7 +3,7 @@ use conduwuit::{Result, err}; use oxide_auth::primitives::prelude::Client; use reqwest::Url; use ruma::{DeviceId, ClientSecret, identifiers_validation}; -use conduwuit_service::oidc::registrar::normalize_redirect; +use conduwuit_service::oidc::normalize_redirect; /// The required parameters to register a new client for OAuth2 application. /// See the required metadata in OAuth2 authorization grant flow in [MSC2966]. @@ -103,7 +103,7 @@ pub(crate) async fn register_client( }, false => None }; - let registerable = match is_private { + let registration = match is_private { | true => &Client::confidential( device_id.as_ref(), redirect_uri, @@ -116,8 +116,8 @@ pub(crate) async fn register_client( scope, ).with_additional_redirect_uris(remaining_uris) }; - tracing::trace!("registering OIDC device : {registerable:#?}"); - services.oidc.register_client(Some(client.client_name.clone()), ®isterable)?; + tracing::trace!("registering OIDC device : {registration:#?}"); + services.oidc.register_client(Some(client.client_name.clone()), ®istration); let client_response = ClientResponse { client_id: device_id.to_string(), diff --git a/src/database/maps.rs b/src/database/maps.rs index eee5f5e46..97a79c201 100644 --- a/src/database/maps.rs +++ b/src/database/maps.rs @@ -434,6 +434,14 @@ pub(super) static MAPS: &[Descriptor] = &[ name: "userroomid_notificationcount", ..descriptor::RANDOM }, + Descriptor { + name: "client_registrar", + ..descriptor::RANDOM + }, + Descriptor { + name: "deviceid_clientidmap", + ..descriptor::RANDOM_SMALL + }, Descriptor { name: "userroomid_invitesender", ..descriptor::RANDOM_SMALL diff --git a/src/service/oidc/mod.rs b/src/service/oidc/mod.rs index ef1ea60d2..040ab9f49 100644 --- a/src/service/oidc/mod.rs +++ b/src/service/oidc/mod.rs @@ -1,31 +1,28 @@ //! OIDC service. //! //! Provides the registrar, authorizer and issuer needed by [api::client::oidc]. -//! The whole OAuth2 flow is taken care of by [oxide-auth]. -//! -//! TODO this service would need a dedicated space in the database. +//! The whole OIDC OAuth2 flow is taken care of by [oxide-auth]. //! //! [oxide-auth]: https://docs.rs/oxide-auth -pub mod registrar; - -use std::sync::{Arc, Mutex}; +use std::{borrow::Cow, sync::{Arc, Mutex}}; use async_trait::async_trait; -use conduwuit::{Result, err}; +use conduwuit::{err, Result}; use conduwuit_core::utils; use oxide_auth::{ - endpoint::Scope, + endpoint::{PreGrant, Scope}, frontends::simple::endpoint::{Generic, Vacant}, primitives::{ - prelude::{ - AuthMap, Authorizer, Client, Issuer, RandomGenerator, Registrar, TokenMap, - }, - registrar::{EncodedClient, RegisteredUrl}, grant::Grant, - } + prelude::{ + AuthMap, Authorizer, Client, ClientUrl, Issuer, RandomGenerator, Registrar, TokenMap, + }, + registrar::{ + Argon2, BoundClient, EncodedClient, RegisteredClient, RegisteredUrl, RegistrarError, + }, + }, }; -use registrar::MatrixClientMap; use ruma::{ api::client::device::Device, MilliSecondsSinceUnixEpoch, @@ -33,27 +30,57 @@ use ruma::{ OwnedUserId, UserId, }; -use database::{Json, Map}; +use database::{Deserialized, Json, Map}; +use serde::{Deserialize, Serialize}; +use url::Url; +use once_cell::sync::Lazy; -use crate::{globals, oidc::registrar::MatrixClient, Dep}; +use crate::{globals, Dep}; pub const SCOPE_PREFIX_DEVICE: &str = "urn:matrix:org.matrix.msc2967.client:device:"; pub const SCOPE_PREFIX_API : &str = "urn:matrix:org.matrix.msc2967.client:api:"; +static PASSWORD_POLICY: Lazy = Lazy::new(Argon2::default); + + +/// A client app that connects to continuwuity via OIDC, as recorded in the +/// database. +#[derive(Clone, Serialize, Deserialize)] +pub struct OidcClient { + /// The name published by the app itself. + pub name: Option, + /// A device id that we'll generate on OIDC registration. + pub device_id: Option, + /// The device's coordinates recorded by oxide-auth. + pub client: EncodedClient, +} struct Services { globals: Dep, } struct Data { + client_registrar: Arc, + deviceid_clientidmap: Arc, userid_devicelistversion: Arc, userdeviceid_metadata: Arc, } pub struct Service { - registrar: Mutex, + /// Authorization tokens are 16 byte random keys to a memory hash map. + /// + /// Will be reinitialised on continuwuity's restart. authorizer: Mutex>, + /// Bearer tokens are also random generated but 256-bit tokens, since they + /// live longer. + /// + /// We could also use a `TokenSigner::ephemeral` here to create signed + /// tokens which can be read and parsed by anyone, but not maliciously + /// created. However, they can not be revoked and thus don't offer even + /// longer lived refresh tokens. + /// + /// Will be reinitialised on continuwuity's restart. issuer: Mutex>, services: Services, db: Data, @@ -61,100 +88,117 @@ pub struct Service { #[async_trait] impl crate::Service for Service { - fn build(args: crate::Args<'_>) -> Result> { Ok(Arc::new(Self::preconfigured(args))) } + fn build(args: crate::Args<'_>) -> Result> { + // TODO implement authorizer and issuer inside database so that token + // requests survive server restarts. + Ok(Arc::new(Self { + authorizer: Mutex::new(AuthMap::new(RandomGenerator::new(16))), + issuer: Mutex::new(TokenMap::new(RandomGenerator::new(16))), + services: Services { + globals: args.depend::("globals"), + }, + db: Data { + client_registrar: args.db["client_registrar"].clone(), + deviceid_clientidmap: args.db["deviceid_clientidmap"].clone(), + userid_devicelistversion: args.db["userid_devicelistversion"].clone(), + userdeviceid_metadata: args.db["userdeviceid_metadata"].clone(), + }, + })) + } fn name(&self) -> &str { crate::service::make_name(std::module_path!()) } } impl Service { - /// Register an OAuth client in the registrar (a ClientMap), so it's known by oxide-auth - /// for future client authentication flows. - pub fn register_client(&self, name: Option, client: &Client) -> Result<()> { - self.registrar - .lock() - .expect("lockable registrar") - .register_client(name.clone(), client.clone()); - - Ok(()) + /// Register an OIDC client in the client_registrar for future + /// authentication flows. + pub fn register_client(&self, display_name: Option, client: &Client) { + let client = client.clone().encode(&*PASSWORD_POLICY); + let client_id = client.client_id.clone(); + let oidc_client = OidcClient { + name: display_name.clone(), + // Matrix clients have no device_id at registration time. + device_id: None, + client, + }; + self.db + .client_registrar + .put(client_id, Json(oidc_client)); } - /// Register a device in the main continuwuity database. This should only happen on successful - /// authentication and consent, and will register the client's device_id. - pub fn register_device( + /// Register a device in the main continuwuity database. This should only + /// happen on successful authentication and consent, and will register the + /// client's device_id. + pub async fn register_device( &self, client_id: &str, (user_id, device_id): (&OwnedUserId, &OwnedDeviceId), display_name: Option<&str>, client_ip: Option, ) -> Result<()> { - let key = (user_id, device_id); - let val = Device { + let device_key = (user_id, device_id); + let device = Device { device_id: device_id.into(), display_name: display_name.map(|n| n.to_string()), last_seen_ip: client_ip, last_seen_ts: Some(MilliSecondsSinceUnixEpoch::now()), }; - increment(&self.db.userid_devicelistversion, user_id.as_bytes()); - self.db.userdeviceid_metadata.put(key, Json(val)); - self - .registrar - .lock() - .expect("lockable registrar") - .set_client_device_id(client_id, &device_id.to_string())?; + increment(&self.db.userid_devicelistversion, user_id.as_bytes()) + .await; + self.db + .userdeviceid_metadata + .put(device_key, Json(device)); + + let mut client : OidcClient = self.db + .client_registrar + .get(client_id) + .await? + .deserialized()?; + client.device_id = Some(device_id.to_string()); + self.db + .client_registrar + .put(client_id.to_string(), Json(client)); + + self.db + .deviceid_clientidmap + .put(device_id, client_id.to_string()); Ok(()) } - #[must_use] - pub(crate) fn preconfigured(args: crate::Args<'_>) -> Self { - Self { - registrar: Mutex::new( - vec![Client::public( - "LocalClient", - RegisteredUrl::Semantic( - "http://localhost/clientside/endpoint".parse().unwrap(), - ), - "default-scope".parse().unwrap(), - )] - .into_iter() - .collect(), - ), - // Authorization tokens are 16 byte random keys to a memory hash map. - authorizer: Mutex::new(AuthMap::new(RandomGenerator::new(16))), - // Bearer tokens are also random generated but 256-bit tokens, since they live longer - // and this example is somewhat paranoid. - // - // We could also use a `TokenSigner::ephemeral` here to create signed tokens which can - // be read and parsed by anyone, but not maliciously created. However, they can not be - // revoked and thus don't offer even longer lived refresh tokens. - issuer: Mutex::new(TokenMap::new(RandomGenerator::new(16))), - services: Services { - globals: args.depend::("globals"), - }, - db: Data { - userid_devicelistversion: args.db["userid_devicelistversion"].clone(), - userdeviceid_metadata: args.db["userdeviceid_metadata"].clone(), - } - } - } - fn grant_from_token(&self, token: &str) -> Option { let issuer = self.issuer.lock().expect("lockable issuer"); - issuer.recover_token(token).expect("infallible recover_token implementation") + issuer.recover_token(token) + .expect("infallible recover_token implementation") } - pub fn client_from_client_id(&self, client_id: &str) -> Option { - let registrar = self.registrar.lock().expect("lockable registrar"); - - //registrar.get_client(client_id).map(|mc| mc.client.clone()) - registrar.get_client(client_id).cloned() + pub async fn client_from_client_id( + &self, + client_id: &str, + ) -> Result> { + self.db + .client_registrar + .get(client_id) + .await? + .deserialized() } - pub fn client_from_device_id(&self, device_id: OwnedDeviceId) -> Option { - let registrar = self.registrar.lock().expect("lockable registrar"); + pub async fn client_from_device_id( + &self, + device_id: OwnedDeviceId, + ) -> Result> { + let client_id: String = self.db + .deviceid_clientidmap + .get(&device_id) + .await? + .deserialized()?; - registrar.find_device(device_id.as_str()).cloned() + self.db + .client_registrar + .get(&client_id) + .await? + .deserialized() } pub fn device_id_from_scope(&self, scope: Scope) -> Result { @@ -172,7 +216,10 @@ impl Service { ) } - pub fn user_and_device_from_token(&self, token: &str) -> Result<(OwnedUserId, OwnedDeviceId)> { + pub async fn user_and_device_from_token( + &self, + token: &str, + ) -> Result<(OwnedUserId, OwnedDeviceId)> { let Some(Grant { owner_id, client_id, .. }) = self.grant_from_token(token) else { return Err(err!(Request(MissingToken("unknown token: {token:?}")))); }; @@ -181,7 +228,9 @@ impl Service { .map_err(|err| err!(Request(InvalidUsername("invalid username {owner_id:?}: {err}"))) )?; - let client = self.client_from_client_id(&client_id).expect("validated client_id"); + let client = self.client_from_client_id(&client_id) + .await? + .expect("validated client_id"); let Some(device_id) = client.device_id else { return Err(err!(Request(Unknown("this client has no device_id yet")))); }; @@ -195,7 +244,7 @@ impl Service { &self, ) -> Generic { Generic { - registrar: self.registrar.lock().unwrap(), + registrar: self, authorizer: self.authorizer.lock().unwrap(), issuer: self.issuer.lock().unwrap(), // Solicitor configured later. @@ -207,8 +256,103 @@ impl Service { } } -fn increment(db: &Arc, key: &[u8]) { - let old = db.get_blocking(key); +async fn increment(db: &Arc, key: &[u8]) { + let old = db.get(key).await; let new = utils::increment(old.ok().as_deref()); db.insert(key, new); } + +/// Substitute "127.0.0.1" and "[::1]" for "localhost" to let oxide-auth compare +/// them ignoring their port. +pub fn normalize_redirect_hostname(url: Url) -> Url { + let mut new_url = url.clone(); + let new_host = url.host_str().map(|h| + h.replace("127.0.0.1", "localhost").replace("[::1]", "localhost") + ); + new_url.set_host(new_host.as_deref()).expect("replaceable redirect hostname"); + + new_url +} + +/// If `url` is a localhost (either 'localhost', '127.0.0.1' or '[::1]'), wrap +/// it in an `IgnorePortOnLocalhost`, so that oxide-auth ignores the port when +/// comparing it with the registered ones. +pub fn normalize_redirect(url: Url) -> RegisteredUrl { + let new_url = normalize_redirect_hostname(url); + + match new_url.host_str() { + Some("localhost") => RegisteredUrl::IgnorePortOnLocalhost(new_url.into()), + _ => RegisteredUrl::Semantic(new_url) + } +} + +/// Let this service act as an oxide-auth `Registrar`. +impl Registrar for Service { + fn bound_redirect<'a>(&self, bound: ClientUrl<'a>) -> Result, RegistrarError> { + let client_handle = self.db + .client_registrar + .get_blocking(bound.client_id.as_ref()) + .map_err(|_| RegistrarError::Unspecified)?; + let oidc_client: OidcClient = client_handle.deserialized() + .map_err(|_| RegistrarError::Unspecified)?; + let client = oidc_client.client; + // Perform exact matching as motivated in the rfc, but substitute + // "127.0.0.1" and "[::1]" for "localhost" to let oxide-auth ignore + // their port. + let redirect_uri = bound.redirect_uri; + let normalized_uri = redirect_uri + .clone() + .map(|u| normalize_redirect(u.to_url())); + let redirect_uri = match normalized_uri { + None => client.redirect_uri.clone(), + Some(url) => { + let original = std::iter::once(&client.redirect_uri); + let alternatives = client.additional_redirect_uris.iter(); + if original + .chain(alternatives) + .any(|registered| *registered == url) + { + // If normalized_uri is Some(url), so is redirect_uri, so unwrap(). + redirect_uri.unwrap().into_owned().into() + } else { + tracing::debug!("the request's redirect url didn't match any registered. bound: {:?}, in client {:#?}", url, client); + return Err(RegistrarError::Unspecified); + } + } + }; + + Ok(BoundClient { + client_id: bound.client_id, + redirect_uri: Cow::Owned(redirect_uri), + }) + } + + fn negotiate(&self, bound: BoundClient<'_>, _scope: Option) -> Result { + let client_handle = self.db + .client_registrar + .get_blocking(bound.client_id.as_ref()) + .map_err(|_| RegistrarError::Unspecified)?; + let oidc_client: OidcClient = client_handle.deserialized() + .map_err(|_| RegistrarError::Unspecified)?; + + Ok(PreGrant { + client_id: bound.client_id.into_owned(), + redirect_uri: bound.redirect_uri.into_owned(), + // Always use the client's scope. + scope: oidc_client.client.default_scope.clone(), + }) + } + + fn check(&self, client_id: &str, passphrase: Option<&[u8]>) -> Result<(), RegistrarError> { + let client_handle = self.db + .client_registrar + .get_blocking(client_id) + .map_err(|_| RegistrarError::Unspecified)?; + let oidc_client: OidcClient = client_handle.deserialized() + .map_err(|_| RegistrarError::Unspecified)?; + let client = oidc_client.client; + + RegisteredClient::new(&client, &*PASSWORD_POLICY) + .check_authentication(passphrase) + } +} diff --git a/src/service/oidc/registrar.rs b/src/service/oidc/registrar.rs deleted file mode 100644 index 9039de157..000000000 --- a/src/service/oidc/registrar.rs +++ /dev/null @@ -1,201 +0,0 @@ -use std::borrow::Cow; -use url::Url; -use std::collections::HashMap; -use std::iter::{Extend, FromIterator}; - -use conduwuit::{Result, err}; -use oxide_auth::endpoint::{PreGrant, Scope}; -use oxide_auth::primitives::prelude::{Client, ClientUrl}; -use oxide_auth::primitives::registrar::{Argon2, BoundClient, EncodedClient, PasswordPolicy, RegisteredClient, RegisteredUrl, Registrar, RegistrarError}; -use once_cell::sync::Lazy; - -/// Substitute "127.0.0.1" and "[::1]" for "localhost" to let oxide-auth compare them -/// ignoring their port. -pub fn normalize_redirect_hostname(url: Url) -> Url { - let mut new_url = url.clone(); - let new_host = url.host_str().map(|h| - h.replace("127.0.0.1", "localhost").replace("[::1]", "localhost") - ); - new_url.set_host(new_host.as_deref()).expect("replaceable redirect hostname"); - - new_url -} - -/// If `url` is a localhost (either 'localhost', '127.0.0.1' or '[::1]'), wrap it in an -/// IgnorePortOnLocalhost, so that oxide-auth ignores the port when comparing it with the -/// registered ones. -pub fn normalize_redirect(url: Url) -> RegisteredUrl { - let new_url = normalize_redirect_hostname(url); - - match new_url.host_str() { - Some("localhost") => RegisteredUrl::IgnorePortOnLocalhost(new_url.into()), - _ => RegisteredUrl::Semantic(new_url) - } -} - - -static DEFAULT_PASSWORD_POLICY: Lazy = Lazy::new(Argon2::default); - -#[derive(Clone)] -pub struct MatrixClient { - pub name: Option, - pub device_id: Option, - pub client: EncodedClient, -} - -/// A very simple, in-memory hash map of client ids to MatrixClient entries. -#[derive(Default)] -pub struct MatrixClientMap { - clients: HashMap, - password_policy: Option>, -} - -impl MatrixClientMap { - /// Create an empty map without any clients in it. - pub fn new() -> MatrixClientMap { - MatrixClientMap::default() - } - - /// Insert or update the client record with an oxide-auth OIDC Client. - /// This should only be called from the OIDC register endpoint. - pub fn register_client(&mut self, name: Option, client: Client) { - let password_policy = Self::current_policy(&self.password_policy); - let client = client.encode(password_policy); - // Matrix clients have no device_id at registration time. - let matrix_client = MatrixClient { name, device_id: None, client: client.clone() }; - self.clients.insert(client.client_id.clone(), matrix_client); - } - - /// Add `device_id` to client stored at `client_id`. - pub fn set_client_device_id(&mut self, client_id: &str, device_id: &str) -> Result<()> { - let client = self.clients.get_mut(client_id).ok_or_else(|| - err!(Request(Unknown("a client under client_id"))))?; - client.device_id = Some(device_id.to_string()); - - Ok(()) - } - - /// Returns a MatrixClient, containing an oxide-auth EncodedClient, and some metadata, - /// like a public name. - pub fn get_client(&self, client_id: &str) -> Option<&MatrixClient> { - self.clients.get(client_id) - } - - pub fn find_device(&self, device_id: &str) -> Option<&EncodedClient> { - self.clients - .values() - .find(|c| c.device_id.as_deref().is_some_and(|d| d == device_id)) - .map(|c| &c.client) - } - - /// Change how passwords are encoded while stored. - pub fn set_password_policy(&mut self, new_policy: P) { - self.password_policy = Some(Box::new(new_policy)) - } - - pub fn get_redirect(&self, client: Client) -> RegisteredUrl { - let password_policy = Self::current_policy(&self.password_policy); - let client = client.encode(password_policy); - - client.redirect_uri - } - - // This is not an instance method because it needs to borrow the box but register needs &mut - fn current_policy<'a>(policy: &'a Option>) -> &'a dyn PasswordPolicy { - policy - .as_ref() - .map(|boxed| &**boxed) - .unwrap_or(&*DEFAULT_PASSWORD_POLICY) - } -} - -impl Extend for MatrixClientMap { - fn extend(&mut self, iter: I) - where - I: IntoIterator, - { - iter.into_iter().for_each(|client| self.register_client(None, client)) - } -} - -impl FromIterator for MatrixClientMap { - fn from_iter(iter: I) -> Self - where - I: IntoIterator, - { - let mut into = MatrixClientMap::new(); - into.extend(iter); - into - } -} - -impl Registrar for MatrixClientMap { - fn bound_redirect<'a>(&self, bound: ClientUrl<'a>) -> Result, RegistrarError> { - let client = match self.clients.get(bound.client_id.as_ref()) { - None => { - tracing::debug!("this client was not registered: {}", bound.client_id); - return Err(RegistrarError::Unspecified); - }, - Some(stored) => &stored.client, - }; - - // Perform exact matching as motivated in the rfc, but substitute "127.0.0.1" and - // "[::1]" for "localhost" to let oxide-auth ignore their port. - let redirect_uri = bound.redirect_uri; - let normalized_uri = redirect_uri - .clone() - .map(|u| normalize_redirect(u.to_url())); - let redirect_uri = match normalized_uri { - None => client.redirect_uri.clone(), - Some(url) => { - let original = std::iter::once(&client.redirect_uri); - let alternatives = client.additional_redirect_uris.iter(); - if original - .chain(alternatives) - .any(|registered| *registered == url) - { - // If normalized_uri is Some(url), so is redirect_uri, so unwrap(). - redirect_uri.unwrap().into_owned().into() - } else { - tracing::debug!("the request's redirect url didn't match any registered. bound: {:?}, in client {:#?}", url, client); - return Err(RegistrarError::Unspecified); - } - } - }; - - Ok(BoundClient { - client_id: bound.client_id, - redirect_uri: Cow::Owned(redirect_uri), - }) - } - - fn negotiate(&self, bound: BoundClient<'_>, _scope: Option) -> Result { - let client = &self - .clients - .get(bound.client_id.as_ref()) - .expect("Bound client appears to not have been constructed with this registrar") - .client; - // Always take the client's scope. - Ok(PreGrant { - client_id: bound.client_id.into_owned(), - redirect_uri: bound.redirect_uri.into_owned(), - scope: client.default_scope.clone(), - }) - } - - fn check(&self, client_id: &str, passphrase: Option<&[u8]>) -> Result<(), RegistrarError> { - let password_policy = Self::current_policy(&self.password_policy); - - self.clients - .get(client_id) - .ok_or_else(|| { - tracing::debug!("this client is not registered yet: {client_id:?}."); - RegistrarError::Unspecified - }).and_then(|mc| - RegisteredClient::new(&mc.client, password_policy).check_authentication(passphrase) - )?; - - Ok(()) - } -} - diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index b28873920..c13407933 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -272,7 +272,7 @@ impl Service { /// Find out which user an access token belongs to. pub async fn find_from_token(&self, token: &str) -> Result<(OwnedUserId, OwnedDeviceId)> { if self.services.server.config.auth.as_ref().is_some_and(|auth| auth.enable_oidc_login) { - self.services.oidc.user_and_device_from_token(token) + self.services.oidc.user_and_device_from_token(token).await } else { self.db.token_userdeviceid.get(token).await.deserialized() } @@ -542,7 +542,7 @@ impl Service { // either... let key = (user_id, device_id); if self.services.server.config.auth.as_ref().is_some_and(|auth| auth.enable_oidc_login) { - if self.services.oidc.client_from_device_id(device_id.into()).is_none() { + if self.services.oidc.client_from_device_id(device_id.into()).await?.is_none() { return Err!(Database(error!( ?user_id, ?device_id,