OIDC: impl client_registrar over db (sync impl)

This commit is contained in:
lafleur authored and Ginger committed 2025-11-11 11:36:28 -05:00
1 parent f2e696ae3e
commit 900ba33a4e
7 files changed
+361 -301

No files matched your search

+11 -6
View File
@@ -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
+4 -4
View File
@@ -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()), &registerable)?;
tracing::trace!("registering OIDC device : {registration:#?}");
services.oidc.register_client(Some(client.client_name.clone()), &registration);
let client_response = ClientResponse {
client_id: device_id.to_string(),
+8
View File
@@ -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
View File
@@ -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)
}
}
-201
View File
@@ -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(())
}
}
+2 -2
View File
@@ -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,