Skip to content

Commit f2ef058

Browse files
authored
Experimental feature to automatically expire inactive sessions (#4022)
Fixes #1875 This adds an experimental feature which allows expiring sessions that are inactive for a certain amount of time. It runs as a scheduled task every 15 minutes, checking for the 'last activity' on each session type. It processes sessions by batches of 100 at a time, to avoid overloading Synapse when syncing back the database. It expires: - all user (browser) sessions - all compatibility sessions - oauth sessions which are: - for a user - using a 'dynamic' client (so the sessions started from clients defined in the config are excluded)
2 parents 871000f + 7bfb1a1 commit f2ef058

File tree

18 files changed

+725
-7
lines changed

18 files changed

+725
-7
lines changed

crates/cli/src/commands/server.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ impl Options {
166166
&mailer,
167167
homeserver_connection.clone(),
168168
url_builder.clone(),
169+
&site_config,
169170
shutdown.soft_shutdown_token(),
170171
shutdown.task_tracker(),
171172
)

crates/cli/src/commands/worker.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ impl Options {
7373
&mailer,
7474
conn,
7575
url_builder,
76+
&site_config,
7677
shutdown.soft_shutdown_token(),
7778
shutdown.task_tracker(),
7879
)

crates/cli/src/util.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use mas_config::{
1212
EmailTransportKind, ExperimentalConfig, MatrixConfig, PasswordsConfig, PolicyConfig,
1313
TemplatesConfig,
1414
};
15-
use mas_data_model::SiteConfig;
15+
use mas_data_model::{SessionExpirationConfig, SiteConfig};
1616
use mas_email::{MailTransport, Mailer};
1717
use mas_handlers::passwords::PasswordManager;
1818
use mas_policy::PolicyFactory;
@@ -180,6 +180,15 @@ pub fn site_config_from_config(
180180
captcha_config: &CaptchaConfig,
181181
) -> Result<SiteConfig, anyhow::Error> {
182182
let captcha = captcha_config_from_config(captcha_config)?;
183+
let session_expiration = experimental_config
184+
.inactive_session_expiration
185+
.as_ref()
186+
.map(|c| SessionExpirationConfig {
187+
oauth_session_inactivity_ttl: c.expire_oauth_sessions.then_some(c.ttl),
188+
compat_session_inactivity_ttl: c.expire_compat_sessions.then_some(c.ttl),
189+
user_session_inactivity_ttl: c.expire_user_sessions.then_some(c.ttl),
190+
});
191+
183192
Ok(SiteConfig {
184193
access_token_ttl: experimental_config.access_token_ttl,
185194
compat_token_ttl: experimental_config.compat_token_ttl,
@@ -198,6 +207,7 @@ pub fn site_config_from_config(
198207
&& account_config.password_recovery_enabled,
199208
captcha,
200209
minimum_password_complexity: password_config.minimum_complexity(),
210+
session_expiration,
201211
})
202212
}
203213

crates/config/src/sections/experimental.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ use serde_with::serde_as;
1111

1212
use crate::ConfigurationSection;
1313

14+
fn default_true() -> bool {
15+
true
16+
}
17+
1418
fn default_token_ttl() -> Duration {
1519
Duration::microseconds(5 * 60 * 1000 * 1000)
1620
}
@@ -19,11 +23,32 @@ fn is_default_token_ttl(value: &Duration) -> bool {
1923
*value == default_token_ttl()
2024
}
2125

26+
/// Configuration options for the inactive session expiration feature
27+
#[serde_as]
28+
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)]
29+
pub struct InactiveSessionExpirationConfig {
30+
/// Time after which an inactive session is automatically finished
31+
#[schemars(with = "u64", range(min = 600, max = 7_776_000))]
32+
#[serde_as(as = "serde_with::DurationSeconds<i64>")]
33+
pub ttl: Duration,
34+
35+
/// Should compatibility sessions expire after inactivity
36+
#[serde(default = "default_true")]
37+
pub expire_compat_sessions: bool,
38+
39+
/// Should OAuth 2.0 sessions expire after inactivity
40+
#[serde(default = "default_true")]
41+
pub expire_oauth_sessions: bool,
42+
43+
/// Should user sessions expire after inactivity
44+
#[serde(default = "default_true")]
45+
pub expire_user_sessions: bool,
46+
}
47+
2248
/// Configuration sections for experimental options
2349
///
2450
/// Do not change these options unless you know what you are doing.
2551
#[serde_as]
26-
#[allow(clippy::struct_excessive_bools)]
2752
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)]
2853
pub struct ExperimentalConfig {
2954
/// Time-to-live of access tokens in seconds. Defaults to 5 minutes.
@@ -44,20 +69,29 @@ pub struct ExperimentalConfig {
4469
)]
4570
#[serde_as(as = "serde_with::DurationSeconds<i64>")]
4671
pub compat_token_ttl: Duration,
72+
73+
/// Experimetal feature to automatically expire inactive sessions
74+
///
75+
/// Disabled by default
76+
#[serde(skip_serializing_if = "Option::is_none")]
77+
pub inactive_session_expiration: Option<InactiveSessionExpirationConfig>,
4778
}
4879

4980
impl Default for ExperimentalConfig {
5081
fn default() -> Self {
5182
Self {
5283
access_token_ttl: default_token_ttl(),
5384
compat_token_ttl: default_token_ttl(),
85+
inactive_session_expiration: None,
5486
}
5587
}
5688
}
5789

