Skip to content
This repository was archived by the owner on Sep 10, 2024. It is now read-only.

Commit 3ee6dfc

Browse files
committed
Record user agents on OAuth 2.0 and compat sessions
1 parent ed5893e commit 3ee6dfc

15 files changed

+263
-13
lines changed

crates/data-model/src/compat/session.rs

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ pub struct CompatSession {
8383
pub user_session_id: Option<Ulid>,
8484
pub created_at: DateTime<Utc>,
8585
pub is_synapse_admin: bool,
86+
pub user_agent: Option<String>,
8687
pub last_active_at: Option<DateTime<Utc>>,
8788
pub last_active_ip: Option<IpAddr>,
8889
}

crates/data-model/src/oauth2/session.rs

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ pub struct Session {
7575
pub user_session_id: Option<Ulid>,
7676
pub client_id: Ulid,
7777
pub scope: Scope,
78+
pub user_agent: Option<String>,
7879
pub last_active_at: Option<DateTime<Utc>>,
7980
pub last_active_ip: Option<IpAddr>,
8081
}

crates/handlers/src/compat/login.rs

+11-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
use axum::{extract::State, response::IntoResponse, Json};
15+
use axum::{extract::State, response::IntoResponse, Json, TypedHeader};
1616
use chrono::Duration;
1717
use hyper::StatusCode;
1818
use mas_axum_utils::sentry::SentryEventID;
@@ -217,9 +217,11 @@ pub(crate) async fn post(
217217
activity_tracker: BoundActivityTracker,
218218
State(homeserver): State<MatrixHomeserver>,
219219
State(site_config): State<SiteConfig>,
220+
user_agent: Option<TypedHeader<headers::UserAgent>>,
220221
Json(input): Json<RequestBody>,
221222
) -> Result<impl IntoResponse, RouteError> {
222-
let (session, user) = match (password_manager.is_enabled(), input.credentials) {
223+
let user_agent = user_agent.map(|ua| ua.to_string());
224+
let (mut session, user) = match (password_manager.is_enabled(), input.credentials) {
223225
(
224226
true,
225227
Credentials::Password {
@@ -245,6 +247,13 @@ pub(crate) async fn post(
245247
}
246248
};
247249

250+
if let Some(user_agent) = user_agent {
251+
session = repo
252+
.compat_session()
253+
.record_user_agent(session, user_agent)
254+
.await?;
255+
}
256+
248257
let user_id = format!("@{username}:{homeserver}", username = user.username);
249258

250259
// If the client asked for a refreshable token, make it expire

crates/handlers/src/oauth2/token.rs

+46-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
use axum::{extract::State, response::IntoResponse, Json};
15+
use axum::{extract::State, response::IntoResponse, Json, TypedHeader};
1616
use chrono::{DateTime, Duration, Utc};
1717
use headers::{CacheControl, HeaderMap, HeaderMapExt, Pragma};
1818
use hyper::StatusCode;
@@ -230,8 +230,10 @@ pub(crate) async fn post(
230230
State(site_config): State<SiteConfig>,
231231
State(encrypter): State<Encrypter>,
232232
policy: Policy,
233+
user_agent: Option<TypedHeader<headers::UserAgent>>,
233234
client_authorization: ClientAuthorization<AccessTokenRequest>,
234235
) -> Result<impl IntoResponse, RouteError> {
236+
let user_agent = user_agent.map(|ua| ua.to_string());
235237
let client = client_authorization
236238
.credentials
237239
.fetch(&mut repo)
@@ -262,6 +264,7 @@ pub(crate) async fn post(
262264
&url_builder,
263265
&site_config,
264266
repo,
267+
user_agent,
265268
)
266269
.await?
267270
}
@@ -274,6 +277,7 @@ pub(crate) async fn post(
274277
&client,
275278
&site_config,
276279
repo,
280+
user_agent,
277281
)
278282
.await?
279283
}
@@ -287,6 +291,7 @@ pub(crate) async fn post(
287291
&site_config,
288292
repo,
289293
policy,
294+
user_agent,
290295
)
291296
.await?
292297
}
@@ -301,6 +306,7 @@ pub(crate) async fn post(
301306
&url_builder,
302307
&site_config,
303308
repo,
309+
user_agent,
304310
)
305311
.await?
306312
}
@@ -329,6 +335,7 @@ async fn authorization_code_grant(
329335
url_builder: &UrlBuilder,
330336
site_config: &SiteConfig,
331337
mut repo: BoxRepository,
338+
user_agent: Option<String>,
332339
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
333340
// Check that the client is allowed to use this grant type
334341
if !client.grant_types.contains(&GrantType::AuthorizationCode) {
@@ -386,12 +393,19 @@ async fn authorization_code_grant(
386393
}
387394
};
388395

389-
let session = repo
396+
let mut session = repo
390397
.oauth2_session()
391398
.lookup(session_id)
392399
.await?
393400
.ok_or(RouteError::NoSuchOAuthSession)?;
394401

402+
if let Some(user_agent) = user_agent {
403+
session = repo
404+
.oauth2_session()
405+
.record_user_agent(session, user_agent)
406+
.await?;
407+
}
408+
395409
// This should never happen, since we looked up in the database using the code
396410
let code = authz_grant.code.as_ref().ok_or(RouteError::InvalidGrant)?;
397411

@@ -490,6 +504,7 @@ async fn refresh_token_grant(
490504
client: &Client,
491505
site_config: &SiteConfig,
492506
mut repo: BoxRepository,
507+
user_agent: Option<String>,
493508
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
494509
// Check that the client is allowed to use this grant type
495510
if !client.grant_types.contains(&GrantType::RefreshToken) {
@@ -502,12 +517,21 @@ async fn refresh_token_grant(
502517
.await?
503518
.ok_or(RouteError::RefreshTokenNotFound)?;
504519

505-
let session = repo
520+
let mut session = repo
506521
.oauth2_session()
507522
.lookup(refresh_token.session_id)
508523
.await?
509524
.ok_or(RouteError::NoSuchOAuthSession)?;
510525

526+
// Let's for now record the user agent on each refresh, that should be responsive enough and
527+
// not too much of a burden on the database.
528+
if let Some(user_agent) = user_agent {
529+
session = repo
530+
.oauth2_session()
531+
.record_user_agent(session, user_agent)
532+
.await?;
533+
}
534+
511535
if !refresh_token.is_valid() {
512536
return Err(RouteError::RefreshTokenInvalid(refresh_token.id));
513537
}
@@ -563,6 +587,7 @@ async fn client_credentials_grant(
563587
site_config: &SiteConfig,
564588
mut repo: BoxRepository,
565589
mut policy: Policy,
590+
user_agent: Option<String>,
566591
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
567592
// Check that the client is allowed to use this grant type
568593
if !client.grant_types.contains(&GrantType::ClientCredentials) {
@@ -584,11 +609,18 @@ async fn client_credentials_grant(
584609
}
585610

586611
// Start the session
587-
let session = repo
612+
let mut session = repo
588613
.oauth2_session()
589614
.add_from_client_credentials(rng, clock, client, scope)
590615
.await?;
591616

617+
if let Some(user_agent) = user_agent {
618+
session = repo
619+
.oauth2_session()
620+
.record_user_agent(session, user_agent)
621+
.await?;
622+
}
623+
592624
let ttl = site_config.access_token_ttl;
593625
let access_token_str = TokenType::AccessToken.generate(rng);
594626

@@ -624,6 +656,7 @@ async fn device_code_grant(
624656
url_builder: &UrlBuilder,
625657
site_config: &SiteConfig,
626658
mut repo: BoxRepository,
659+
user_agent: Option<String>,
627660
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
628661
// Check that the client is allowed to use this grant type
629662
if !client.grant_types.contains(&GrantType::DeviceCode) {
@@ -670,11 +703,19 @@ async fn device_code_grant(
670703
.ok_or(RouteError::NoSuchBrowserSession)?;
671704

672705
// Start the session
673-
let session = repo
706+
let mut session = repo
674707
.oauth2_session()
675708
.add_from_browser_session(rng, clock, client, &browser_session, grant.scope)
676709
.await?;
677710

711+
// XXX: should we get the user agent from the device code grant instead?
712+
if let Some(user_agent) = user_agent {
713+
session = repo
714+
.oauth2_session()
715+
.record_user_agent(session, user_agent)
716+
.await?;
717+
}
718+
678719
let ttl = site_config.access_token_ttl;
679720
let access_token_str = TokenType::AccessToken.generate(rng);
680721

crates/storage-pg/.sqlx/query-1919d402fd6f148d14417f633be3353004f458c85f7b4f361802f86651900fbc.json

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/storage-pg/.sqlx/query-29148548d592046f7d711676911e3847e376e443ccd841f76b17a81f53fafc3a.json

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/storage-pg/.sqlx/query-31aace373b20b5dbf65fa51d8663da7571d85b6a7d2d544d69e7d04260cdffc9.json renamed to crates/storage-pg/.sqlx/query-5a2e9b5002c1927c0035c22e393172b36ab46a4377b46618205151ea041886d5.json

+9-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/storage-pg/.sqlx/query-04e25c9267bf2eb143a6445345229081e7b386743a93b3833ef8ad9d09972f3b.json renamed to crates/storage-pg/.sqlx/query-bb6f55a4cc10bec8ec0fc138485f6b4d308302bb1fa3accb12932d1e5ce457e9.json

+9-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-- Copyright 2024 The Matrix.org Foundation C.I.C.
2+
--
3+
-- Licensed under the Apache License, Version 2.0 (the "License");
4+
-- you may not use this file except in compliance with the License.
5+
-- You may obtain a copy of the License at
6+
--
7+
-- http://www.apache.org/licenses/LICENSE-2.0
8+
--
9+
-- Unless required by applicable law or agreed to in writing, software
10+
-- distributed under the License is distributed on an "AS IS" BASIS,
11+
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
-- See the License for the specific language governing permissions and
13+
-- limitations under the License.
14+
15+
-- Adds user agent columns to oauth and compat sessions tables
16+
ALTER TABLE oauth2_sessions ADD COLUMN user_agent TEXT;
17+
ALTER TABLE compat_sessions ADD COLUMN user_agent TEXT;

crates/storage-pg/src/app_session.rs

+12
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ mod priv_ {
7373
pub(super) created_at: DateTime<Utc>,
7474
pub(super) finished_at: Option<DateTime<Utc>>,
7575
pub(super) is_synapse_admin: Option<bool>,
76+
pub(super) user_agent: Option<String>,
7677
pub(super) last_active_at: Option<DateTime<Utc>>,
7778
pub(super) last_active_ip: Option<IpAddr>,
7879
}
@@ -98,6 +99,7 @@ impl TryFrom<AppSessionLookup> for AppSession {
9899
created_at,
99100
finished_at,
100101
is_synapse_admin,
102+
user_agent,
101103
last_active_at,
102104
last_active_ip,
103105
} = value;
@@ -143,6 +145,7 @@ impl TryFrom<AppSessionLookup> for AppSession {
143145
user_session_id,
144146
created_at,
145147
is_synapse_admin,
148+
user_agent,
146149
last_active_at,
147150
last_active_ip,
148151
};
@@ -182,6 +185,7 @@ impl TryFrom<AppSessionLookup> for AppSession {
182185
user_id: user_id.map(Ulid::from),
183186
user_session_id,
184187
scope,
188+
user_agent,
185189
last_active_at,
186190
last_active_ip,
187191
};
@@ -250,6 +254,10 @@ impl<'c> AppSessionRepository for PgAppSessionRepository<'c> {
250254
AppSessionLookupIden::FinishedAt,
251255
)
252256
.expr_as(Expr::cust("NULL"), AppSessionLookupIden::IsSynapseAdmin)
257+
.expr_as(
258+
Expr::col((OAuth2Sessions::Table, OAuth2Sessions::UserAgent)),
259+
AppSessionLookupIden::UserAgent,
260+
)
253261
.expr_as(
254262
Expr::col((OAuth2Sessions::Table, OAuth2Sessions::LastActiveAt)),
255263
AppSessionLookupIden::LastActiveAt,
@@ -317,6 +325,10 @@ impl<'c> AppSessionRepository for PgAppSessionRepository<'c> {
317325
Expr::col((CompatSessions::Table, CompatSessions::IsSynapseAdmin)),
318326
AppSessionLookupIden::IsSynapseAdmin,
319327
)
328+
.expr_as(
329+
Expr::col((CompatSessions::Table, CompatSessions::UserAgent)),
330+
AppSessionLookupIden::UserAgent,
331+
)
320332
.expr_as(
321333
Expr::col((CompatSessions::Table, CompatSessions::LastActiveAt)),
322334
AppSessionLookupIden::LastActiveAt,

0 commit comments

Comments
 (0)