From 1664a2c225f489ae74cd22f264a6e1f91c2887e3 Mon Sep 17 00:00:00 2001 From: Jason Volk Date: Thu, 31 Jul 2025 08:19:57 +0000 Subject: [PATCH] Implement refresh-tokens. (resolves #50) Signed-off-by: Jason Volk --- src/api/client/device.rs | 3 +- src/api/client/mod.rs | 2 +- src/api/client/register.rs | 19 +- src/api/client/session/mod.rs | 25 +- src/api/client/session/refresh.rs | 54 +++++ src/api/router.rs | 1 + src/api/router/auth.rs | 132 +++++----- src/core/config/mod.rs | 12 + src/database/maps.rs | 4 + src/service/users/device.rs | 226 ++++++++++++++---- src/service/users/mod.rs | 4 +- .../complement/test_results.jsonl | 2 +- tuwunel-example.toml | 8 + 13 files changed, 364 insertions(+), 128 deletions(-) create mode 100644 src/api/client/session/refresh.rs diff --git a/src/api/client/device.rs b/src/api/client/device.rs index 435cfd92..3f9876eb 100644 --- a/src/api/client/device.rs +++ b/src/api/client/device.rs @@ -100,7 +100,8 @@ pub(crate) async fn update_device_route( .create_device( sender_user, &device_id, - &appservice.registration.as_token, + (&appservice.registration.as_token, None), + None, None, Some(client.to_string()), ) diff --git a/src/api/client/mod.rs b/src/api/client/mod.rs index 1b7ed592..bb761ace 100644 --- a/src/api/client/mod.rs +++ b/src/api/client/mod.rs @@ -89,7 +89,7 @@ pub(super) use well_known::*; const DEVICE_ID_LENGTH: usize = 10; /// generated user access token length -const TOKEN_LENGTH: usize = 32; +const TOKEN_LENGTH: usize = tuwunel_service::users::device::TOKEN_LENGTH; /// generated user session ID length const SESSION_ID_LENGTH: usize = tuwunel_service::uiaa::SESSION_ID_LENGTH; diff --git a/src/api/client/register.rs b/src/api/client/register.rs index aa29bb28..051fbce8 100644 --- a/src/api/client/register.rs +++ b/src/api/client/register.rs @@ -17,8 +17,9 @@ use ruma::{ push, }; use tuwunel_core::{Err, Error, Result, debug_info, error, info, is_equal_to, utils, warn}; +use tuwunel_service::users::device::generate_refresh_token; -use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper}; +use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH, join_room_by_id_helper}; use crate::Ruma; const RANDOM_USER_ID_LENGTH: usize = 10; @@ -432,7 +433,12 @@ pub(crate) async fn register_route( .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into()); // Generate new token for the device - let token = utils::random_string(TOKEN_LENGTH); + let (access_token, expires_in) = services + .users + .generate_access_token(body.body.refresh_token); + + // Generate a new refresh_token if requested by client + let refresh_token = expires_in.is_some().then(generate_refresh_token); // Create device for this account services @@ -440,7 +446,8 @@ pub(crate) async fn register_route( .create_device( &user_id, &device_id, - &token, + (&access_token, expires_in), + refresh_token.as_deref(), body.initial_device_display_name.clone(), Some(client.to_string()), ) @@ -574,11 +581,11 @@ pub(crate) async fn register_route( } Ok(register::v3::Response { - access_token: Some(token), user_id, device_id: Some(device_id), - refresh_token: None, - expires_in: None, + access_token: Some(access_token), + refresh_token, + expires_in, }) } diff --git a/src/api/client/session/mod.rs b/src/api/client/session/mod.rs index 385acff9..18a2fffb 100644 --- a/src/api/client/session/mod.rs +++ b/src/api/client/session/mod.rs @@ -3,6 +3,7 @@ mod jwt; mod ldap; mod logout; mod password; +mod refresh; mod token; use axum::extract::State; @@ -21,10 +22,12 @@ use ruma::api::client::session::{ }, }; use tuwunel_core::{Err, Result, info, utils, utils::stream::ReadyExt}; +use tuwunel_service::users::device::generate_refresh_token; use self::{ldap::ldap_login, password::password_login}; pub(crate) use self::{ logout::{logout_all_route, logout_route}, + refresh::refresh_token_route, token::login_token_route, }; use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH}; @@ -87,7 +90,12 @@ pub(crate) async fn login_route( }; // Generate a new token for the device - let access_token = utils::random_string(TOKEN_LENGTH); + let (access_token, expires_in) = services + .users + .generate_access_token(body.body.refresh_token); + + // Generate a new refresh_token if requested by client + let refresh_token = expires_in.is_some().then(generate_refresh_token); // Generate new device id if the user didn't specify one let device_id = body @@ -108,7 +116,8 @@ pub(crate) async fn login_route( .create_device( &user_id, &device_id, - &access_token, + (&access_token, expires_in), + refresh_token.as_deref(), body.initial_device_display_name.clone(), Some(client.to_string()), ) @@ -116,7 +125,13 @@ pub(crate) async fn login_route( } else { services .users - .set_access_token(&user_id, &device_id, &access_token) + .set_access_token( + &user_id, + &device_id, + &access_token, + expires_in, + refresh_token.as_deref(), + ) .await?; } @@ -141,7 +156,7 @@ pub(crate) async fn login_route( device_id, home_server, well_known, - expires_in: None, - refresh_token: None, + expires_in, + refresh_token, }) } diff --git a/src/api/client/session/refresh.rs b/src/api/client/session/refresh.rs new file mode 100644 index 00000000..fabe2a1f --- /dev/null +++ b/src/api/client/session/refresh.rs @@ -0,0 +1,54 @@ +use axum::extract::State; +use axum_client_ip::InsecureClientIp; +use ruma::api::client::session::refresh_token::v3::{Request, Response}; +use tuwunel_core::{Err, Result, debug_info, err}; +use tuwunel_service::users::device::generate_refresh_token; + +use crate::Ruma; + +/// # `POST /_matrix/client/v3/refresh` +/// +/// Refresh an access token. +/// +/// +#[tracing::instrument(skip_all, fields(%client), name = "refresh_token")] +pub(crate) async fn refresh_token_route( + State(services): State, + InsecureClientIp(client): InsecureClientIp, + body: Ruma, +) -> Result { + let refresh_token_claim = body.body.refresh_token; + + if !refresh_token_claim.starts_with("refresh_") { + return Err!(Request(Forbidden("Refresh token is malformed."))); + } + + let (user_id, device_id, ..) = services + .users + .find_from_token(&refresh_token_claim) + .await + .map_err(|e| err!(Request(Forbidden("Refresh token is unrecognized: {e}"))))?; + + // New tokens + let refresh_token = Some(generate_refresh_token()); + let (access_token, expires_in_ms) = services.users.generate_access_token(true); + + services + .users + .set_access_token( + &user_id, + &device_id, + &access_token, + expires_in_ms, + refresh_token.as_deref(), + ) + .await?; + + debug_info!(?user_id, ?device_id, ?expires_in_ms, "refreshed their access_token",); + + Ok(Response { + access_token, + refresh_token, + expires_in_ms, + }) +} diff --git a/src/api/router.rs b/src/api/router.rs index 91550832..00caf4ac 100644 --- a/src/api/router.rs +++ b/src/api/router.rs @@ -35,6 +35,7 @@ pub fn build(router: Router, server: &Server) -> Router { .ruma_route(&client::get_login_types_route) .ruma_route(&client::login_route) .ruma_route(&client::login_token_route) + .ruma_route(&client::refresh_token_route) .ruma_route(&client::whoami_route) .ruma_route(&client::logout_route) .ruma_route(&client::logout_all_route) diff --git a/src/api/router/auth.rs b/src/api/router/auth.rs index 104acbea..f3df9d03 100644 --- a/src/api/router/auth.rs +++ b/src/api/router/auth.rs @@ -29,7 +29,9 @@ use ruma::{ federation::{authentication::XMatrix, openid::get_openid_userinfo}, }, }; -use tuwunel_core::{Err, Error, Result, debug_error, err, warn}; +use tuwunel_core::{ + Err, Error, Result, debug_error, err, is_less_than, utils::result::LogDebugErr, warn, +}; use tuwunel_service::{ Services, appservice::RegistrationInfo, @@ -40,7 +42,8 @@ use super::request::Request; enum Token { Appservice(Box), - User((OwnedUserId, OwnedDeviceId)), + User((OwnedUserId, OwnedDeviceId, Option)), + Expired((OwnedUserId, OwnedDeviceId)), Invalid, None, } @@ -51,6 +54,7 @@ pub(super) struct Auth { pub(super) sender_user: Option, pub(super) sender_device: Option, pub(super) appservice_info: Option, + pub(super) _expires_at: Option, } #[tracing::instrument( @@ -65,6 +69,11 @@ pub(super) async fn auth( json_body: Option<&CanonicalJsonValue>, metadata: &Metadata, ) -> Result { + use AuthScheme::{AccessToken, AccessTokenOptional, AppserviceToken, ServerSignatures}; + use Error::BadRequest; + use ErrorKind::UnknownToken; + use Token::{Appservice, Expired, Invalid, User}; + let bearer: Option>> = request.parts.extract().await.unwrap_or(None); @@ -73,70 +82,76 @@ pub(super) async fn auth( | None => request.query.access_token.as_deref(), }; - let token = find_token(services, token).await?; + let token = match find_token(services, token).await? { + | User((user_id, device_id, expires_at)) + if expires_at.is_some_and(is_less_than!(SystemTime::now())) => + Expired((user_id, device_id)), + + | token => token, + }; if metadata.authentication == AuthScheme::None { check_auth_still_required(services, metadata, &token)?; } match (metadata.authentication, token) { - | (AuthScheme::AccessToken, Token::Appservice(info)) => - Ok(auth_appservice(services, request, info).await?), - | ( - AuthScheme::None | AuthScheme::AccessTokenOptional | AuthScheme::AppserviceToken, - Token::Appservice(info), - ) => Ok(Auth { - appservice_info: Some(*info), + | (AccessToken, Appservice(info)) => Ok(auth_appservice(services, request, info).await?), + + | (AccessToken | AccessTokenOptional | AuthScheme::None, User(user)) => Ok(Auth { + sender_user: Some(user.0), + sender_device: Some(user.1), + _expires_at: user.2, ..Auth::default() }), - | (AuthScheme::AccessToken, Token::None) => match metadata { - | &get_turn_server_info::v3::Request::METADATA => { - if services.server.config.turn_allow_guests { - Ok(Auth::default()) - } else { - Err!(Request(MissingToken("Missing access token."))) - } - }, + + | (AccessToken, Token::None) => match metadata { + | &get_turn_server_info::v3::Request::METADATA + if services.server.config.turn_allow_guests => + Ok(Auth::default()), + | _ => Err!(Request(MissingToken("Missing access token."))), }, - | ( - AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None, - Token::User((user_id, device_id)), - ) => Ok(Auth { - sender_user: Some(user_id), - sender_device: Some(device_id), - ..Auth::default() - }), - | (AuthScheme::ServerSignatures, Token::None) => - Ok(auth_server(services, request, json_body).await?), - | ( - AuthScheme::None | AuthScheme::AppserviceToken | AuthScheme::AccessTokenOptional, - Token::None, - ) => Ok(Auth::default()), - | (AuthScheme::ServerSignatures, Token::Appservice(_) | Token::User(_)) => - Err!(Request(Unauthorized("Only server signatures should be used on this endpoint."))), - | (AuthScheme::AppserviceToken, Token::User(_)) => Err!(Request(Unauthorized( - "Only appservice access tokens should be used on this endpoint." - ))), - | (AuthScheme::None, Token::Invalid) => { + + | (AppserviceToken, User(_)) => + Err!(Request(Unauthorized("Appservice tokens must be used on this endpoint."))), + + | (ServerSignatures, Appservice(_) | User(_)) => + Err!(Request(Unauthorized("Server signatures must be used on this endpoint."))), + + | (ServerSignatures, Token::None) => Ok(auth_server(services, request, json_body).await?), + + | (AuthScheme::None | AccessTokenOptional | AppserviceToken, Appservice(info)) => + Ok(Auth { + appservice_info: Some(*info), + ..Auth::default() + }), + + | (AuthScheme::None | AccessTokenOptional | AppserviceToken, Token::None) => + Ok(Auth::default()), + + | (AuthScheme::None, Invalid) + if request.query.access_token.is_some() + && metadata == &get_openid_userinfo::v1::Request::METADATA => + { // OpenID federation endpoint uses a query param with the same name, drop this // once query params for user auth are removed from the spec. This is // required to make integration manager work. - if request.query.access_token.is_some() - && metadata == &get_openid_userinfo::v1::Request::METADATA - { - Ok(Auth::default()) - } else { - Err(Error::BadRequest( - ErrorKind::UnknownToken { soft_logout: false }, - "Unknown access token.", - )) - } + Ok(Auth::default()) }, - | (_, Token::Invalid) => Err(Error::BadRequest( - ErrorKind::UnknownToken { soft_logout: false }, - "Unknown access token.", - )), + + | (_, Expired((user_id, device_id))) => { + services + .users + .remove_access_token(&user_id, &device_id) + .await + .log_debug_err() + .ok(); + + Err(BadRequest(UnknownToken { soft_logout: true }, "Expired access token.")) + }, + + | (_, Invalid) => + Err(BadRequest(UnknownToken { soft_logout: false }, "Unknown access token.")), } } @@ -159,7 +174,7 @@ fn check_auth_still_required(services: &Services, metadata: &Metadata, token: &T .require_auth_for_profile_requests => match token { | Token::Appservice(_) | Token::User(_) => Ok(()), - | Token::None | Token::Invalid => + | Token::None | Token::Expired(_) | Token::Invalid => Err!(Request(MissingToken("Missing or invalid access token."))), }, | &get_public_rooms::v3::Request::METADATA @@ -169,7 +184,7 @@ fn check_auth_still_required(services: &Services, metadata: &Metadata, token: &T .allow_public_room_directory_without_auth => match token { | Token::Appservice(_) | Token::User(_) => Ok(()), - | Token::None | Token::Invalid => + | Token::None | Token::Expired(_) | Token::Invalid => Err!(Request(MissingToken("Missing or invalid access token."))), }, | _ => Ok(()), @@ -183,7 +198,7 @@ async fn find_token(services: &Services, token: Option<&str>) -> Result { let user_token = services .users - .find_from_access_token(token) + .find_from_token(token) .map_ok(Token::User); let appservice_token = services @@ -226,10 +241,9 @@ async fn auth_appservice( } Ok(Auth { - origin: None, sender_user: Some(user_id), - sender_device: None, appservice_info: Some(*info), + ..Auth::default() }) } @@ -304,9 +318,7 @@ async fn auth_server( Ok(Auth { origin: origin.to_owned().into(), - sender_user: None, - sender_device: None, - appservice_info: None, + ..Auth::default() }) } diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 48261ddc..595025ac 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -860,6 +860,16 @@ pub struct Config { #[serde(default = "default_login_token_ttl")] pub login_token_ttl: u64, + /// Access token TTL in seconds. + /// + /// For clients that support refresh-tokens, the access-token provided on + /// login will be invalidated after this amount of time and the client will + /// be soft-logged-out until refreshing it. + /// + /// default: 604800 + #[serde(default = "default_access_token_ttl")] + pub access_token_ttl: u64, + /// Static TURN username to provide the client if not using a shared secret /// ("turn_secret"), It is recommended to use a shared secret over static /// credentials. @@ -2675,3 +2685,5 @@ fn default_client_sync_timeout_min() -> u64 { 5000 } fn default_client_sync_timeout_default() -> u64 { 30000 } fn default_client_sync_timeout_max() -> u64 { 90000 } + +fn default_access_token_ttl() -> u64 { 604_800 } diff --git a/src/database/maps.rs b/src/database/maps.rs index 7e832486..4d14d8e4 100644 --- a/src/database/maps.rs +++ b/src/database/maps.rs @@ -346,6 +346,10 @@ pub(super) static MAPS: &[Descriptor] = &[ name: "userdeviceid_metadata", ..descriptor::RANDOM_SMALL }, + Descriptor { + name: "userdeviceid_refresh", + ..descriptor::RANDOM_SMALL + }, Descriptor { name: "userdeviceid_token", ..descriptor::RANDOM_SMALL diff --git a/src/service/users/device.rs b/src/service/users/device.rs index 15aed62b..49fbdf4d 100644 --- a/src/service/users/device.rs +++ b/src/service/users/device.rs @@ -1,6 +1,9 @@ -use std::sync::Arc; +use std::{ + sync::Arc, + time::{Duration, SystemTime}, +}; -use futures::{Stream, StreamExt}; +use futures::{FutureExt, Stream, StreamExt, future::join}; use ruma::{ DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, UserId, api::client::device::Device, events::AnyToDeviceEvent, serde::Raw, @@ -8,17 +11,26 @@ use ruma::{ use serde_json::json; use tuwunel_core::{ Err, Result, at, implement, - utils::{self, ReadyExt, stream::TryIgnore}, + utils::{ + self, ReadyExt, + stream::TryIgnore, + time::{duration_since_epoch, timepoint_from_epoch, timepoint_from_now}, + }, }; use tuwunel_database::{Deserialized, Ignore, Interfix, Json, Map}; +/// generated user access token length +pub const TOKEN_LENGTH: usize = 32; + /// Adds a new device to a user. #[implement(super::Service)] +#[tracing::instrument(level = "debug", skip(self))] pub async fn create_device( &self, user_id: &UserId, device_id: &DeviceId, - token: &str, + (access_token, expires_in): (&str, Option), + refresh_token: Option<&str>, initial_device_display_name: Option, client_ip: Option, ) -> Result { @@ -38,25 +50,16 @@ pub async fn create_device( increment(&self.db.userid_devicelistversion, user_id.as_bytes()); self.db.userdeviceid_metadata.put(key, Json(val)); - self.set_access_token(user_id, device_id, token) + self.set_access_token(user_id, device_id, access_token, expires_in, refresh_token) .await } /// Removes a device from a user. #[implement(super::Service)] +#[tracing::instrument(level = "debug", skip(self))] pub async fn remove_device(&self, user_id: &UserId, device_id: &DeviceId) { - let userdeviceid = (user_id, device_id); - - // Remove tokens - if let Ok(old_token) = self - .db - .userdeviceid_token - .qry(&userdeviceid) - .await - { - self.db.userdeviceid_token.del(userdeviceid); - self.db.token_userdeviceid.remove(&old_token); - } + // Remove access tokens + self.remove_tokens(user_id, device_id).await; // Remove todevice events let prefix = (user_id, device_id, Interfix); @@ -71,6 +74,7 @@ pub async fn remove_device(&self, user_id: &UserId, device_id: &DeviceId) { increment(&self.db.userid_devicelistversion, user_id.as_bytes()); + let userdeviceid = (user_id, device_id); self.db.userdeviceid_metadata.del(userdeviceid); self.mark_device_key_update(user_id).await; } @@ -89,50 +93,97 @@ pub fn all_device_ids<'a>( .map(|(_, device_id): (Ignore, &DeviceId)| device_id) } -/// Replaces the access token of one device. +/// Find out which user an access or refresh token belongs to. #[implement(super::Service)] -pub async fn set_access_token( +#[tracing::instrument(level = "trace", skip(self, token))] +pub async fn find_from_token( &self, - user_id: &UserId, - device_id: &DeviceId, token: &str, -) -> Result { - 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." - ))); - } - - // Remove old token - if let Ok(old_token) = self.db.userdeviceid_token.qry(&key).await { - self.db.token_userdeviceid.remove(&old_token); - // It will be removed from userdeviceid_token by the insert later - } - - // Assign token to user device combination - self.db.userdeviceid_token.put_raw(key, token); - self.db.token_userdeviceid.raw_put(token, key); - - Ok(()) -} - -/// Find out which user an access token belongs to. -#[implement(super::Service)] -pub async fn find_from_access_token(&self, token: &str) -> Result<(OwnedUserId, OwnedDeviceId)> { +) -> Result<(OwnedUserId, OwnedDeviceId, Option)> { self.db .token_userdeviceid .get(token) .await .deserialized() + .and_then(|(user_id, device_id, expires_at): (_, _, Option)| { + let expires_at = expires_at + .map(Duration::from_secs) + .map(timepoint_from_epoch) + .transpose()?; + + Ok((user_id, device_id, expires_at)) + }) +} + +#[implement(super::Service)] +#[tracing::instrument(level = "debug", skip(self))] +pub async fn remove_tokens(&self, user_id: &UserId, device_id: &DeviceId) { + let remove_access = self + .remove_access_token(user_id, device_id) + .map(Result::ok); + + let remove_refresh = self + .remove_refresh_token(user_id, device_id) + .map(Result::ok); + + join(remove_access, remove_refresh).await; +} + +/// Replaces the access token of one device. +#[implement(super::Service)] +#[tracing::instrument(level = "debug", skip(self))] +pub async fn set_access_token( + &self, + user_id: &UserId, + device_id: &DeviceId, + access_token: &str, + expires_in: Option, + refresh_token: Option<&str>, +) -> Result { + if let Some(refresh_token) = refresh_token { + self.set_refresh_token(user_id, device_id, refresh_token) + .await?; + } + + // Remove old token. + self.remove_access_token(user_id, device_id) + .await + .ok(); + + let expires_at = expires_in + .map(timepoint_from_now) + .transpose()? + .map(duration_since_epoch) + .as_ref() + .map(Duration::as_secs); + + let userdeviceid = (user_id, device_id); + let value = (user_id, device_id, expires_at); + self.db + .token_userdeviceid + .raw_put(access_token, value); + self.db + .userdeviceid_token + .put_raw(userdeviceid, access_token); + + Ok(()) +} + +/// Revoke the access token without deleting the device. Take care to not leave +/// dangling devices if using this method. +#[implement(super::Service)] +pub async fn remove_access_token(&self, user_id: &UserId, device_id: &DeviceId) -> Result { + let userdeviceid = (user_id, device_id); + let access_token = self + .db + .userdeviceid_token + .qry(&userdeviceid) + .await?; + + self.db.userdeviceid_token.del(userdeviceid); + self.db.token_userdeviceid.remove(&access_token); + + Ok(()) } #[implement(super::Service)] @@ -145,6 +196,75 @@ pub async fn get_access_token(&self, user_id: &UserId, device_id: &DeviceId) -> .deserialized() } +#[implement(super::Service)] +pub fn generate_access_token(&self, expires: bool) -> (String, Option) { + let access_token = utils::random_string(TOKEN_LENGTH); + let expires_in = expires + .then_some(self.services.server.config.access_token_ttl) + .map(Duration::from_secs); + + (access_token, expires_in) +} + +/// Replaces the refresh token of one device. +#[implement(super::Service)] +#[tracing::instrument(level = "debug", skip(self))] +pub async fn set_refresh_token( + &self, + user_id: &UserId, + device_id: &DeviceId, + refresh_token: &str, +) -> Result { + debug_assert!(refresh_token.starts_with("refresh_"), "refresh_token missing prefix"); + + // Remove old token + self.remove_refresh_token(user_id, device_id) + .await + .ok(); + + let userdeviceid = (user_id, device_id); + self.db + .token_userdeviceid + .raw_put(refresh_token, userdeviceid); + self.db + .userdeviceid_refresh + .put_raw(userdeviceid, refresh_token); + + Ok(()) +} + +/// Revoke the refresh token without deleting the device. Take care to not leave +/// dangling devices if using this method. +#[implement(super::Service)] +pub async fn remove_refresh_token(&self, user_id: &UserId, device_id: &DeviceId) -> Result { + let userdeviceid = (user_id, device_id); + let refresh_token = self + .db + .userdeviceid_refresh + .qry(&userdeviceid) + .await?; + + self.db.userdeviceid_refresh.del(userdeviceid); + self.db.token_userdeviceid.remove(&refresh_token); + + Ok(()) +} + +#[implement(super::Service)] +pub async fn get_refresh_token(&self, user_id: &UserId, device_id: &DeviceId) -> Result { + let key = (user_id, device_id); + self.db + .userdeviceid_refresh + .qry(&key) + .await + .deserialized() +} + +#[must_use] +pub fn generate_refresh_token() -> String { + format!("refresh_{}", utils::random_string(TOKEN_LENGTH)) +} + #[implement(super::Service)] pub async fn add_to_device_event( &self, diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index 7ddf138c..a123aa42 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -1,4 +1,4 @@ -mod device; +pub mod device; mod keys; mod ldap; mod profile; @@ -44,6 +44,7 @@ struct Data { token_userdeviceid: Arc, userdeviceid_metadata: Arc, userdeviceid_token: Arc, + userdeviceid_refresh: Arc, userfilterid_filter: Arc, userid_avatarurl: Arc, userid_blurhash: Arc, @@ -80,6 +81,7 @@ impl crate::Service for Service { token_userdeviceid: args.db["token_userdeviceid"].clone(), userdeviceid_metadata: args.db["userdeviceid_metadata"].clone(), userdeviceid_token: args.db["userdeviceid_token"].clone(), + userdeviceid_refresh: args.db["userdeviceid_refresh"].clone(), userfilterid_filter: args.db["userfilterid_filter"].clone(), userid_avatarurl: args.db["userid_avatarurl"].clone(), userid_blurhash: args.db["userid_blurhash"].clone(), diff --git a/tests/test_results/complement/test_results.jsonl b/tests/test_results/complement/test_results.jsonl index c71f626c..de07b661 100644 --- a/tests/test_results/complement/test_results.jsonl +++ b/tests/test_results/complement/test_results.jsonl @@ -623,7 +623,7 @@ {"Action":"pass","Test":"TestToDeviceMessagesOverFederation"} {"Action":"pass","Test":"TestToDeviceMessagesOverFederation/good_connectivity"} {"Action":"pass","Test":"TestToDeviceMessagesOverFederation/interrupted_connectivity"} -{"Action":"fail","Test":"TestTxnIdWithRefreshToken"} +{"Action":"pass","Test":"TestTxnIdWithRefreshToken"} {"Action":"fail","Test":"TestTxnIdempotency"} {"Action":"pass","Test":"TestTxnIdempotencyScopedToDevice"} {"Action":"pass","Test":"TestTxnInEvent"} diff --git a/tuwunel-example.toml b/tuwunel-example.toml index 51732371..d00576ec 100644 --- a/tuwunel-example.toml +++ b/tuwunel-example.toml @@ -703,6 +703,14 @@ # #login_token_ttl = 120000 +# Access token TTL in seconds. +# +# For clients that support refresh-tokens, the access-token provided on +# login will be invalidated after this amount of time and the client will +# be soft-logged-out until refreshing it. +# +#access_token_ttl = 604800 + # Static TURN username to provide the client if not using a shared secret # ("turn_secret"), It is recommended to use a shared secret over static # credentials.