Skip to content

Commit 0e5c964

Browse files
committed
Log out oauth & compat sessions when receiving a backchannel logout request
1 parent c42f20b commit 0e5c964

File tree

9 files changed

+146
-7
lines changed

9 files changed

+146
-7
lines changed

crates/cli/src/sync.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,9 @@ pub async fn config_sync(
283283
mas_config::UpstreamOAuth2OnBackchannelLogout::LogoutBrowserOnly => {
284284
mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::LogoutBrowserOnly
285285
}
286+
mas_config::UpstreamOAuth2OnBackchannelLogout::LogoutAll => {
287+
mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::LogoutAll
288+
}
286289
};
287290

288291
repo.upstream_oauth_provider()

crates/config/src/sections/upstream_oauth2.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,10 @@ pub enum OnBackchannelLogout {
418418

419419
/// Only log out the MAS 'browser session' started by this OIDC session
420420
LogoutBrowserOnly,
421+
422+
/// Log out all sessions started by this OIDC session, including MAS
423+
/// 'browser sessions' and client sessions
424+
LogoutAll,
421425
}
422426

423427
impl OnBackchannelLogout {

crates/data-model/src/upstream_oauth2/provider.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ pub struct InvalidUpstreamOAuth2TokenAuthMethod(String);
221221
pub enum OnBackchannelLogout {
222222
DoNothing,
223223
LogoutBrowserOnly,
224+
LogoutAll,
224225
}
225226

226227
impl OnBackchannelLogout {
@@ -229,6 +230,7 @@ impl OnBackchannelLogout {
229230
match self {
230231
Self::DoNothing => "do_nothing",
231232
Self::LogoutBrowserOnly => "logout_browser_only",
233+
Self::LogoutAll => "logout_all",
232234
}
233235
}
234236
}
@@ -246,6 +248,7 @@ impl std::str::FromStr for OnBackchannelLogout {
246248
match s {
247249
"do_nothing" => Ok(Self::DoNothing),
248250
"logout_browser_only" => Ok(Self::LogoutBrowserOnly),
251+
"logout_all" => Ok(Self::LogoutAll),
249252
s => Err(InvalidUpstreamOAuth2OnBackchannelLogout(s.to_owned())),
250253
}
251254
}

crates/handlers/src/upstream_oauth2/backchannel_logout.rs

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
44
// Please see LICENSE files in the repository root for full details.
55

6-
use std::collections::HashMap;
6+
use std::collections::{HashMap, HashSet};
77

88
use axum::{
99
Form, Json,
@@ -22,7 +22,11 @@ use mas_oidc_client::{
2222
requests::jose::{JwtVerificationData, verify_signed_jwt},
2323
};
2424
use mas_storage::{
25-
BoxClock, BoxRepository, upstream_oauth2::UpstreamOAuthSessionFilter,
25+
BoxClock, BoxRepository, BoxRng, Pagination,
26+
compat::CompatSessionFilter,
27+
oauth2::OAuth2SessionFilter,
28+
queue::{QueueJobRepositoryExt as _, SyncDevicesJob},
29+
upstream_oauth2::UpstreamOAuthSessionFilter,
2630
user::BrowserSessionFilter,
2731
};
2832
use oauth2_types::errors::{ClientError, ClientErrorCode};
@@ -131,6 +135,7 @@ const EVENTS: Claim<LogoutTokenEvents> = Claim::new("events");
131135
)]
132136
pub(crate) async fn post(
133137
clock: BoxClock,
138+
mut rng: BoxRng,
134139
mut repo: BoxRepository,
135140
State(metadata_cache): State<MetadataCache>,
136141
State(client): State<reqwest::Client>,
@@ -242,10 +247,67 @@ pub(crate) async fn post(
242247
}
243248
UpstreamOAuthProviderOnBackchannelLogout::LogoutBrowserOnly => {
244249
let filter = BrowserSessionFilter::new()
245-
.authenticated_by_upstream_sessions_only(auth_session_filter);
250+
.authenticated_by_upstream_sessions_only(auth_session_filter)
251+
.active_only();
246252
let affected = repo.browser_session().finish_bulk(&clock, filter).await?;
247253
tracing::info!("Finished {affected} browser sessions");
248254
}
255+
UpstreamOAuthProviderOnBackchannelLogout::LogoutAll => {
256+
let browser_session_filter = BrowserSessionFilter::new()
257+
.authenticated_by_upstream_sessions_only(auth_session_filter);
258+
259+
// We need to loop through all the browser sessions to find all the
260+
// users affected so that we can trigger a device sync job for them
261+
let mut cursor = Pagination::first(1000);
262+
let mut user_ids = HashSet::new();
263+
loop {
264+
let browser_sessions = repo
265+
.browser_session()
266+
.list(browser_session_filter, cursor)
267+
.await?;
268+
for browser_session in browser_sessions.edges {
269+
user_ids.insert(browser_session.user.id);
270+
cursor = cursor.after(browser_session.id);
271+
}
272+
273+
if !browser_sessions.has_next_page {
274+
break;
275+
}
276+
}
277+
278+
let browser_sessions_affected = repo
279+
.browser_session()
280+
.finish_bulk(&clock, browser_session_filter.active_only())
281+
.await?;
282+
283+
let oauth2_session_filter = OAuth2SessionFilter::new()
284+
.active_only()
285+
.for_browser_sessions(browser_session_filter);
286+
287+
let oauth2_sessions_affected = repo
288+
.oauth2_session()
289+
.finish_bulk(&clock, oauth2_session_filter)
290+
.await?;
291+
292+
let compat_session_filter = CompatSessionFilter::new()
293+
.active_only()
294+
.for_browser_sessions(browser_session_filter);
295+
296+
let compat_sessions_affected = repo
297+
.compat_session()
298+
.finish_bulk(&clock, compat_session_filter)
299+
.await?;
300+
301+
tracing::info!(
302+
"Finished {browser_sessions_affected} browser sessions, {oauth2_sessions_affected} OAuth 2.0 sessions and {compat_sessions_affected} compatibility sessions"
303+
);
304+
305+
for user_id in user_ids {
306+
tracing::info!(user.id = %user_id, "Queueing a device sync job for user");
307+
let job = SyncDevicesJob::new_for_id(user_id);
308+
repo.queue_job().schedule_job(&mut rng, &clock, job).await?;
309+
}
310+
}
249311
}
250312

