mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-11-15 05:06:53 +00:00
oidc: implement registering devices
This commit is contained in:
1 parent
3bbbfcdd46
commit
600b8cb366
6 files changed
+181
-52
No files matched your search
+92
-14
@@ -13,30 +13,50 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use conduwuit::{Result, err};
|
||||
use conduwuit_core::utils;
|
||||
use oxide_auth::{
|
||||
endpoint::Scope,
|
||||
frontends::simple::endpoint::{Generic, Vacant},
|
||||
primitives::{
|
||||
grant::Grant,
|
||||
prelude::{
|
||||
AuthMap, Authorizer, Client, Issuer, RandomGenerator, Registrar, TokenMap,
|
||||
},
|
||||
registrar::RegisteredUrl,
|
||||
},
|
||||
registrar::{EncodedClient, RegisteredUrl},
|
||||
grant::Grant,
|
||||
}
|
||||
};
|
||||
use registrar::ClientMap;
|
||||
use ruma::{OwnedDeviceId, OwnedUserId, UserId};
|
||||
use registrar::MatrixClientMap;
|
||||
use ruma::{
|
||||
api::client::device::Device,
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
OwnedDeviceId,
|
||||
OwnedUserId,
|
||||
UserId,
|
||||
};
|
||||
use database::{Json, Map};
|
||||
|
||||
use crate::{globals, oidc::registrar::MatrixClient, 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:";
|
||||
|
||||
use crate::{globals, Dep};
|
||||
|
||||
struct Services {
|
||||
globals: Dep<globals::Service>,
|
||||
}
|
||||
|
||||
struct Data {
|
||||
userid_devicelistversion: Arc<Map>,
|
||||
userdeviceid_metadata: Arc<Map>,
|
||||
}
|
||||
|
||||
pub struct Service {
|
||||
registrar: Mutex<ClientMap>,
|
||||
registrar: Mutex<MatrixClientMap>,
|
||||
authorizer: Mutex<AuthMap<RandomGenerator>>,
|
||||
issuer: Mutex<TokenMap<RandomGenerator>>,
|
||||
services: Services,
|
||||
db: Data,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -47,11 +67,34 @@ impl crate::Service for Service {
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub fn register_client(&self, client: &Client) -> Result<()> {
|
||||
/// 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(client.clone());
|
||||
.register_client(name.clone(), client.clone());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register a device in the main continuwuity database. Should only be called once the client
|
||||
/// and the user both have authenticated.
|
||||
pub fn register_device(
|
||||
&self,
|
||||
(user_id, device_id): (&OwnedUserId, &OwnedDeviceId),
|
||||
display_name: Option<&str>,
|
||||
client_ip: Option<String>,
|
||||
) -> Result<()> {
|
||||
let key = (user_id, device_id);
|
||||
let val = 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));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -82,6 +125,10 @@ impl Service {
|
||||
services: Services {
|
||||
globals: args.depend::<globals::Service>("globals"),
|
||||
},
|
||||
db: Data {
|
||||
userid_devicelistversion: args.db["userid_devicelistversion"].clone(),
|
||||
userdeviceid_metadata: args.db["userdeviceid_metadata"].clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,8 +138,36 @@ impl Service {
|
||||
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 fn client_from_device_id(&self, device_id: OwnedDeviceId) -> Option<EncodedClient> {
|
||||
let registrar = self.registrar.lock().expect("lockable registrar");
|
||||
|
||||
registrar.find_device(device_id.as_str()).cloned()
|
||||
}
|
||||
|
||||
pub fn device_id_from_scope(&self, scope: Scope) -> Result<OwnedDeviceId> {
|
||||
let Some(device_id) = scope
|
||||
.iter()
|
||||
.find(|s| s.starts_with(SCOPE_PREFIX_DEVICE)) else {
|
||||
tracing::warn!("device_id not found in scope {scope:?}");
|
||||
return Err(err!(Request(InvalidParam("something went wrong with the scope"))));
|
||||
};
|
||||
let device_id = device_id.replace(SCOPE_PREFIX_DEVICE, "");
|
||||
|
||||
OwnedDeviceId::try_from(device_id.clone())
|
||||
.map_err(|err|
|
||||
err!(Request(InvalidParam("invalid device_id {device_id:?}: {err}")))
|
||||
)
|
||||
}
|
||||
|
||||
pub 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 {
|
||||
let Some(Grant { owner_id, scope, .. }) = self.grant_from_token(token) else {
|
||||
return Err(err!(Request(MissingToken("unknown token: {token:?}"))));
|
||||
};
|
||||
let server_name = self.services.globals.server_name();
|
||||
@@ -100,10 +175,7 @@ impl Service {
|
||||
.map_err(|err|
|
||||
err!(Request(InvalidUsername("invalid username {owner_id:?}: {err}")))
|
||||
)?;
|
||||
let device_id = OwnedDeviceId::try_from(client_id.clone())
|
||||
.map_err(|err|
|
||||
err!(Request(InvalidParam("invalid client_id {client_id:?}: {err}")))
|
||||
)?;
|
||||
let device_id = self.device_id_from_scope(scope)?;
|
||||
|
||||
Ok((owner_id, device_id))
|
||||
}
|
||||
@@ -124,3 +196,9 @@ impl Service {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn increment(db: &Arc<Map>, key: &[u8]) {
|
||||
let old = db.get_blocking(key);
|
||||
let new = utils::increment(old.ok().as_deref());
|
||||
db.insert(key, new);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ use url::Url;
|
||||
use std::collections::HashMap;
|
||||
use std::iter::{Extend, FromIterator};
|
||||
|
||||
use crate::oidc::SCOPE_PREFIX_DEVICE;
|
||||
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};
|
||||
@@ -35,26 +36,50 @@ pub fn normalize_redirect(url: Url) -> RegisteredUrl {
|
||||
|
||||
static DEFAULT_PASSWORD_POLICY: Lazy<Argon2> = Lazy::new(Argon2::default);
|
||||
|
||||
/// A very simple, in-memory hash map of client ids to Client entries.
|
||||
#[derive(Clone)]
|
||||
pub struct MatrixClient {
|
||||
pub name: Option<String>,
|
||||
// scope option ?,
|
||||
pub client: EncodedClient,
|
||||
}
|
||||
|
||||
/// A very simple, in-memory hash map of client ids to MatrixClient entries.
|
||||
#[derive(Default)]
|
||||
pub struct ClientMap {
|
||||
clients: HashMap<String, EncodedClient>,
|
||||
pub struct MatrixClientMap {
|
||||
clients: HashMap<String, MatrixClient>,
|
||||
password_policy: Option<Box<dyn PasswordPolicy>>,
|
||||
}
|
||||
|
||||
impl ClientMap {
|
||||
impl MatrixClientMap {
|
||||
/// Create an empty map without any clients in it.
|
||||
pub fn new() -> ClientMap {
|
||||
ClientMap::default()
|
||||
pub fn new() -> MatrixClientMap {
|
||||
MatrixClientMap::default()
|
||||
}
|
||||
|
||||
/// Insert or update the client record.
|
||||
pub fn register_client(&mut self, client: Client) {
|
||||
/// Insert or update the client record with an oxide-auth Client. It will be wrapped in a
|
||||
/// MatrixClient.
|
||||
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);
|
||||
self.clients.insert(client.client_id.clone(), client);
|
||||
let matrix_client = MatrixClient { name, client: client.clone() };
|
||||
self.clients.insert(client.client_id.clone(), matrix_client);
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let scope = format!("{SCOPE_PREFIX_DEVICE}:{device_id}");
|
||||
|
||||
self.clients
|
||||
.values()
|
||||
.find(|c| c.client.default_scope.iter().any(|s| s == scope))
|
||||
.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))
|
||||
@@ -76,34 +101,34 @@ impl ClientMap {
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<Client> for ClientMap {
|
||||
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(client))
|
||||
iter.into_iter().for_each(|client| self.register_client(None, client))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<Client> for ClientMap {
|
||||
impl FromIterator<Client> for MatrixClientMap {
|
||||
fn from_iter<I>(iter: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = Client>,
|
||||
{
|
||||
let mut into = ClientMap::new();
|
||||
let mut into = MatrixClientMap::new();
|
||||
into.extend(iter);
|
||||
into
|
||||
}
|
||||
}
|
||||
|
||||
impl Registrar for ClientMap {
|
||||
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,
|
||||
Some(stored) => &stored.client,
|
||||
};
|
||||
|
||||
// Perform exact matching as motivated in the rfc, but substitute "127.0.0.1" and
|
||||
@@ -112,7 +137,7 @@ impl Registrar for ClientMap {
|
||||
let normalized_uri = redirect_uri
|
||||
.clone()
|
||||
.map(|u| normalize_redirect(u.to_url()));
|
||||
let registered_url = match normalized_uri {
|
||||
let redirect_uri = match normalized_uri {
|
||||
None => client.redirect_uri.clone(),
|
||||
Some(url) => {
|
||||
let original = std::iter::once(&client.redirect_uri);
|
||||
@@ -132,16 +157,17 @@ impl Registrar for ClientMap {
|
||||
|
||||
Ok(BoundClient {
|
||||
client_id: bound.client_id,
|
||||
redirect_uri: Cow::Owned(registered_url),
|
||||
redirect_uri: Cow::Owned(redirect_uri),
|
||||
})
|
||||
}
|
||||
|
||||
/// Always overrides the scope with a default scope.
|
||||
fn negotiate(&self, bound: BoundClient<'_>, _scope: Option<Scope>) -> Result<PreGrant, RegistrarError> {
|
||||
let client = self
|
||||
let client = &self
|
||||
.clients
|
||||
.get(bound.client_id.as_ref())
|
||||
.expect("Bound client appears to not have been constructed with this registrar");
|
||||
.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(),
|
||||
@@ -157,8 +183,8 @@ impl Registrar for ClientMap {
|
||||
.ok_or_else(|| {
|
||||
tracing::debug!("this client is not registered yet: {client_id:?}.");
|
||||
RegistrarError::Unspecified
|
||||
}).and_then(|client|
|
||||
RegisteredClient::new(client, password_policy).check_authentication(passphrase)
|
||||
}).and_then(|mc|
|
||||
RegisteredClient::new(&mc.client, password_policy).check_authentication(passphrase)
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -541,12 +541,22 @@ impl Service {
|
||||
// Only existing devices should be able to call this, but we shouldn't assert
|
||||
// either...
|
||||
let key = (user_id, device_id);
|
||||
if self.db.userdeviceid_metadata.qry(&key).await.is_err() {
|
||||
return Err!(Database(error!(
|
||||
?user_id,
|
||||
?device_id,
|
||||
"User does not exist or device has no metadata."
|
||||
)));
|
||||
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() {
|
||||
return Err!(Database(error!(
|
||||
?user_id,
|
||||
?device_id,
|
||||
"Device has no metadata."
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
if self.db.userdeviceid_metadata.qry(&key).await.is_err() {
|
||||
return Err!(Database(error!(
|
||||
?user_id,
|
||||
?device_id,
|
||||
"User does not exist or device has no metadata."
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let mut key = user_id.as_bytes().to_vec();
|
||||
|
||||
Reference in new issue
Block a user