5890
impl ExperimentalConfig {
5991
pub(crate) fn is_default(&self) -> bool {
60-
is_default_token_ttl(&self.access_token_ttl) && is_default_token_ttl(&self.compat_token_ttl)
92+
is_default_token_ttl(&self.access_token_ttl)
93+
&& is_default_token_ttl(&self.compat_token_ttl)
94+
&& self.inactive_session_expiration.is_none()
6195
}
6296
}
6397

crates/data-model/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub use self::{
3232
AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, Client, DeviceCodeGrant,
3333
DeviceCodeGrantState, InvalidRedirectUriError, JwksOrJwksUri, Pkce, Session, SessionState,
3434
},
35-
site_config::{CaptchaConfig, CaptchaService, SiteConfig},
35+
site_config::{CaptchaConfig, CaptchaService, SessionExpirationConfig, SiteConfig},
3636
tokens::{
3737
AccessToken, AccessTokenState, RefreshToken, RefreshTokenState, TokenFormatError, TokenType,
3838
},

crates/data-model/src/site_config.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ pub struct CaptchaConfig {
2828
pub secret_key: String,
2929
}
3030

31+
/// Automatic session expiration configuration
32+
#[derive(Debug, Clone)]
33+
pub struct SessionExpirationConfig {
34+
pub user_session_inactivity_ttl: Option<Duration>,
35+
pub oauth_session_inactivity_ttl: Option<Duration>,
36+
pub compat_session_inactivity_ttl: Option<Duration>,
37+
}
38+
3139
/// Random site configuration we want accessible in various places.
3240
#[allow(clippy::struct_excessive_bools)]
3341
#[derive(Debug, Clone)]
@@ -74,4 +82,6 @@ pub struct SiteConfig {
7482
/// Minimum password complexity, between 0 and 4.
7583
/// This is a score from zxcvbn.
7684
pub minimum_password_complexity: u8,
85+
86+
pub session_expiration: Option<SessionExpirationConfig>,
7787
}

crates/handlers/src/admin/v1/oauth2_sessions/list.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,22 @@ impl std::fmt::Display for OAuth2SessionStatus {
4646
}
4747
}
4848

49+
#[derive(Deserialize, JsonSchema, Clone, Copy)]
50+
#[serde(rename_all = "snake_case")]
51+
enum OAuth2ClientKind {
52+
Dynamic,
53+
Static,
54+
}
55+
56+
impl std::fmt::Display for OAuth2ClientKind {
57+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58+
match self {
59+
Self::Dynamic => write!(f, "dynamic"),
60+
Self::Static => write!(f, "static"),
61+
}
62+
}
63+
}
64+
4965
#[derive(FromRequestParts, Deserialize, JsonSchema, OperationIo)]
5066
#[serde(rename = "OAuth2SessionFilter")]
5167
#[aide(input_with = "Query<FilterParams>")]
@@ -61,6 +77,10 @@ pub struct FilterParams {
6177
#[schemars(with = "Option<crate::admin::schema::Ulid>")]
6278
client: Option<Ulid>,
6379

80+
/// Retrieve the items only for a specific client kind
81+
#[serde(rename = "filter[client-kind]")]
82+
client_kind: Option<OAuth2ClientKind>,
83+
6484
/// Retrieve the items started from the given browser session
6585
#[serde(rename = "filter[user-session]")]
6686
#[schemars(with = "Option<crate::admin::schema::Ulid>")]
@@ -95,6 +115,11 @@ impl std::fmt::Display for FilterParams {
95115
sep = '&';
96116
}
97117

118+
if let Some(client_kind) = self.client_kind {
119+
write!(f, "{sep}filter[client-kind]={client_kind}")?;
120+
sep = '&';
121+
}
122+
98123
if let Some(user_session) = self.user_session {
99124
write!(f, "{sep}filter[user-session]={user_session}")?;
100125
sep = '&';
@@ -232,6 +257,12 @@ pub async fn handler(
232257
None => filter,
233258
};
234259

260+
let filter = match params.client_kind {
261+
Some(OAuth2ClientKind::Dynamic) => filter.only_dynamic_clients(),
262+
Some(OAuth2ClientKind::Static) => filter.only_static_clients(),
263+
None => filter,
264+
};
265+
235266
let user_session = if let Some(user_session_id) = params.user_session {
236267
let user_session = repo
237268
.browser_session()

crates/handlers/src/test_utils.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ pub fn test_site_config() -> SiteConfig {
139139
account_recovery_allowed: true,
140140
captcha: None,
141141
minimum_password_complexity: 1,
142+
session_expiration: None,
142143
}
143144
}
144145

crates/storage-pg/src/iden.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ pub enum OAuth2Sessions {
8383
LastActiveIp,
8484
}
8585

86+
#[derive(sea_query::Iden)]
87+
#[iden = "oauth2_clients"]
88+
pub enum OAuth2Clients {
89+
Table,
90+
#[iden = "oauth2_client_id"]
91+
OAuth2ClientId,
92+
IsStatic,
93+
}
94+
8695
#[derive(sea_query::Iden)]
8796
#[iden = "upstream_oauth_providers"]
8897
pub enum UpstreamOAuthProviders {

crates/storage-pg/src/oauth2/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@ mod tests {
525525
let pagination = Pagination::first(10);
526526

527527
// First, list all the sessions
528-
let filter = OAuth2SessionFilter::new();
528+
let filter = OAuth2SessionFilter::new().for_any_user();
529529
let list = repo
530530
.oauth2_session()
531531
.list(filter, pagination)

0 commit comments

Comments
 (0)