251313
repo.save().await?;

crates/storage-pg/src/compat/session.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use uuid::Uuid;
2727
use crate::{
2828
DatabaseError, DatabaseInconsistencyError,
2929
filter::{Filter, StatementExt, StatementWithJoinsExt},
30-
iden::{CompatSessions, CompatSsoLogins},
30+
iden::{CompatSessions, CompatSsoLogins, UserSessions},
3131
pagination::QueryBuilderExt,
3232
tracing::ExecuteExt,
3333
};
@@ -190,6 +190,18 @@ impl Filter for CompatSessionFilter<'_> {
190190
Expr::col((CompatSessions::Table, CompatSessions::UserSessionId))
191191
.eq(Uuid::from(browser_session.id))
192192
}))
193+
.add_option(self.browser_session_filter().map(|browser_session_filter| {
194+
Expr::col((CompatSessions::Table, CompatSessions::UserSessionId)).in_subquery(
195+
Query::select()
196+
.expr(Expr::col((
197+
UserSessions::Table,
198+
UserSessions::UserSessionId,
199+
)))
200+
.apply_filter(browser_session_filter)
201+
.from(UserSessions::Table)
202+
.take(),
203+
)
204+
}))
193205
.add_option(self.state().map(|state| {
194206
if state.is_active() {
195207
Expr::col((CompatSessions::Table, CompatSessions::FinishedAt)).is_null()

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use uuid::Uuid;
2424
use crate::{
2525
DatabaseError, DatabaseInconsistencyError,
2626
filter::{Filter, StatementExt},
27-
iden::{OAuth2Clients, OAuth2Sessions},
27+
iden::{OAuth2Clients, OAuth2Sessions, UserSessions},
2828
pagination::QueryBuilderExt,
2929
tracing::ExecuteExt,
3030
};
@@ -141,6 +141,18 @@ impl Filter for OAuth2SessionFilter<'_> {
141141
Expr::col((OAuth2Sessions::Table, OAuth2Sessions::UserSessionId))
142142
.eq(Uuid::from(browser_session.id))
143143
}))
144+
.add_option(self.browser_session_filter().map(|browser_session_filter| {
145+
Expr::col((OAuth2Sessions::Table, OAuth2Sessions::UserSessionId)).in_subquery(
146+
Query::select()
147+
.expr(Expr::col((
148+
UserSessions::Table,
149+
UserSessions::UserSessionId,
150+
)))
151+
.apply_filter(browser_session_filter)
152+
.from(UserSessions::Table)
153+
.take(),
154+
)
155+
}))
144156
.add_option(self.state().map(|state| {
145157
if state.is_active() {
146158
Expr::col((OAuth2Sessions::Table, OAuth2Sessions::FinishedAt)).is_null()

crates/storage/src/compat/session.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use mas_data_model::{BrowserSession, CompatSession, CompatSsoLogin, Device, User
1212
use rand_core::RngCore;
1313
use ulid::Ulid;
1414

15-
use crate::{Clock, Page, Pagination, repository_impl};
15+
use crate::{Clock, Page, Pagination, repository_impl, user::BrowserSessionFilter};
1616

1717
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1818
pub enum CompatSessionState {
@@ -59,6 +59,7 @@ impl CompatSessionType {
5959
pub struct CompatSessionFilter<'a> {
6060
user: Option<&'a User>,
6161
browser_session: Option<&'a BrowserSession>,
62+
browser_session_filter: Option<BrowserSessionFilter<'a>>,
6263
state: Option<CompatSessionState>,
6364
auth_type: Option<CompatSessionType>,
6465
device: Option<&'a Device>,
@@ -106,12 +107,28 @@ impl<'a> CompatSessionFilter<'a> {
106107
self
107108
}
108109

110+
/// Set the browser sessions filter
111+
#[must_use]
112+
pub fn for_browser_sessions(
113+
mut self,
114+
browser_session_filter: BrowserSessionFilter<'a>,
115+
) -> Self {
116+
self.browser_session_filter = Some(browser_session_filter);
117+
self
118+
}
119+
109120
/// Get the browser session filter
110121
#[must_use]
111122
pub fn browser_session(&self) -> Option<&'a BrowserSession> {
112123
self.browser_session
113124
}
114125

126+
/// Get the browser sessions filter
127+
#[must_use]
128+
pub fn browser_session_filter(&self) -> Option<BrowserSessionFilter<'a>> {
129+
self.browser_session_filter
130+
}
131+
115132
/// Only return sessions with a last active time before the given time
116133
#[must_use]
117134
pub fn with_last_active_before(mut self, last_active_before: DateTime<Utc>) -> Self {

crates/storage/src/oauth2/session.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use oauth2_types::scope::Scope;
1313
use rand_core::RngCore;
1414
use ulid::Ulid;
1515

16-
use crate::{Clock, Pagination, pagination::Page, repository_impl};
16+
use crate::{Clock, Pagination, pagination::Page, repository_impl, user::BrowserSessionFilter};
1717

1818
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1919
pub enum OAuth2SessionState {
@@ -49,6 +49,7 @@ pub struct OAuth2SessionFilter<'a> {
4949
user: Option<&'a User>,
5050
any_user: Option<bool>,
5151
browser_session: Option<&'a BrowserSession>,
52+
browser_session_filter: Option<BrowserSessionFilter<'a>>,
5253
device: Option<&'a Device>,
5354
client: Option<&'a Client>,
5455
client_kind: Option<ClientKind>,
@@ -109,6 +110,16 @@ impl<'a> OAuth2SessionFilter<'a> {
109110
self
110111
}
111112

113+
/// List sessions started by a set of browser sessions
114+
#[must_use]
115+
pub fn for_browser_sessions(
116+
mut self,
117+
browser_session_filter: BrowserSessionFilter<'a>,
118+
) -> Self {
119+
self.browser_session_filter = Some(browser_session_filter);
120+
self
121+
}
122+
112123
/// Get the browser session filter
113124
///
114125
/// Returns [`None`] if no browser session filter was set
@@ -117,6 +128,14 @@ impl<'a> OAuth2SessionFilter<'a> {
117128
self.browser_session
118129
}
119130

131+
/// Get the browser sessions filter
132+
///
133+
/// Returns [`None`] if no browser session filter was set
134+
#[must_use]
135+
pub fn browser_session_filter(&self) -> Option<BrowserSessionFilter<'a>> {
136+
self.browser_session_filter
137+
}
138+
120139
/// List sessions for a specific client
121140
#[must_use]
122141
pub fn for_client(mut self, client: &'a Client) -> Self {

docs/config.schema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2459,6 +2459,13 @@
24592459
"enum": [
24602460
"logout_browser_only"
24612461
]
2462+
},
2463+
{
2464+
"description": "Log out all sessions started by this OIDC session, including MAS 'browser sessions' and client sessions",
2465+
"type": "string",
2466+
"enum": [
2467+
"logout_all"
2468+
]
24622469
}
24632470
]
24642471
},

0 commit comments

Comments
 (0)