mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-11-15 05:06:53 +00:00
OIDC: impl client_registrar over db (sync impl)
This commit is contained in:
1 parent
f2e696ae3e
commit
900ba33a4e
7 files changed
+361
-301
No files matched your search
Generated
+106
-2
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
+230
-86
@@ -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<Argon2> = 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<String>,
|
||||
/// A device id that we'll generate on OIDC registration.
|
||||
pub device_id: Option<String>,
|
||||
/// The device's coordinates recorded by oxide-auth.
|
||||
pub client: EncodedClient,
|
||||
}
|
||||
|
||||
struct Services {
|
||||
globals: Dep<globals::Service>,
|
||||
}
|
||||
|
||||
struct Data {
|
||||
client_registrar: Arc<Map>,
|
||||
deviceid_clientidmap: Arc<Map>,
|
||||
userid_devicelistversion: Arc<Map>,
|
||||
userdeviceid_metadata: Arc<Map>,
|
||||
}
|
||||
|
||||
pub struct Service {
|
||||
registrar: Mutex<MatrixClientMap>,
|
||||
/// Authorization tokens are 16 byte random keys to a memory hash map.
|
||||
///
|
||||
/// Will be reinitialised on continuwuity's restart.
|
||||
authorizer: Mutex<AuthMap<RandomGenerator>>,
|
||||
/// 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<TokenMap<RandomGenerator>>,
|
||||
services: Services,
|
||||
db: Data,
|
||||
@@ -61,100 +88,117 @@ pub struct Service {
|
||||
|
||||
#[async_trait]
|
||||
impl crate::Service for Service {
|
||||
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> { Ok(Arc::new(Self::preconfigured(args))) }
|
||||
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
// 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::Service>("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<String>, 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<String>, 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<String>,
|
||||
) -> 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::Service>("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<Grant> {
|
||||
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<MatrixClient> {
|
||||
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<Option<OidcClient>> {
|
||||
self.db
|
||||
.client_registrar
|
||||
.get(client_id)
|
||||
.await?
|
||||
.deserialized()
|
||||
}
|
||||
|
||||
pub fn client_from_device_id(&self, device_id: OwnedDeviceId) -> Option<EncodedClient> {
|
||||
let registrar = self.registrar.lock().expect("lockable registrar");
|
||||
pub async fn client_from_device_id(
|
||||
&self,
|
||||
device_id: OwnedDeviceId,
|
||||
) -> Result<Option<OidcClient>> {
|
||||
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<OwnedDeviceId> {
|
||||
@@ -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<impl Registrar + '_, impl Authorizer + '_, impl Issuer + '_> {
|
||||
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<Map>, key: &[u8]) {
|
||||
let old = db.get_blocking(key);
|
||||
async fn increment(db: &Arc<Map>, 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<BoundClient<'a>, 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<Scope>) -> Result<PreGrant, 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)?;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<Argon2> = Lazy::new(Argon2::default);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MatrixClient {
|
||||
pub name: Option<String>,
|
||||
pub device_id: Option<String>,
|
||||
pub client: EncodedClient,
|
||||
}
|
||||
|
||||
/// A very simple, in-memory hash map of client ids to MatrixClient entries.
|
||||
#[derive(Default)]
|
||||
pub struct MatrixClientMap {
|
||||
clients: HashMap<String, MatrixClient>,
|
||||
password_policy: Option<Box<dyn PasswordPolicy>>,
|
||||
}
|
||||
|
||||
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<String>, 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<P: PasswordPolicy + 'static>(&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<Box<dyn PasswordPolicy>>) -> &'a dyn PasswordPolicy {
|
||||
policy
|
||||
.as_ref()
|
||||
.map(|boxed| &**boxed)
|
||||
.unwrap_or(&*DEFAULT_PASSWORD_POLICY)
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<Client> for MatrixClientMap {
|
||||
fn extend<I>(&mut self, iter: I)
|
||||
where
|
||||
I: IntoIterator<Item = Client>,
|
||||
{
|
||||
iter.into_iter().for_each(|client| self.register_client(None, client))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<Client> for MatrixClientMap {
|
||||
fn from_iter<I>(iter: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = Client>,
|
||||
{
|
||||
let mut into = MatrixClientMap::new();
|
||||
into.extend(iter);
|
||||
into
|
||||
}
|
||||
}
|
||||
|
||||
impl Registrar for MatrixClientMap {
|
||||
fn bound_redirect<'a>(&self, bound: ClientUrl<'a>) -> Result<BoundClient<'a>, 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<Scope>) -> Result<PreGrant, RegistrarError> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user