diff --git a/crates/data-model/src/users.rs b/crates/data-model/src/users.rs index f71c0b26..b6187785 100644 --- a/crates/data-model/src/users.rs +++ b/crates/data-model/src/users.rs @@ -80,6 +80,7 @@ pub struct BrowserSession { pub user: User, pub created_at: DateTime, pub finished_at: Option>, + pub user_agent: Option, } impl BrowserSession { @@ -99,6 +100,7 @@ impl BrowserSession { user, created_at: now, finished_at: None, + user_agent: Some("Mozilla/5.0".to_owned()), }) .collect() } diff --git a/crates/handlers/src/graphql/tests.rs b/crates/handlers/src/graphql/tests.rs index b458c016..fb17fbee 100644 --- a/crates/handlers/src/graphql/tests.rs +++ b/crates/handlers/src/graphql/tests.rs @@ -84,7 +84,7 @@ async fn start_oauth_session( let browser_session = repo .browser_session() - .add(&mut rng, &state.clock, user) + .add(&mut rng, &state.clock, user, None) .await .unwrap(); diff --git a/crates/handlers/src/oauth2/introspection.rs b/crates/handlers/src/oauth2/introspection.rs index d143b056..3051f984 100644 --- a/crates/handlers/src/oauth2/introspection.rs +++ b/crates/handlers/src/oauth2/introspection.rs @@ -421,7 +421,7 @@ mod tests { let browser_session = repo .browser_session() - .add(&mut state.rng(), &state.clock, &user) + .add(&mut state.rng(), &state.clock, &user, None) .await .unwrap(); diff --git a/crates/handlers/src/oauth2/revoke.rs b/crates/handlers/src/oauth2/revoke.rs index 6ad1c5a3..28cc76e5 100644 --- a/crates/handlers/src/oauth2/revoke.rs +++ b/crates/handlers/src/oauth2/revoke.rs @@ -284,7 +284,7 @@ mod tests { let browser_session = repo .browser_session() - .add(&mut state.rng(), &state.clock, &user) + .add(&mut state.rng(), &state.clock, &user, None) .await .unwrap(); diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index c9674cb1..837840b3 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -464,7 +464,7 @@ mod tests { let browser_session = repo .browser_session() - .add(&mut state.rng(), &state.clock, &user) + .add(&mut state.rng(), &state.clock, &user, None) .await .unwrap(); @@ -672,7 +672,7 @@ mod tests { let browser_session = repo .browser_session() - .add(&mut state.rng(), &state.clock, &user) + .add(&mut state.rng(), &state.clock, &user, None) .await .unwrap(); diff --git a/crates/handlers/src/upstream_oauth2/link.rs b/crates/handlers/src/upstream_oauth2/link.rs index 9b205ded..9a49c4ef 100644 --- a/crates/handlers/src/upstream_oauth2/link.rs +++ b/crates/handlers/src/upstream_oauth2/link.rs @@ -15,7 +15,7 @@ use axum::{ extract::{Path, State}, response::{Html, IntoResponse}, - Form, + Form, TypedHeader, }; use hyper::StatusCode; use mas_axum_utils::{ @@ -170,8 +170,10 @@ pub(crate) async fn get( mut repo: BoxRepository, State(templates): State, cookie_jar: CookieJar, + user_agent: Option>, Path(link_id): Path, ) -> Result { + let user_agent = user_agent.map(|ua| ua.as_str().to_owned()); let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar); let (session_id, post_auth_action) = sessions_cookie .lookup_link(link_id) @@ -264,7 +266,10 @@ pub(crate) async fn get( .filter(mas_data_model::User::is_valid) .ok_or(RouteError::UserNotFound)?; - let session = repo.browser_session().add(&mut rng, &clock, &user).await?; + let session = repo + .browser_session() + .add(&mut rng, &clock, &user, user_agent) + .await?; let upstream_session = repo .upstream_oauth_session() @@ -352,9 +357,11 @@ pub(crate) async fn post( clock: BoxClock, mut repo: BoxRepository, cookie_jar: CookieJar, + user_agent: Option>, Path(link_id): Path, Form(form): Form>, ) -> Result { + let user_agent = user_agent.map(|ua| ua.as_str().to_owned()); let form = cookie_jar.verify_form(&clock, form)?; let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar); @@ -503,7 +510,9 @@ pub(crate) async fn post( .associate_to_user(&link, &user) .await?; - repo.browser_session().add(&mut rng, &clock, &user).await? + repo.browser_session() + .add(&mut rng, &clock, &user, user_agent) + .await? } _ => return Err(RouteError::InvalidFormAction), diff --git a/crates/handlers/src/views/login.rs b/crates/handlers/src/views/login.rs index 44552955..7108fc56 100644 --- a/crates/handlers/src/views/login.rs +++ b/crates/handlers/src/views/login.rs @@ -15,7 +15,9 @@ use axum::{ extract::{Form, Query, State}, response::{Html, IntoResponse, Response}, + TypedHeader, }; +use headers::UserAgent; use hyper::StatusCode; use mas_axum_utils::{ cookies::CookieJar, @@ -109,8 +111,10 @@ pub(crate) async fn post( mut repo: BoxRepository, Query(query): Query, cookie_jar: CookieJar, + user_agent: Option>, Form(form): Form>, ) -> Result { + let user_agent = user_agent.map(|ua| ua.as_str().to_owned()); if !password_manager.is_enabled() { // XXX: is it necessary to have better errors here? return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response()); @@ -158,6 +162,7 @@ pub(crate) async fn post( &clock, &form.username, &form.password, + user_agent, ) .await { @@ -193,6 +198,7 @@ async fn login( clock: &impl Clock, username: &str, password: &str, + user_agent: Option, ) -> Result { // XXX: we're loosing the error context here // First, lookup the user @@ -245,7 +251,7 @@ async fn login( // Start a new session let user_session = repo .browser_session() - .add(&mut rng, clock, &user) + .add(&mut rng, clock, &user, user_agent) .await .map_err(|_| FormError::Internal)?; diff --git a/crates/handlers/src/views/register.rs b/crates/handlers/src/views/register.rs index cdab522c..3e32bad1 100644 --- a/crates/handlers/src/views/register.rs +++ b/crates/handlers/src/views/register.rs @@ -17,7 +17,9 @@ use std::{str::FromStr, sync::Arc}; use axum::{ extract::{Form, Query, State}, response::{Html, IntoResponse, Response}, + TypedHeader, }; +use headers::UserAgent; use hyper::StatusCode; use lettre::Address; use mas_axum_utils::{ @@ -104,8 +106,10 @@ pub(crate) async fn post( mut repo: BoxRepository, Query(query): Query, cookie_jar: CookieJar, + user_agent: Option>, Form(form): Form>, ) -> Result { + let user_agent = user_agent.map(|ua| ua.as_str().to_owned()); if !password_manager.is_enabled() { return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response()); } @@ -206,7 +210,10 @@ pub(crate) async fn post( let next = mas_router::AccountVerifyEmail::new(user_email.id).and_maybe(query.post_auth_action); - let session = repo.browser_session().add(&mut rng, &clock, &user).await?; + let session = repo + .browser_session() + .add(&mut rng, &clock, &user, user_agent) + .await?; repo.browser_session() .authenticate_with_password(&mut rng, &clock, &session, &user_password) diff --git a/crates/storage-pg/.sqlx/query-73fe61f03a41778c6273b1c2dbdb13b91fbccfe5fbdbead8c4868d52a61a0f9d.json b/crates/storage-pg/.sqlx/query-b98052065469a71643bb332cbeb07e0e43c620ffa1a592eb45ab326e0064efa8.json similarity index 62% rename from crates/storage-pg/.sqlx/query-73fe61f03a41778c6273b1c2dbdb13b91fbccfe5fbdbead8c4868d52a61a0f9d.json rename to crates/storage-pg/.sqlx/query-b98052065469a71643bb332cbeb07e0e43c620ffa1a592eb45ab326e0064efa8.json index 14662495..e1cce155 100644 --- a/crates/storage-pg/.sqlx/query-73fe61f03a41778c6273b1c2dbdb13b91fbccfe5fbdbead8c4868d52a61a0f9d.json +++ b/crates/storage-pg/.sqlx/query-b98052065469a71643bb332cbeb07e0e43c620ffa1a592eb45ab326e0064efa8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT s.user_session_id\n , s.created_at AS \"user_session_created_at\"\n , s.finished_at AS \"user_session_finished_at\"\n , u.user_id\n , u.username AS \"user_username\"\n , u.primary_user_email_id AS \"user_primary_user_email_id\"\n , u.created_at AS \"user_created_at\"\n , u.locked_at AS \"user_locked_at\"\n FROM user_sessions s\n INNER JOIN users u\n USING (user_id)\n WHERE s.user_session_id = $1\n ", + "query": "\n SELECT s.user_session_id\n , s.created_at AS \"user_session_created_at\"\n , s.finished_at AS \"user_session_finished_at\"\n , s.user_agent AS \"user_session_user_agent\"\n , u.user_id\n , u.username AS \"user_username\"\n , u.primary_user_email_id AS \"user_primary_user_email_id\"\n , u.created_at AS \"user_created_at\"\n , u.locked_at AS \"user_locked_at\"\n FROM user_sessions s\n INNER JOIN users u\n USING (user_id)\n WHERE s.user_session_id = $1\n ", "describe": { "columns": [ { @@ -20,26 +20,31 @@ }, { "ordinal": 3, + "name": "user_session_user_agent", + "type_info": "Text" + }, + { + "ordinal": 4, "name": "user_id", "type_info": "Uuid" }, { - "ordinal": 4, + "ordinal": 5, "name": "user_username", "type_info": "Text" }, { - "ordinal": 5, + "ordinal": 6, "name": "user_primary_user_email_id", "type_info": "Uuid" }, { - "ordinal": 6, + "ordinal": 7, "name": "user_created_at", "type_info": "Timestamptz" }, { - "ordinal": 7, + "ordinal": 8, "name": "user_locked_at", "type_info": "Timestamptz" } @@ -53,6 +58,7 @@ false, false, true, + true, false, false, true, @@ -60,5 +66,5 @@ true ] }, - "hash": "73fe61f03a41778c6273b1c2dbdb13b91fbccfe5fbdbead8c4868d52a61a0f9d" + "hash": "b98052065469a71643bb332cbeb07e0e43c620ffa1a592eb45ab326e0064efa8" } diff --git a/crates/storage-pg/.sqlx/query-c1d90a7f2287ec779c81a521fab19e5ede3fa95484033e0312c30d9b6ecc03f0.json b/crates/storage-pg/.sqlx/query-f41f76c94cd68fca2285b1cc60f426603c84df4ef1c6ce5dc441a63d2dc46f6e.json similarity index 52% rename from crates/storage-pg/.sqlx/query-c1d90a7f2287ec779c81a521fab19e5ede3fa95484033e0312c30d9b6ecc03f0.json rename to crates/storage-pg/.sqlx/query-f41f76c94cd68fca2285b1cc60f426603c84df4ef1c6ce5dc441a63d2dc46f6e.json index e148ad91..e112b131 100644 --- a/crates/storage-pg/.sqlx/query-c1d90a7f2287ec779c81a521fab19e5ede3fa95484033e0312c30d9b6ecc03f0.json +++ b/crates/storage-pg/.sqlx/query-f41f76c94cd68fca2285b1cc60f426603c84df4ef1c6ce5dc441a63d2dc46f6e.json @@ -1,16 +1,17 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO user_sessions (user_session_id, user_id, created_at)\n VALUES ($1, $2, $3)\n ", + "query": "\n INSERT INTO user_sessions (user_session_id, user_id, created_at, user_agent)\n VALUES ($1, $2, $3, $4)\n ", "describe": { "columns": [], "parameters": { "Left": [ "Uuid", "Uuid", - "Timestamptz" + "Timestamptz", + "Text" ] }, "nullable": [] }, - "hash": "c1d90a7f2287ec779c81a521fab19e5ede3fa95484033e0312c30d9b6ecc03f0" + "hash": "f41f76c94cd68fca2285b1cc60f426603c84df4ef1c6ce5dc441a63d2dc46f6e" } diff --git a/crates/storage-pg/migrations/20230829141928_user_session_user_agent.sql b/crates/storage-pg/migrations/20230829141928_user_session_user_agent.sql new file mode 100644 index 00000000..04562606 --- /dev/null +++ b/crates/storage-pg/migrations/20230829141928_user_session_user_agent.sql @@ -0,0 +1,16 @@ +-- Copyright 2023 The Matrix.org Foundation C.I.C. +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- This adds a user_agent column to the user_sessions table +ALTER TABLE user_sessions ADD COLUMN user_agent TEXT; \ No newline at end of file diff --git a/crates/storage-pg/src/iden.rs b/crates/storage-pg/src/iden.rs index 5c594cf2..9f9d8599 100644 --- a/crates/storage-pg/src/iden.rs +++ b/crates/storage-pg/src/iden.rs @@ -18,9 +18,10 @@ pub enum UserSessions { Table, UserSessionId, + UserId, CreatedAt, FinishedAt, - UserId, + UserAgent, } #[derive(sea_query::Iden)] diff --git a/crates/storage-pg/src/oauth2/mod.rs b/crates/storage-pg/src/oauth2/mod.rs index 1b00c8e2..f73380d5 100644 --- a/crates/storage-pg/src/oauth2/mod.rs +++ b/crates/storage-pg/src/oauth2/mod.rs @@ -177,7 +177,7 @@ mod tests { .unwrap(); let user_session = repo .browser_session() - .add(&mut rng, &clock, &user) + .add(&mut rng, &clock, &user, None) .await .unwrap(); @@ -387,7 +387,7 @@ mod tests { .unwrap(); let user1_session = repo .browser_session() - .add(&mut rng, &clock, &user1) + .add(&mut rng, &clock, &user1, None) .await .unwrap(); @@ -398,7 +398,7 @@ mod tests { .unwrap(); let user2_session = repo .browser_session() - .add(&mut rng, &clock, &user2) + .add(&mut rng, &clock, &user2, None) .await .unwrap(); diff --git a/crates/storage-pg/src/user/session.rs b/crates/storage-pg/src/user/session.rs index bc838d7e..80f09962 100644 --- a/crates/storage-pg/src/user/session.rs +++ b/crates/storage-pg/src/user/session.rs @@ -53,6 +53,7 @@ struct SessionLookup { user_session_id: Uuid, user_session_created_at: DateTime, user_session_finished_at: Option>, + user_session_user_agent: Option, user_id: Uuid, user_username: String, user_primary_user_email_id: Option, @@ -79,6 +80,7 @@ impl TryFrom for BrowserSession { user, created_at: value.user_session_created_at, finished_at: value.user_session_finished_at, + user_agent: value.user_session_user_agent, }) } } @@ -139,6 +141,7 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> { SELECT s.user_session_id , s.created_at AS "user_session_created_at" , s.finished_at AS "user_session_finished_at" + , s.user_agent AS "user_session_user_agent" , u.user_id , u.username AS "user_username" , u.primary_user_email_id AS "user_primary_user_email_id" @@ -175,6 +178,7 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> { rng: &mut (dyn RngCore + Send), clock: &dyn Clock, user: &User, + user_agent: Option, ) -> Result { let created_at = clock.now(); let id = Ulid::from_datetime_with_source(created_at.into(), rng); @@ -182,12 +186,13 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> { sqlx::query!( r#" - INSERT INTO user_sessions (user_session_id, user_id, created_at) - VALUES ($1, $2, $3) + INSERT INTO user_sessions (user_session_id, user_id, created_at, user_agent) + VALUES ($1, $2, $3, $4) "#, Uuid::from(id), Uuid::from(user.id), created_at, + user_agent, ) .traced() .execute(&mut *self.conn) @@ -199,6 +204,7 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> { user: user.clone(), created_at, finished_at: None, + user_agent, }; Ok(session) @@ -265,6 +271,10 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> { Expr::col((UserSessions::Table, UserSessions::FinishedAt)), SessionLookupIden::UserSessionFinishedAt, ) + .expr_as( + Expr::col((UserSessions::Table, UserSessions::UserAgent)), + SessionLookupIden::UserSessionUserAgent, + ) .expr_as( Expr::col((Users::Table, Users::UserId)), SessionLookupIden::UserId, diff --git a/crates/storage-pg/src/user/tests.rs b/crates/storage-pg/src/user/tests.rs index cf46bfa6..5ea30c74 100644 --- a/crates/storage-pg/src/user/tests.rs +++ b/crates/storage-pg/src/user/tests.rs @@ -433,7 +433,7 @@ async fn test_user_session(pool: PgPool) { let session = repo .browser_session() - .add(&mut rng, &clock, &user) + .add(&mut rng, &clock, &user, None) .await .unwrap(); assert_eq!(session.user.id, user.id); diff --git a/crates/storage/src/user/session.rs b/crates/storage/src/user/session.rs index d86a3b5e..e4fc6814 100644 --- a/crates/storage/src/user/session.rs +++ b/crates/storage/src/user/session.rs @@ -114,6 +114,7 @@ pub trait BrowserSessionRepository: Send + Sync { /// * `rng`: The random number generator to use /// * `clock`: The clock used to generate timestamps /// * `user`: The user to create the session for + /// * `user_agent`: If available, the user agent of the browser /// /// # Errors /// @@ -123,6 +124,7 @@ pub trait BrowserSessionRepository: Send + Sync { rng: &mut (dyn RngCore + Send), clock: &dyn Clock, user: &User, + user_agent: Option, ) -> Result; /// Finish a [`BrowserSession`] @@ -234,6 +236,7 @@ repository_impl!(BrowserSessionRepository: rng: &mut (dyn RngCore + Send), clock: &dyn Clock, user: &User, + user_agent: Option, ) -> Result; async fn finish( &mut self,