Skip to content

Commit 48843db

Browse files
authored
Clear the session cookie on logout from the GraphQL API (#4328)
2 parents 744bb2c + 77b04ef commit 48843db

File tree

3 files changed

+59
-9
lines changed

3 files changed

+59
-9
lines changed

Diff for: crates/handlers/src/graphql/mod.rs

+20-9
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ use opentelemetry_semantic_conventions::trace::{GRAPHQL_DOCUMENT, GRAPHQL_OPERAT
3838
use rand::{SeedableRng, thread_rng};
3939
use rand_chacha::ChaChaRng;
4040
use sqlx::PgPool;
41+
use state::has_session_ended;
4142
use tracing::{Instrument, info_span};
4243
use ulid::Ulid;
4344

@@ -237,7 +238,7 @@ async fn get_requester(
237238
clock: &impl Clock,
238239
activity_tracker: &BoundActivityTracker,
239240
mut repo: BoxRepository,
240-
session_info: SessionInfo,
241+
session_info: &SessionInfo,
241242
user_agent: Option<String>,
242243
token: Option<&str>,
243244
) -> Result<Requester, RouteError> {
@@ -328,13 +329,13 @@ pub async fn post(
328329
.as_ref()
329330
.map(|TypedHeader(Authorization(bearer))| bearer.token());
330331
let user_agent = user_agent.map(|TypedHeader(h)| h.to_string());
331-
let (session_info, _cookie_jar) = cookie_jar.session_info();
332+
let (session_info, mut cookie_jar) = cookie_jar.session_info();
332333
let requester = get_requester(
333334
undocumented_oauth2_access,
334335
&clock,
335336
&activity_tracker,
336337
repo,
337-
session_info,
338+
&session_info,
338339
user_agent,
339340
token,
340341
)
@@ -352,7 +353,12 @@ pub async fn post(
352353
.data(requester); // XXX: this should probably return another error response?
353354

354355
let span = span_for_graphql_request(&request);
355-
let response = schema.execute(request).instrument(span).await;
356+
let mut response = schema.execute(request).instrument(span).await;
357+
358+
if has_session_ended(&mut response) {
359+
let session_info = session_info.mark_session_ended();
360+
cookie_jar = cookie_jar.update_session_info(&session_info);
361+
}
356362

357363
let cache_control = response
358364
.cache_control
@@ -362,7 +368,7 @@ pub async fn post(
362368

363369
let headers = response.http_headers.clone();
364370

365-
Ok((headers, cache_control, Json(response)))
371+
Ok((headers, cache_control, cookie_jar, Json(response)))
366372
}
367373

368374
pub async fn get(
@@ -382,13 +388,13 @@ pub async fn get(
382388
.as_ref()
383389
.map(|TypedHeader(Authorization(bearer))| bearer.token());
384390
let user_agent = user_agent.map(|TypedHeader(h)| h.to_string());
385-
let (session_info, _cookie_jar) = cookie_jar.session_info();
391+
let (session_info, mut cookie_jar) = cookie_jar.session_info();
386392
let requester = get_requester(
387393
undocumented_oauth2_access,
388394
&clock,
389395
&activity_tracker,
390396
repo,
391-
session_info,
397+
&session_info,
392398
user_agent,
393399
token,
394400
)
@@ -398,7 +404,12 @@ pub async fn get(
398404
async_graphql::http::parse_query_string(&query.unwrap_or_default())?.data(requester);
399405

400406
let span = span_for_graphql_request(&request);
401-
let response = schema.execute(request).instrument(span).await;
407+
let mut response = schema.execute(request).instrument(span).await;
408+
409+
if has_session_ended(&mut response) {
410+
let session_info = session_info.mark_session_ended();
411+
cookie_jar = cookie_jar.update_session_info(&session_info);
412+
}
402413

403414
let cache_control = response
404415
.cache_control
@@ -408,7 +419,7 @@ pub async fn get(
408419

409420
let headers = response.http_headers.clone();
410421

411-
Ok((headers, cache_control, Json(response)))
422+
Ok((headers, cache_control, cookie_jar, Json(response)))
412423
}
413424

414425
pub async fn playground() -> impl IntoResponse {

Diff for: crates/handlers/src/graphql/mutations/browser_session.rs

+9
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ impl BrowserSessionMutations {
8888

8989
repo.save().await?;
9090

91+
// If we are ending the *current* session, we need to clear the session cookie
92+
// as well
93+
if requester
94+
.browser_session()
95+
.is_some_and(|s| s.id == session.id)
96+
{
97+
ctx.mark_session_ended();
98+
}
99+
91100
Ok(EndBrowserSessionPayload::Ended(Box::new(session)))
92101
}
93102
}

Diff for: crates/handlers/src/graphql/state.rs

+30
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// SPDX-License-Identifier: AGPL-3.0-only
55
// Please see LICENSE in the repository root for full details.
66

7+
use async_graphql::{Response, ServerError};
78
use mas_data_model::SiteConfig;
89
use mas_matrix::HomeserverConnection;
910
use mas_policy::Policy;
@@ -12,6 +13,8 @@ use mas_storage::{BoxClock, BoxRepository, BoxRng, RepositoryError};
1213

1314
use crate::{Limiter, graphql::Requester, passwords::PasswordManager};
1415

16+
const CLEAR_SESSION_SENTINEL: &str = "__CLEAR_SESSION__";
17+
1518
#[async_trait::async_trait]
1619
pub trait State {
1720
async fn repository(&self) -> Result<BoxRepository, RepositoryError>;
@@ -30,6 +33,8 @@ pub type BoxState = Box<dyn State + Send + Sync + 'static>;
3033
pub trait ContextExt {
3134
fn state(&self) -> &BoxState;
3235

36+
fn mark_session_ended(&self);
37+
3338
fn requester(&self) -> &Requester;
3439
}
3540

@@ -38,7 +43,32 @@ impl ContextExt for async_graphql::Context<'_> {
3843
self.data_unchecked()
3944
}
4045

46+
fn mark_session_ended(&self) {
47+
// Add a sentinel to the error context, so that we can know that we need to
48+
// clear the session
49+
// XXX: this is a bit of a hack, but the only sane way to get infos from within
50+
// a mutation up to the HTTP handler
51+
self.add_error(ServerError::new(CLEAR_SESSION_SENTINEL, None));
52+
}
53+
4154
fn requester(&self) -> &Requester {
4255
self.data_unchecked()
4356
}
4457
}
58+
59+
/// Returns true if the response contains a sentinel error indicating that the
60+
/// current cookie session has ended, and the session cookie should be cleared.
61+
///
62+
/// Also removes the sentinel error from the response.
63+
pub fn has_session_ended(response: &mut Response) -> bool {
64+
let errors = std::mem::take(&mut response.errors);
65+
let mut must_clear_session = false;
66+
for error in errors {
67+
if error.message == CLEAR_SESSION_SENTINEL {
68+
must_clear_session = true;
69+
} else {
70+
response.errors.push(error);
71+
}
72+
}
73+
must_clear_session
74+
}

0 commit comments

Comments
 (0)