From 9a3496ae70b10443aeb7e32f97d95732e9f9015a Mon Sep 17 00:00:00 2001 From: Ginger Date: Sat, 25 Jul 2026 17:15:08 -0400 Subject: [PATCH] feat: Add admin command to issue access tokens --- src/admin/token/commands.rs | 2 +- src/admin/token/mod.rs | 2 +- src/admin/user/commands.rs | 25 ++++++++++++++++++++++- src/admin/user/mod.rs | 9 +++++++++ src/api/client/account/mod.rs | 1 - src/api/client/account/register.rs | 17 ++++------------ src/api/client/device.rs | 2 +- src/api/client/mod.rs | 3 --- src/api/client/session.rs | 27 ++++++++++--------------- src/service/oauth/mod.rs | 28 +++++++++++++------------- src/service/users/dehydrated_device.rs | 2 +- src/service/users/device.rs | 19 +++++++++++------ 12 files changed, 79 insertions(+), 58 deletions(-) diff --git a/src/admin/token/commands.rs b/src/admin/token/commands.rs index 21882bc32..b33542fc1 100644 --- a/src/admin/token/commands.rs +++ b/src/admin/token/commands.rs @@ -3,7 +3,7 @@ use futures::StreamExt; use service::registration_tokens::TokenExpires; impl crate::Context<'_> { - pub(super) async fn issue_token(&self, expires: super::TokenExpires) -> Result { + pub(super) async fn issue_registration_token(&self, expires: super::TokenExpires) -> Result { let expires = { if expires.immortal { None diff --git a/src/admin/token/mod.rs b/src/admin/token/mod.rs index 9265a2f5f..f274e55d5 100644 --- a/src/admin/token/mod.rs +++ b/src/admin/token/mod.rs @@ -10,7 +10,7 @@ use crate::admin_command_dispatch; pub enum TokenCommand { /// Issue a new registration token #[clap(name = "issue")] - IssueToken { + IssueRegistrationToken { /// When this token will expire. #[command(flatten)] expires: TokenExpires, diff --git a/src/admin/user/commands.rs b/src/admin/user/commands.rs index 65a637088..f3202adc0 100644 --- a/src/admin/user/commands.rs +++ b/src/admin/user/commands.rs @@ -20,7 +20,7 @@ use ruma::{ tag::{TagEvent, TagEventContent, TagInfo}, }, }; -use service::users::{AccountStatus, HashedPassword}; +use service::users::{AccountStatus, DeviceToken, HashedPassword}; use crate::{ get_room_info, @@ -66,6 +66,29 @@ impl crate::Context<'_> { .await } + pub(super) async fn issue_access_token(&self, username: String, password: String) -> Result { + let user_id = parse_active_local_user_id(self.services, &username).await?; + + let user_id = self + .services + .users + .check_password(&user_id, &password) + .await?; + let token = DeviceToken::new_random(); + + let device_id = self + .services + .users + .create_device(&user_id, None, Some(token.clone()), None, None) + .await?; + + self.write_str(&format!( + "Created device `{device_id}` with access token `{}` for {user_id}", + token.into_token() + )) + .await + } + pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) -> Result { // Validate user id let user_id = parse_local_user_id(self.services, &user_id)?; diff --git a/src/admin/user/mod.rs b/src/admin/user/mod.rs index c5278505a..71c6345b9 100644 --- a/src/admin/user/mod.rs +++ b/src/admin/user/mod.rs @@ -18,6 +18,15 @@ pub enum UserCommand { password: Option, }, + /// Issue an access token for a user. This command will not work on + /// shadow users, such as appservice puppets or accounts imported from + /// an identity provider. + #[clap(name = "issue-token")] + IssueAccessToken { + username: String, + password: String, + }, + /// Reset user password ResetPassword { /// Log out existing sessions diff --git a/src/api/client/account/mod.rs b/src/api/client/account/mod.rs index be7d46bfa..bd2d94110 100644 --- a/src/api/client/account/mod.rs +++ b/src/api/client/account/mod.rs @@ -25,7 +25,6 @@ use ruma::{ }; use service::{mailer::messages, uiaa::UiaaInitiator, users::HashedPassword}; -use super::DEVICE_ID_LENGTH; use crate::{Ruma, router::ClientIdentity}; pub(crate) mod register; diff --git a/src/api/client/account/register.rs b/src/api/client/account/register.rs index df5ee04db..2c988862b 100644 --- a/src/api/client/account/register.rs +++ b/src/api/client/account/register.rs @@ -1,10 +1,7 @@ use std::collections::HashMap; use axum::extract::State; -use conduwuit::{ - Err, Result, debug_info, info, - utils::{self}, -}; +use conduwuit::{Err, Result, debug_info, info}; use conduwuit_service::Services; use futures::StreamExt; use lettre::{Address, message::Mailbox}; @@ -24,7 +21,6 @@ use service::{ users::{DeviceToken, HashedPassword}, }; -use super::DEVICE_ID_LENGTH; use crate::{Ruma, client_ip::ClientIp}; /// # `POST /_matrix/client/v3/register` @@ -114,26 +110,21 @@ pub(crate) async fn register_route( ))); } - // Generate new device id if the user didn't specify one - let device_id = body - .device_id - .clone() - .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into()); - // Generate new token for the device let new_token = DeviceToken::new_random(); // Create device for this account - services + let device_id = services .users .create_device( &user_id, - &device_id, + body.device_id.clone(), Some(new_token.clone()), body.initial_device_display_name.clone(), Some(client.to_string()), ) .await?; + (Some(new_token), Some(device_id)) } else { // Don't create a device for inhibited logins diff --git a/src/api/client/device.rs b/src/api/client/device.rs index 99d03d972..843532581 100644 --- a/src/api/client/device.rs +++ b/src/api/client/device.rs @@ -88,7 +88,7 @@ pub(crate) async fn update_device_route( .users .create_device( sender_user, - &body.device_id, + Some(body.device_id.clone()), None, body.display_name.clone(), Some(client.to_string()), diff --git a/src/api/client/mod.rs b/src/api/client/mod.rs index 32438a88c..c0bb2dc98 100644 --- a/src/api/client/mod.rs +++ b/src/api/client/mod.rs @@ -90,8 +90,5 @@ pub(super) use user_directory::*; pub(super) use voip::*; pub(super) use well_known::*; -/// generated device ID length -const DEVICE_ID_LENGTH: usize = 10; - /// generated user access token length const TOKEN_LENGTH: usize = 32; diff --git a/src/api/client/session.rs b/src/api/client/session.rs index bb4f77957..c88bcea21 100644 --- a/src/api/client/session.rs +++ b/src/api/client/session.rs @@ -3,7 +3,7 @@ use std::time::Duration; use axum::extract::State; use conduwuit::{ Err, Result, debug, err, info, - utils::{self, ReadyExt, stream::BroadbandExt}, + utils::{ReadyExt, stream::BroadbandExt}, warn, }; use conduwuit_service::Services; @@ -30,7 +30,6 @@ use ruma::{ }; use service::users::DeviceToken; -use super::DEVICE_ID_LENGTH; use crate::{Ruma, client_ip::ClientIp}; /// # `GET /_matrix/client/v3/login` @@ -189,43 +188,39 @@ pub(crate) async fn login_route( }, }; - // Generate new device id if the user didn't specify one - let device_id = body - .device_id - .clone() - .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into()); - // Generate a new token for the device let token = DeviceToken::new_random(); // Determine if device_id was provided and exists in the db for this user - let device_exists = if body.device_id.is_some() { + let existing_device_id = if let Some(device_id) = &body.device_id { services .users .all_device_ids(&user_id) - .ready_any(|v| v == device_id) + .ready_find(|v| v == device_id) .await } else { - false + None }; - if device_exists { + let device_id = if let Some(existing_device_id) = existing_device_id { services .users - .set_token(&user_id, &device_id, token.clone()) + .set_token(&user_id, &existing_device_id, token.clone()) .await?; + + existing_device_id } else { services .users .create_device( &user_id, - &device_id, + body.device_id.clone(), Some(token.clone()), body.initial_device_display_name.clone(), Some(client.to_string()), ) - .await?; - } + .await? + }; // send client well-known if specified so the client knows to reconfigure itself let client_discovery_info: Option = services diff --git a/src/service/oauth/mod.rs b/src/service/oauth/mod.rs index a999810ce..06f455fba 100644 --- a/src/service/oauth/mod.rs +++ b/src/service/oauth/mod.rs @@ -547,7 +547,7 @@ impl Service { .iter() .find_map(|scope| { if let Scope::Device(device_id) = scope { - Some(device_id) + Some(device_id.to_owned()) } else { None } @@ -557,7 +557,7 @@ impl Service { if self .services .users - .get_device_metadata(&authorizing_user, device_id) + .get_device_metadata(&authorizing_user, &device_id) .await .is_ok() { @@ -567,11 +567,11 @@ impl Service { )); } - self.services + let device_id = self.services .users .create_device( &authorizing_user, - device_id, + Some(device_id), Some(access_token.clone()), client_name, None, @@ -581,8 +581,16 @@ impl Service { // failure during authentication, which should(?) be impossible(?) .expect("failed to create device"); + info!( + ?client_id, + ?authorizing_user, + ?device_id, + ?requested_scopes, + "Created new oauth session" + ); + self.db.userdeviceid_oauthsessioninfo.put( - (&authorizing_user, device_id), + (&authorizing_user, &device_id), Json(SessionInfo { client_id: client_id.clone(), current_refresh_token: refresh_token.clone(), @@ -595,18 +603,10 @@ impl Service { Json(RefreshTokenInfo { client_id: client_id.clone(), user_id: authorizing_user.clone(), - device_id: device_id.to_owned(), + device_id, }), ); - info!( - ?client_id, - ?authorizing_user, - ?device_id, - ?requested_scopes, - "Created new oauth session" - ); - Ok(TokenResponse { access_token: access_token.into_token(), token_type: TokenType::Bearer, diff --git a/src/service/users/dehydrated_device.rs b/src/service/users/dehydrated_device.rs index 9d4448bc0..83ab1b343 100644 --- a/src/service/users/dehydrated_device.rs +++ b/src/service/users/dehydrated_device.rs @@ -40,7 +40,7 @@ impl super::Service { self.create_device( user_id, - &request.device_id, + Some(request.device_id.clone()), None, request.initial_device_display_name.clone(), None, diff --git a/src/service/users/device.rs b/src/service/users/device.rs index af4273d8a..3ad105dae 100644 --- a/src/service/users/device.rs +++ b/src/service/users/device.rs @@ -42,32 +42,39 @@ impl DeviceToken { impl super::Service { /// Adds a new device to a user. /// + /// If no `device_id` is provided, a random one will be generated. + /// /// If no `token` is provided, the device will not be able to be logged /// into. pub async fn create_device( &self, user_id: &UserId, - device_id: &DeviceId, + device_id: Option, token: Option, initial_device_display_name: Option, client_ip: Option, - ) -> Result<()> { + ) -> Result { + const DEVICE_ID_LENGTH: usize = 10; + self.status(user_id).await.ensure_active()?; - let key = (user_id, device_id); - let mut device = Device::new(device_id.into()); + let device_id = + device_id.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into()); + + let mut device = Device::new(device_id.clone()); device.display_name = initial_device_display_name; device.last_seen_ip = client_ip; device.last_seen_ts = Some(MilliSecondsSinceUnixEpoch::now()); + let key = (user_id, &device_id); increment(&self.db.userid_devicelistversion, user_id.as_bytes()); self.db.userdeviceid_metadata.put(key, Json(device)); if let Some(token) = token { - self.set_token(user_id, device_id, token).await?; + self.set_token(user_id, &device_id, token).await?; } - Ok(()) + Ok(device_id) } /// Removes a device from a user.