From b37480d6b2ae4e224d7f4d18b8db8fad8ad6016c Mon Sep 17 00:00:00 2001 From: Ginger Date: Thu, 30 Apr 2026 16:46:00 -0400 Subject: [PATCH] fix: Return the correct error code for expired access tokens --- src/api/router/auth.rs | 28 ++++++++++++++++++++++++---- src/service/appservice/mod.rs | 2 +- src/service/oauth/mod.rs | 6 ++++-- src/service/users/account.rs | 19 ++++++++++++++----- src/service/users/mod.rs | 1 + 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/api/router/auth.rs b/src/api/router/auth.rs index f6f7fa496..72fb12017 100644 --- a/src/api/router/auth.rs +++ b/src/api/router/auth.rs @@ -1,6 +1,7 @@ use std::any::{Any, TypeId}; -use conduwuit::{Err, Result, err}; +use conduwuit::{Err, Error, Result, err}; +use http::StatusCode; use ruma::{ DeviceId, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId, api::{ @@ -10,12 +11,15 @@ use ruma::{ AuthScheme, NoAccessToken, NoAuthentication, }, client, + error::{ErrorKind, UnknownTokenErrorData}, federation::authentication::ServerSignatures, }, + assign, }; use service::{ Services, server_keys::{PubKeyMap, PubKeys}, + users::AccessTokenStatus, }; use crate::{router::args::AuthQueryParams, service::appservice::RegistrationInfo}; @@ -165,8 +169,20 @@ impl CheckAuth for AccessToken { if output.is_empty() { return Err!(Request(Unauthorized("Missing access token."))); } - if let Some((sender_user, sender_device)) = services.users.find_from_token(&output).await + if let Some((sender_user, sender_device, status)) = + services.users.find_from_token(&output).await { + // If the token is expired we return a soft logout + if matches!(status, AccessTokenStatus::Expired) { + return Err(Error::Request( + ErrorKind::UnknownToken( + assign!(UnknownTokenErrorData::new(), { soft_logout: true }), + ), + "This token has expired".into(), + StatusCode::UNAUTHORIZED, + )); + } + // Locked users can only use /logout and /logout/all if services .users @@ -177,7 +193,7 @@ impl CheckAuth for AccessToken { if !(route == TypeId::of::() || route == TypeId::of::()) { - return Err!(Request(Unauthorized("Your account is locked."))); + return Err!(Request(UserLocked("Your account is locked."))); } } @@ -228,7 +244,11 @@ impl CheckAuth for AccessToken { appservice_info: Box::new(appservice_info), }) } else { - Err!(Request(Unauthorized("Invalid access token."))) + Err(Error::Request( + ErrorKind::UnknownToken(UnknownTokenErrorData::new()), + "Invalid token".into(), + StatusCode::UNAUTHORIZED, + )) } } } diff --git a/src/service/appservice/mod.rs b/src/service/appservice/mod.rs index cf37520d8..ebd73d8af 100644 --- a/src/service/appservice/mod.rs +++ b/src/service/appservice/mod.rs @@ -67,7 +67,7 @@ impl crate::Service for Service { for (id, registration) in appservices { // During startup, resolve any token collisions in favour of appservices // by logging out conflicting user devices - if let Some((user_id, device_id)) = self + if let Some((user_id, device_id, _)) = self .services .users .find_from_token(®istration.as_token) diff --git a/src/service/oauth/mod.rs b/src/service/oauth/mod.rs index 17e7c7c83..28cdfea77 100644 --- a/src/service/oauth/mod.rs +++ b/src/service/oauth/mod.rs @@ -314,8 +314,10 @@ impl Service { .deserialized::() { (refresh_token_info.user_id, refresh_token_info.device_id) - } else if let Some(user) = self.services.users.find_from_token(&token).await { - user + } else if let Some((user_id, device_id, _)) = + self.services.users.find_from_token(&token).await + { + (user_id, device_id) } else { return Err!("Invalid token"); }; diff --git a/src/service/users/account.rs b/src/service/users/account.rs index 2c034882d..191b3f8ea 100644 --- a/src/service/users/account.rs +++ b/src/service/users/account.rs @@ -14,6 +14,12 @@ use ruminuwuity::invite_permission_config::{FilterLevel, InvitePermissionConfigE use crate::users::{HashedPassword, UserSuspension}; +/// The status of an access token. +pub enum AccessTokenStatus { + Valid, + Expired, +} + impl super::Service { /// Returns true/false based on whether the recipient/receiving user has /// ignored the sender. @@ -242,10 +248,13 @@ impl super::Service { /// Find out which user an access token belongs to. Will panic if the access /// token is empty. - pub async fn find_from_token(&self, token: &str) -> Option<(OwnedUserId, OwnedDeviceId)> { + pub async fn find_from_token( + &self, + token: &str, + ) -> Option<(OwnedUserId, OwnedDeviceId, AccessTokenStatus)> { assert!(!token.is_empty(), "Empty access token"); - let user = self + let (user_id, device_id) = self .db .token_userdeviceid .get(token) @@ -257,7 +266,7 @@ impl super::Service { if let Some(expires) = self .db .userdeviceid_tokenexpires - .qry(&user) + .qry(&(&user_id, &device_id)) .await .deserialized::() .ok() @@ -268,11 +277,11 @@ impl super::Service { .expect("expiry time should not overflow SystemTime"); if SystemTime::now() > expires_at { - return None; + return Some((user_id, device_id, AccessTokenStatus::Expired)); } } - Some(user) + Some((user_id, device_id, AccessTokenStatus::Valid)) } /// Returns an iterator over all users on this homeserver. diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index d9960e2e5..813ffda5f 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -7,6 +7,7 @@ pub(super) mod profile; use std::{mem, sync::Arc}; +pub use account::AccessTokenStatus; use conduwuit::{ Err, Error, Result, Server, err, utils::{self},