1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-31 09:24:31 +03:00

Store the browser user-agent when starting a browser session

This commit is contained in:
Quentin Gliech
2023-08-29 16:45:42 +02:00
parent 1849b86a7d
commit 5d3b8cd92f
16 changed files with 87 additions and 26 deletions

View File

@ -80,6 +80,7 @@ pub struct BrowserSession {
pub user: User, pub user: User,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub finished_at: Option<DateTime<Utc>>, pub finished_at: Option<DateTime<Utc>>,
pub user_agent: Option<String>,
} }
impl BrowserSession { impl BrowserSession {
@ -99,6 +100,7 @@ impl BrowserSession {
user, user,
created_at: now, created_at: now,
finished_at: None, finished_at: None,
user_agent: Some("Mozilla/5.0".to_owned()),
}) })
.collect() .collect()
} }

View File

@ -84,7 +84,7 @@ async fn start_oauth_session(
let browser_session = repo let browser_session = repo
.browser_session() .browser_session()
.add(&mut rng, &state.clock, user) .add(&mut rng, &state.clock, user, None)
.await .await
.unwrap(); .unwrap();

View File

@ -421,7 +421,7 @@ mod tests {
let browser_session = repo let browser_session = repo
.browser_session() .browser_session()
.add(&mut state.rng(), &state.clock, &user) .add(&mut state.rng(), &state.clock, &user, None)
.await .await
.unwrap(); .unwrap();

View File

@ -284,7 +284,7 @@ mod tests {
let browser_session = repo let browser_session = repo
.browser_session() .browser_session()
.add(&mut state.rng(), &state.clock, &user) .add(&mut state.rng(), &state.clock, &user, None)
.await .await
.unwrap(); .unwrap();

View File

@ -464,7 +464,7 @@ mod tests {
let browser_session = repo let browser_session = repo
.browser_session() .browser_session()
.add(&mut state.rng(), &state.clock, &user) .add(&mut state.rng(), &state.clock, &user, None)
.await .await
.unwrap(); .unwrap();
@ -672,7 +672,7 @@ mod tests {
let browser_session = repo let browser_session = repo
.browser_session() .browser_session()
.add(&mut state.rng(), &state.clock, &user) .add(&mut state.rng(), &state.clock, &user, None)
.await .await
.unwrap(); .unwrap();

View File

@ -15,7 +15,7 @@
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
response::{Html, IntoResponse}, response::{Html, IntoResponse},
Form, Form, TypedHeader,
}; };
use hyper::StatusCode; use hyper::StatusCode;
use mas_axum_utils::{ use mas_axum_utils::{
@ -170,8 +170,10 @@ pub(crate) async fn get(
mut repo: BoxRepository, mut repo: BoxRepository,
State(templates): State<Templates>, State(templates): State<Templates>,
cookie_jar: CookieJar, cookie_jar: CookieJar,
user_agent: Option<TypedHeader<headers::UserAgent>>,
Path(link_id): Path<Ulid>, Path(link_id): Path<Ulid>,
) -> Result<impl IntoResponse, RouteError> { ) -> Result<impl IntoResponse, RouteError> {
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar); let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar);
let (session_id, post_auth_action) = sessions_cookie let (session_id, post_auth_action) = sessions_cookie
.lookup_link(link_id) .lookup_link(link_id)
@ -264,7 +266,10 @@ pub(crate) async fn get(
.filter(mas_data_model::User::is_valid) .filter(mas_data_model::User::is_valid)
.ok_or(RouteError::UserNotFound)?; .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 let upstream_session = repo
.upstream_oauth_session() .upstream_oauth_session()
@ -352,9 +357,11 @@ pub(crate) async fn post(
clock: BoxClock, clock: BoxClock,
mut repo: BoxRepository, mut repo: BoxRepository,
cookie_jar: CookieJar, cookie_jar: CookieJar,
user_agent: Option<TypedHeader<headers::UserAgent>>,
Path(link_id): Path<Ulid>, Path(link_id): Path<Ulid>,
Form(form): Form<ProtectedForm<FormData>>, Form(form): Form<ProtectedForm<FormData>>,
) -> Result<impl IntoResponse, RouteError> { ) -> Result<impl IntoResponse, RouteError> {
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
let form = cookie_jar.verify_form(&clock, form)?; let form = cookie_jar.verify_form(&clock, form)?;
let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar); let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar);
@ -503,7 +510,9 @@ pub(crate) async fn post(
.associate_to_user(&link, &user) .associate_to_user(&link, &user)
.await?; .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), _ => return Err(RouteError::InvalidFormAction),

View File

@ -15,7 +15,9 @@
use axum::{ use axum::{
extract::{Form, Query, State}, extract::{Form, Query, State},
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
TypedHeader,
}; };
use headers::UserAgent;
use hyper::StatusCode; use hyper::StatusCode;
use mas_axum_utils::{ use mas_axum_utils::{
cookies::CookieJar, cookies::CookieJar,
@ -109,8 +111,10 @@ pub(crate) async fn post(
mut repo: BoxRepository, mut repo: BoxRepository,
Query(query): Query<OptionalPostAuthAction>, Query(query): Query<OptionalPostAuthAction>,
cookie_jar: CookieJar, cookie_jar: CookieJar,
user_agent: Option<TypedHeader<UserAgent>>,
Form(form): Form<ProtectedForm<LoginForm>>, Form(form): Form<ProtectedForm<LoginForm>>,
) -> Result<Response, FancyError> { ) -> Result<Response, FancyError> {
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
if !password_manager.is_enabled() { if !password_manager.is_enabled() {
// XXX: is it necessary to have better errors here? // XXX: is it necessary to have better errors here?
return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response()); return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response());
@ -158,6 +162,7 @@ pub(crate) async fn post(
&clock, &clock,
&form.username, &form.username,
&form.password, &form.password,
user_agent,
) )
.await .await
{ {
@ -193,6 +198,7 @@ async fn login(
clock: &impl Clock, clock: &impl Clock,
username: &str, username: &str,
password: &str, password: &str,
user_agent: Option<String>,
) -> Result<BrowserSession, FormError> { ) -> Result<BrowserSession, FormError> {
// XXX: we're loosing the error context here // XXX: we're loosing the error context here
// First, lookup the user // First, lookup the user
@ -245,7 +251,7 @@ async fn login(
// Start a new session // Start a new session
let user_session = repo let user_session = repo
.browser_session() .browser_session()
.add(&mut rng, clock, &user) .add(&mut rng, clock, &user, user_agent)
.await .await
.map_err(|_| FormError::Internal)?; .map_err(|_| FormError::Internal)?;

View File

@ -17,7 +17,9 @@ use std::{str::FromStr, sync::Arc};
use axum::{ use axum::{
extract::{Form, Query, State}, extract::{Form, Query, State},
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
TypedHeader,
}; };
use headers::UserAgent;
use hyper::StatusCode; use hyper::StatusCode;
use lettre::Address; use lettre::Address;
use mas_axum_utils::{ use mas_axum_utils::{
@ -104,8 +106,10 @@ pub(crate) async fn post(
mut repo: BoxRepository, mut repo: BoxRepository,
Query(query): Query<OptionalPostAuthAction>, Query(query): Query<OptionalPostAuthAction>,
cookie_jar: CookieJar, cookie_jar: CookieJar,
user_agent: Option<TypedHeader<UserAgent>>,
Form(form): Form<ProtectedForm<RegisterForm>>, Form(form): Form<ProtectedForm<RegisterForm>>,
) -> Result<Response, FancyError> { ) -> Result<Response, FancyError> {
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
if !password_manager.is_enabled() { if !password_manager.is_enabled() {
return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response()); 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 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() repo.browser_session()
.authenticate_with_password(&mut rng, &clock, &session, &user_password) .authenticate_with_password(&mut rng, &clock, &session, &user_password)

View File

@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "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": { "describe": {
"columns": [ "columns": [
{ {
@ -20,26 +20,31 @@
}, },
{ {
"ordinal": 3, "ordinal": 3,
"name": "user_session_user_agent",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "user_id", "name": "user_id",
"type_info": "Uuid" "type_info": "Uuid"
}, },
{ {
"ordinal": 4, "ordinal": 5,
"name": "user_username", "name": "user_username",
"type_info": "Text" "type_info": "Text"
}, },
{ {
"ordinal": 5, "ordinal": 6,
"name": "user_primary_user_email_id", "name": "user_primary_user_email_id",
"type_info": "Uuid" "type_info": "Uuid"
}, },
{ {
"ordinal": 6, "ordinal": 7,
"name": "user_created_at", "name": "user_created_at",
"type_info": "Timestamptz" "type_info": "Timestamptz"
}, },
{ {
"ordinal": 7, "ordinal": 8,
"name": "user_locked_at", "name": "user_locked_at",
"type_info": "Timestamptz" "type_info": "Timestamptz"
} }
@ -53,6 +58,7 @@
false, false,
false, false,
true, true,
true,
false, false,
false, false,
true, true,
@ -60,5 +66,5 @@
true true
] ]
}, },
"hash": "73fe61f03a41778c6273b1c2dbdb13b91fbccfe5fbdbead8c4868d52a61a0f9d" "hash": "b98052065469a71643bb332cbeb07e0e43c620ffa1a592eb45ab326e0064efa8"
} }

View File

@ -1,16 +1,17 @@
{ {
"db_name": "PostgreSQL", "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": { "describe": {
"columns": [], "columns": [],
"parameters": { "parameters": {
"Left": [ "Left": [
"Uuid", "Uuid",
"Uuid", "Uuid",
"Timestamptz" "Timestamptz",
"Text"
] ]
}, },
"nullable": [] "nullable": []
}, },
"hash": "c1d90a7f2287ec779c81a521fab19e5ede3fa95484033e0312c30d9b6ecc03f0" "hash": "f41f76c94cd68fca2285b1cc60f426603c84df4ef1c6ce5dc441a63d2dc46f6e"
} }

View File

@ -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;

View File

@ -18,9 +18,10 @@
pub enum UserSessions { pub enum UserSessions {
Table, Table,
UserSessionId, UserSessionId,
UserId,
CreatedAt, CreatedAt,
FinishedAt, FinishedAt,
UserId, UserAgent,
} }
#[derive(sea_query::Iden)] #[derive(sea_query::Iden)]

View File

@ -177,7 +177,7 @@ mod tests {
.unwrap(); .unwrap();
let user_session = repo let user_session = repo
.browser_session() .browser_session()
.add(&mut rng, &clock, &user) .add(&mut rng, &clock, &user, None)
.await .await
.unwrap(); .unwrap();
@ -387,7 +387,7 @@ mod tests {
.unwrap(); .unwrap();
let user1_session = repo let user1_session = repo
.browser_session() .browser_session()
.add(&mut rng, &clock, &user1) .add(&mut rng, &clock, &user1, None)
.await .await
.unwrap(); .unwrap();
@ -398,7 +398,7 @@ mod tests {
.unwrap(); .unwrap();
let user2_session = repo let user2_session = repo
.browser_session() .browser_session()
.add(&mut rng, &clock, &user2) .add(&mut rng, &clock, &user2, None)
.await .await
.unwrap(); .unwrap();

View File

@ -53,6 +53,7 @@ struct SessionLookup {
user_session_id: Uuid, user_session_id: Uuid,
user_session_created_at: DateTime<Utc>, user_session_created_at: DateTime<Utc>,
user_session_finished_at: Option<DateTime<Utc>>, user_session_finished_at: Option<DateTime<Utc>>,
user_session_user_agent: Option<String>,
user_id: Uuid, user_id: Uuid,
user_username: String, user_username: String,
user_primary_user_email_id: Option<Uuid>, user_primary_user_email_id: Option<Uuid>,
@ -79,6 +80,7 @@ impl TryFrom<SessionLookup> for BrowserSession {
user, user,
created_at: value.user_session_created_at, created_at: value.user_session_created_at,
finished_at: value.user_session_finished_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 SELECT s.user_session_id
, s.created_at AS "user_session_created_at" , s.created_at AS "user_session_created_at"
, s.finished_at AS "user_session_finished_at" , s.finished_at AS "user_session_finished_at"
, s.user_agent AS "user_session_user_agent"
, u.user_id , u.user_id
, u.username AS "user_username" , u.username AS "user_username"
, u.primary_user_email_id AS "user_primary_user_email_id" , 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), rng: &mut (dyn RngCore + Send),
clock: &dyn Clock, clock: &dyn Clock,
user: &User, user: &User,
user_agent: Option<String>,
) -> Result<BrowserSession, Self::Error> { ) -> Result<BrowserSession, Self::Error> {
let created_at = clock.now(); let created_at = clock.now();
let id = Ulid::from_datetime_with_source(created_at.into(), rng); let id = Ulid::from_datetime_with_source(created_at.into(), rng);
@ -182,12 +186,13 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
sqlx::query!( sqlx::query!(
r#" r#"
INSERT INTO user_sessions (user_session_id, user_id, created_at) INSERT INTO user_sessions (user_session_id, user_id, created_at, user_agent)
VALUES ($1, $2, $3) VALUES ($1, $2, $3, $4)
"#, "#,
Uuid::from(id), Uuid::from(id),
Uuid::from(user.id), Uuid::from(user.id),
created_at, created_at,
user_agent,
) )
.traced() .traced()
.execute(&mut *self.conn) .execute(&mut *self.conn)
@ -199,6 +204,7 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
user: user.clone(), user: user.clone(),
created_at, created_at,
finished_at: None, finished_at: None,
user_agent,
}; };
Ok(session) Ok(session)
@ -265,6 +271,10 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
Expr::col((UserSessions::Table, UserSessions::FinishedAt)), Expr::col((UserSessions::Table, UserSessions::FinishedAt)),
SessionLookupIden::UserSessionFinishedAt, SessionLookupIden::UserSessionFinishedAt,
) )
.expr_as(
Expr::col((UserSessions::Table, UserSessions::UserAgent)),
SessionLookupIden::UserSessionUserAgent,
)
.expr_as( .expr_as(
Expr::col((Users::Table, Users::UserId)), Expr::col((Users::Table, Users::UserId)),
SessionLookupIden::UserId, SessionLookupIden::UserId,

View File

@ -433,7 +433,7 @@ async fn test_user_session(pool: PgPool) {
let session = repo let session = repo
.browser_session() .browser_session()
.add(&mut rng, &clock, &user) .add(&mut rng, &clock, &user, None)
.await .await
.unwrap(); .unwrap();
assert_eq!(session.user.id, user.id); assert_eq!(session.user.id, user.id);

View File

@ -114,6 +114,7 @@ pub trait BrowserSessionRepository: Send + Sync {
/// * `rng`: The random number generator to use /// * `rng`: The random number generator to use
/// * `clock`: The clock used to generate timestamps /// * `clock`: The clock used to generate timestamps
/// * `user`: The user to create the session for /// * `user`: The user to create the session for
/// * `user_agent`: If available, the user agent of the browser
/// ///
/// # Errors /// # Errors
/// ///
@ -123,6 +124,7 @@ pub trait BrowserSessionRepository: Send + Sync {
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),
clock: &dyn Clock, clock: &dyn Clock,
user: &User, user: &User,
user_agent: Option<String>,
) -> Result<BrowserSession, Self::Error>; ) -> Result<BrowserSession, Self::Error>;
/// Finish a [`BrowserSession`] /// Finish a [`BrowserSession`]
@ -234,6 +236,7 @@ repository_impl!(BrowserSessionRepository:
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),
clock: &dyn Clock, clock: &dyn Clock,
user: &User, user: &User,
user_agent: Option<String>,
) -> Result<BrowserSession, Self::Error>; ) -> Result<BrowserSession, Self::Error>;
async fn finish( async fn finish(
&mut self, &mut self,