You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-08-07 17:03:01 +03:00
Parse User Agents on the backend side (#2388)
* Parse user agents on the server side * Parse and expose user agents on the backend * Use the parsed user agent in the device consent page * Fix the device icon tests * Fix clippy warnings * Box stuff to avoid large enum variants * Ignore a clippy warning * Fix the requester boxing
This commit is contained in:
@@ -20,6 +20,8 @@ crc = "3.0.1"
|
||||
ulid.workspace = true
|
||||
rand.workspace = true
|
||||
rand_chacha = "0.3.1"
|
||||
regex = "1.10.3"
|
||||
woothee = "0.13.0"
|
||||
|
||||
mas-iana.workspace = true
|
||||
mas-jose.workspace = true
|
||||
|
26
crates/data-model/examples/ua-parser.rs
Normal file
26
crates/data-model/examples/ua-parser.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright 2024 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.
|
||||
|
||||
use mas_data_model::UserAgent;
|
||||
|
||||
/// Simple command-line tool to try out user-agent parsing
|
||||
///
|
||||
/// It parses user-agents from stdin and prints the parsed user-agent to stdout.
|
||||
fn main() {
|
||||
for line in std::io::stdin().lines() {
|
||||
let user_agent = line.unwrap();
|
||||
let user_agent = UserAgent::parse(user_agent);
|
||||
println!("{user_agent:?}");
|
||||
}
|
||||
}
|
@@ -19,7 +19,7 @@ use serde::Serialize;
|
||||
use ulid::Ulid;
|
||||
|
||||
use super::Device;
|
||||
use crate::InvalidTransitionError;
|
||||
use crate::{InvalidTransitionError, UserAgent};
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
|
||||
pub enum CompatSessionState {
|
||||
@@ -83,7 +83,7 @@ pub struct CompatSession {
|
||||
pub user_session_id: Option<Ulid>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub is_synapse_admin: bool,
|
||||
pub user_agent: Option<String>,
|
||||
pub user_agent: Option<UserAgent>,
|
||||
pub last_active_at: Option<DateTime<Utc>>,
|
||||
pub last_active_ip: Option<IpAddr>,
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ pub(crate) mod compat;
|
||||
pub(crate) mod oauth2;
|
||||
pub(crate) mod tokens;
|
||||
pub(crate) mod upstream_oauth2;
|
||||
pub(crate) mod user_agent;
|
||||
pub(crate) mod users;
|
||||
|
||||
/// Error when an invalid state transition is attempted.
|
||||
@@ -46,6 +47,7 @@ pub use self::{
|
||||
UpstreamOAuthProviderImportAction, UpstreamOAuthProviderImportPreference,
|
||||
UpstreamOAuthProviderPkceMode, UpstreamOAuthProviderSubjectPreference,
|
||||
},
|
||||
user_agent::{DeviceType, UserAgent},
|
||||
users::{
|
||||
Authentication, AuthenticationMethod, BrowserSession, Password, User, UserEmail,
|
||||
UserEmailVerification, UserEmailVerificationState,
|
||||
|
@@ -19,7 +19,7 @@ use oauth2_types::scope::Scope;
|
||||
use serde::Serialize;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{BrowserSession, InvalidTransitionError, Session};
|
||||
use crate::{BrowserSession, InvalidTransitionError, Session, UserAgent};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case", tag = "state")]
|
||||
@@ -200,7 +200,7 @@ pub struct DeviceCodeGrant {
|
||||
pub ip_address: Option<IpAddr>,
|
||||
|
||||
/// The user agent used to request this device code grant.
|
||||
pub user_agent: Option<String>,
|
||||
pub user_agent: Option<UserAgent>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for DeviceCodeGrant {
|
||||
|
@@ -19,7 +19,7 @@ use oauth2_types::scope::Scope;
|
||||
use serde::Serialize;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::InvalidTransitionError;
|
||||
use crate::{InvalidTransitionError, UserAgent};
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
|
||||
pub enum SessionState {
|
||||
@@ -75,7 +75,7 @@ pub struct Session {
|
||||
pub user_session_id: Option<Ulid>,
|
||||
pub client_id: Ulid,
|
||||
pub scope: Scope,
|
||||
pub user_agent: Option<String>,
|
||||
pub user_agent: Option<UserAgent>,
|
||||
pub last_active_at: Option<DateTime<Utc>>,
|
||||
pub last_active_ip: Option<IpAddr>,
|
||||
}
|
||||
|
217
crates/data-model/src/user_agent.rs
Normal file
217
crates/data-model/src/user_agent.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
// Copyright 2024 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.
|
||||
|
||||
use serde::Serialize;
|
||||
use woothee::{parser::Parser, woothee::VALUE_UNKNOWN};
|
||||
|
||||
#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DeviceType {
|
||||
Pc,
|
||||
Mobile,
|
||||
Tablet,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct UserAgent {
|
||||
pub name: Option<String>,
|
||||
pub version: Option<String>,
|
||||
pub os: Option<String>,
|
||||
pub os_version: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub device_type: DeviceType,
|
||||
pub raw: String,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for UserAgent {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.raw
|
||||
}
|
||||
}
|
||||
|
||||
impl UserAgent {
|
||||
fn parse_custom(user_agent: &str) -> Option<(&str, &str, &str, &str, Option<&str>)> {
|
||||
let regex = regex::Regex::new(r"^(?P<name>[^/]+)/(?P<version>[^ ]+) \((?P<segments>.+)\)$")
|
||||
.unwrap();
|
||||
|
||||
let captures = regex.captures(user_agent)?;
|
||||
let name = captures.name("name")?.as_str();
|
||||
let version = captures.name("version")?.as_str();
|
||||
let segments: Vec<&str> = captures
|
||||
.name("segments")?
|
||||
.as_str()
|
||||
.split(';')
|
||||
.map(str::trim)
|
||||
.collect();
|
||||
|
||||
match segments[..] {
|
||||
["Linux", "U", os, model, ..] | [model, os, ..] => {
|
||||
// Most android model have a `/[build version]` suffix we don't care about
|
||||
let model = model.split_once('/').map_or(model, |(model, _)| model);
|
||||
// Some android version also have `Build/[build version]` suffix we don't care
|
||||
// about
|
||||
let model = model.strip_suffix("Build").unwrap_or(model);
|
||||
// And let's trim any leftovers
|
||||
let model = model.trim();
|
||||
|
||||
let (os, os_version) = if let Some((os, version)) = os.split_once(' ') {
|
||||
(os, Some(version))
|
||||
} else {
|
||||
(os, None)
|
||||
};
|
||||
|
||||
Some((name, version, model, os, os_version))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn parse(user_agent: String) -> Self {
|
||||
if !user_agent.contains("Mozilla/") {
|
||||
if let Some((name, version, model, os, os_version)) =
|
||||
UserAgent::parse_custom(&user_agent)
|
||||
{
|
||||
let mut device_type = DeviceType::Unknown;
|
||||
|
||||
// Handle mobile simple mobile devices
|
||||
if os == "Android" || os == "iOS" {
|
||||
device_type = DeviceType::Mobile;
|
||||
}
|
||||
|
||||
// Handle iPads
|
||||
if model.contains("iPad") {
|
||||
device_type = DeviceType::Tablet;
|
||||
}
|
||||
|
||||
return Self {
|
||||
name: Some(name.to_owned()),
|
||||
version: Some(version.to_owned()),
|
||||
os: Some(os.to_owned()),
|
||||
os_version: os_version.map(std::borrow::ToOwned::to_owned),
|
||||
model: Some(model.to_owned()),
|
||||
device_type,
|
||||
raw: user_agent,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let mut model = None;
|
||||
let Some(mut result) = Parser::new().parse(&user_agent) else {
|
||||
return Self {
|
||||
raw: user_agent,
|
||||
name: None,
|
||||
version: None,
|
||||
os: None,
|
||||
os_version: None,
|
||||
model: None,
|
||||
device_type: DeviceType::Unknown,
|
||||
};
|
||||
};
|
||||
|
||||
let mut device_type = match result.category {
|
||||
"pc" => DeviceType::Pc,
|
||||
"smartphone" | "mobilephone" => DeviceType::Mobile,
|
||||
_ => DeviceType::Unknown,
|
||||
};
|
||||
|
||||
// Special handling for Chrome user-agent reduction cases
|
||||
// https://www.chromium.org/updates/ua-reduction/
|
||||
match (result.os, &*result.os_version) {
|
||||
// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/533.88 (KHTML, like Gecko)
|
||||
// Chrome/109.1.2342.76 Safari/533.88
|
||||
("Windows 10", "NT 10.0") if user_agent.contains("Windows NT 10.0; Win64; x64") => {
|
||||
result.os = "Windows";
|
||||
result.os_version = VALUE_UNKNOWN.into();
|
||||
}
|
||||
|
||||
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like
|
||||
// Gecko) Chrome/100.0.4896.133 Safari/537.36
|
||||
("Mac OSX", "10.15.7") if user_agent.contains("Macintosh; Intel Mac OS X 10_15_7") => {
|
||||
result.os = "macOS";
|
||||
result.os_version = VALUE_UNKNOWN.into();
|
||||
}
|
||||
|
||||
// Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)
|
||||
// Chrome/100.0.0.0 Safari/537.36
|
||||
("Linux", _) if user_agent.contains("X11; Linux x86_64") => {
|
||||
result.os = "Linux";
|
||||
result.os_version = VALUE_UNKNOWN.into();
|
||||
}
|
||||
|
||||
// Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko)
|
||||
// Chrome/107.0.0.0 Safari/537.36
|
||||
("ChromeOS", _) if user_agent.contains("X11; CrOS x86_64 14541.0.0") => {
|
||||
result.os = "Chrome OS";
|
||||
result.os_version = VALUE_UNKNOWN.into();
|
||||
}
|
||||
|
||||
// Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko)
|
||||
// Chrome/100.0.0.0 Mobile Safari/537.36
|
||||
("Android", "10") if user_agent.contains("Linux; Android 10; K") => {
|
||||
result.os = "Android";
|
||||
result.os_version = VALUE_UNKNOWN.into();
|
||||
}
|
||||
|
||||
// Safari also freezes the OS version
|
||||
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like
|
||||
// Gecko) Version/17.3.1 Safari/605.1.15
|
||||
("Mac OSX", "10.15.7") if user_agent.contains("Macintosh; Intel Mac OS X 10_15_7") => {
|
||||
result.os = "macOS";
|
||||
result.os_version = VALUE_UNKNOWN.into();
|
||||
}
|
||||
|
||||
// Woothee identifies iPhone and iPod in the OS, but we want to map them to iOS and use
|
||||
// them as model
|
||||
("iPhone" | "iPod", _) => {
|
||||
model = Some(result.os.to_owned());
|
||||
result.os = "iOS";
|
||||
}
|
||||
|
||||
("iPad", _) => {
|
||||
model = Some(result.os.to_owned());
|
||||
device_type = DeviceType::Tablet;
|
||||
result.os = "iPadOS";
|
||||
}
|
||||
|
||||
// Also map `Mac OSX` to `macOS`
|
||||
("Mac OSX", _) => {
|
||||
result.os = "macOS";
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// For some reason, the version on Windows is on the OS field
|
||||
// This transforms `Windows 10` into `Windows` and `10`
|
||||
if let Some(version) = result.os.strip_prefix("Windows ") {
|
||||
result.os = "Windows";
|
||||
result.os_version = version.into();
|
||||
}
|
||||
|
||||
Self {
|
||||
name: (result.name != VALUE_UNKNOWN).then(|| result.name.to_owned()),
|
||||
version: (result.version != VALUE_UNKNOWN).then(|| result.version.to_owned()),
|
||||
os: (result.os != VALUE_UNKNOWN).then(|| result.os.to_owned()),
|
||||
os_version: (result.os_version != VALUE_UNKNOWN)
|
||||
.then(|| result.os_version.into_owned()),
|
||||
device_type,
|
||||
model,
|
||||
raw: user_agent,
|
||||
}
|
||||
}
|
||||
}
|
@@ -19,6 +19,8 @@ use rand::{Rng, SeedableRng};
|
||||
use serde::Serialize;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::UserAgent;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct User {
|
||||
pub id: Ulid,
|
||||
@@ -83,7 +85,7 @@ pub struct BrowserSession {
|
||||
pub user: User,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub finished_at: Option<DateTime<Utc>>,
|
||||
pub user_agent: Option<String>,
|
||||
pub user_agent: Option<UserAgent>,
|
||||
pub last_active_at: Option<DateTime<Utc>>,
|
||||
pub last_active_ip: Option<IpAddr>,
|
||||
}
|
||||
@@ -105,7 +107,9 @@ impl BrowserSession {
|
||||
user,
|
||||
created_at: now,
|
||||
finished_at: None,
|
||||
user_agent: Some("Mozilla/5.0".to_owned()),
|
||||
user_agent: Some(UserAgent::parse(
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36".to_owned()
|
||||
)),
|
||||
last_active_at: Some(now),
|
||||
last_active_ip: None,
|
||||
})
|
||||
|
@@ -49,10 +49,10 @@ pub enum Requester {
|
||||
Anonymous,
|
||||
|
||||
/// The requester is a browser session, stored in a cookie.
|
||||
BrowserSession(BrowserSession),
|
||||
BrowserSession(Box<BrowserSession>),
|
||||
|
||||
/// The requester is a OAuth2 session, with an access token.
|
||||
OAuth2Session(Session, Option<User>),
|
||||
OAuth2Session(Box<(Session, Option<User>)>),
|
||||
}
|
||||
|
||||
trait OwnerId {
|
||||
@@ -108,21 +108,21 @@ impl Requester {
|
||||
fn browser_session(&self) -> Option<&BrowserSession> {
|
||||
match self {
|
||||
Self::BrowserSession(session) => Some(session),
|
||||
Self::OAuth2Session(_, _) | Self::Anonymous => None,
|
||||
Self::OAuth2Session(_) | Self::Anonymous => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn user(&self) -> Option<&User> {
|
||||
match self {
|
||||
Self::BrowserSession(session) => Some(&session.user),
|
||||
Self::OAuth2Session(_session, user) => user.as_ref(),
|
||||
Self::OAuth2Session(tuple) => tuple.1.as_ref(),
|
||||
Self::Anonymous => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn oauth2_session(&self) -> Option<&Session> {
|
||||
match self {
|
||||
Self::OAuth2Session(session, _) => Some(session),
|
||||
Self::OAuth2Session(tuple) => Some(&tuple.0),
|
||||
Self::BrowserSession(_) | Self::Anonymous => None,
|
||||
}
|
||||
}
|
||||
@@ -148,10 +148,10 @@ impl Requester {
|
||||
|
||||
fn is_admin(&self) -> bool {
|
||||
match self {
|
||||
Self::OAuth2Session(session, _user) => {
|
||||
Self::OAuth2Session(tuple) => {
|
||||
// TODO: is this the right scope?
|
||||
// This has to be in sync with the policy
|
||||
session.scope.contains("urn:mas:admin")
|
||||
tuple.0.scope.contains("urn:mas:admin")
|
||||
}
|
||||
Self::BrowserSession(_) | Self::Anonymous => false,
|
||||
}
|
||||
@@ -160,7 +160,7 @@ impl Requester {
|
||||
|
||||
impl From<BrowserSession> for Requester {
|
||||
fn from(session: BrowserSession) -> Self {
|
||||
Self::BrowserSession(session)
|
||||
Self::BrowserSession(Box::new(session))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -24,7 +24,7 @@ use mas_storage::{
|
||||
|
||||
use super::{
|
||||
AppSession, CompatSession, Cursor, NodeCursor, NodeType, OAuth2Session, PreloadedTotalCount,
|
||||
SessionState, User,
|
||||
SessionState, User, UserAgent,
|
||||
};
|
||||
use crate::state::ContextExt;
|
||||
|
||||
@@ -87,9 +87,9 @@ impl BrowserSession {
|
||||
}
|
||||
}
|
||||
|
||||
/// The user-agent string with which the session was created.
|
||||
pub async fn user_agent(&self) -> Option<&str> {
|
||||
self.0.user_agent.as_deref()
|
||||
/// The user-agent with which the session was created.
|
||||
pub async fn user_agent(&self) -> Option<UserAgent> {
|
||||
self.0.user_agent.clone().map(UserAgent::from)
|
||||
}
|
||||
|
||||
/// The last IP address used by the session.
|
||||
|
@@ -18,7 +18,7 @@ use chrono::{DateTime, Utc};
|
||||
use mas_storage::{compat::CompatSessionRepository, user::UserRepository};
|
||||
use url::Url;
|
||||
|
||||
use super::{BrowserSession, NodeType, SessionState, User};
|
||||
use super::{BrowserSession, NodeType, SessionState, User, UserAgent};
|
||||
use crate::state::ContextExt;
|
||||
|
||||
/// Lazy-loaded reverse reference.
|
||||
@@ -103,6 +103,11 @@ impl CompatSession {
|
||||
self.session.finished_at()
|
||||
}
|
||||
|
||||
/// The user-agent with which the session was created.
|
||||
pub async fn user_agent(&self) -> Option<UserAgent> {
|
||||
self.session.user_agent.clone().map(UserAgent::from)
|
||||
}
|
||||
|
||||
/// The associated SSO login, if any.
|
||||
pub async fn sso_login(
|
||||
&self,
|
||||
|
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use async_graphql::{Enum, Interface, Object};
|
||||
use async_graphql::{Enum, Interface, Object, SimpleObject};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
mod browser_sessions;
|
||||
@@ -73,3 +73,69 @@ pub enum SessionState {
|
||||
/// The session is no longer active.
|
||||
Finished,
|
||||
}
|
||||
|
||||
/// The type of a user agent
|
||||
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum DeviceType {
|
||||
/// A personal computer, laptop or desktop
|
||||
Pc,
|
||||
|
||||
/// A mobile phone. Can also sometimes be a tablet.
|
||||
Mobile,
|
||||
|
||||
/// A tablet
|
||||
Tablet,
|
||||
|
||||
/// Unknown device type
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<mas_data_model::DeviceType> for DeviceType {
|
||||
fn from(device_type: mas_data_model::DeviceType) -> Self {
|
||||
match device_type {
|
||||
mas_data_model::DeviceType::Pc => Self::Pc,
|
||||
mas_data_model::DeviceType::Mobile => Self::Mobile,
|
||||
mas_data_model::DeviceType::Tablet => Self::Tablet,
|
||||
mas_data_model::DeviceType::Unknown => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A parsed user agent string
|
||||
#[derive(SimpleObject)]
|
||||
pub struct UserAgent {
|
||||
/// The user agent string
|
||||
pub raw: String,
|
||||
|
||||
/// The name of the browser
|
||||
pub name: Option<String>,
|
||||
|
||||
/// The version of the browser
|
||||
pub version: Option<String>,
|
||||
|
||||
/// The operating system name
|
||||
pub os: Option<String>,
|
||||
|
||||
/// The operating system version
|
||||
pub os_version: Option<String>,
|
||||
|
||||
/// The device model
|
||||
pub model: Option<String>,
|
||||
|
||||
/// The device type
|
||||
pub device_type: DeviceType,
|
||||
}
|
||||
|
||||
impl From<mas_data_model::UserAgent> for UserAgent {
|
||||
fn from(ua: mas_data_model::UserAgent) -> Self {
|
||||
Self {
|
||||
raw: ua.raw,
|
||||
name: ua.name,
|
||||
version: ua.version,
|
||||
os: ua.os,
|
||||
os_version: ua.os_version,
|
||||
model: ua.model,
|
||||
device_type: ua.device_type.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ use oauth2_types::{oidc::ApplicationType, scope::Scope};
|
||||
use ulid::Ulid;
|
||||
use url::Url;
|
||||
|
||||
use super::{BrowserSession, NodeType, SessionState, User};
|
||||
use super::{BrowserSession, NodeType, SessionState, User, UserAgent};
|
||||
use crate::{state::ContextExt, UserId};
|
||||
|
||||
/// An OAuth 2.0 session represents a client session which used the OAuth APIs
|
||||
@@ -67,6 +67,11 @@ impl OAuth2Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// The user-agent with which the session was created.
|
||||
pub async fn user_agent(&self) -> Option<UserAgent> {
|
||||
self.0.user_agent.clone().map(UserAgent::from)
|
||||
}
|
||||
|
||||
/// The state of the session.
|
||||
pub async fn state(&self) -> SessionState {
|
||||
match &self.0.state {
|
||||
|
@@ -40,7 +40,7 @@ pub struct EndCompatSessionInput {
|
||||
/// The payload of the `endCompatSession` mutation.
|
||||
pub enum EndCompatSessionPayload {
|
||||
NotFound,
|
||||
Ended(mas_data_model::CompatSession),
|
||||
Ended(Box<mas_data_model::CompatSession>),
|
||||
}
|
||||
|
||||
/// The status of the `endCompatSession` mutation.
|
||||
@@ -66,7 +66,7 @@ impl EndCompatSessionPayload {
|
||||
/// Returns the ended session.
|
||||
async fn compat_session(&self) -> Option<CompatSession> {
|
||||
match self {
|
||||
Self::Ended(session) => Some(CompatSession::new(session.clone())),
|
||||
Self::Ended(session) => Some(CompatSession::new(*session.clone())),
|
||||
Self::NotFound => None,
|
||||
}
|
||||
}
|
||||
@@ -110,6 +110,6 @@ impl CompatSessionMutations {
|
||||
|
||||
repo.save().await?;
|
||||
|
||||
Ok(EndCompatSessionPayload::Ended(session))
|
||||
Ok(EndCompatSessionPayload::Ended(Box::new(session)))
|
||||
}
|
||||
}
|
||||
|
@@ -31,8 +31,11 @@ impl ViewerQuery {
|
||||
|
||||
match requester {
|
||||
Requester::BrowserSession(session) => Viewer::user(session.user.clone()),
|
||||
Requester::OAuth2Session(_session, Some(user)) => Viewer::user(user.clone()),
|
||||
Requester::OAuth2Session(_, None) | Requester::Anonymous => Viewer::anonymous(),
|
||||
Requester::OAuth2Session(tuple) => match &tuple.1 {
|
||||
Some(user) => Viewer::user(user.clone()),
|
||||
None => Viewer::anonymous(),
|
||||
},
|
||||
Requester::Anonymous => Viewer::anonymous(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,10 +44,8 @@ impl ViewerQuery {
|
||||
let requester = ctx.requester();
|
||||
|
||||
match requester {
|
||||
Requester::BrowserSession(session) => ViewerSession::browser_session(session.clone()),
|
||||
Requester::OAuth2Session(session, _user) => {
|
||||
ViewerSession::oauth2_session(session.clone())
|
||||
}
|
||||
Requester::BrowserSession(session) => ViewerSession::browser_session(*session.clone()),
|
||||
Requester::OAuth2Session(tuple) => ViewerSession::oauth2_session(tuple.0.clone()),
|
||||
Requester::Anonymous => ViewerSession::anonymous(),
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ use axum::{extract::State, response::IntoResponse, Json, TypedHeader};
|
||||
use chrono::Duration;
|
||||
use hyper::StatusCode;
|
||||
use mas_axum_utils::sentry::SentryEventID;
|
||||
use mas_data_model::{CompatSession, CompatSsoLoginState, Device, TokenType, User};
|
||||
use mas_data_model::{CompatSession, CompatSsoLoginState, Device, TokenType, User, UserAgent};
|
||||
use mas_storage::{
|
||||
compat::{
|
||||
CompatAccessTokenRepository, CompatRefreshTokenRepository, CompatSessionRepository,
|
||||
@@ -220,7 +220,7 @@ pub(crate) async fn post(
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
Json(input): Json<RequestBody>,
|
||||
) -> Result<impl IntoResponse, RouteError> {
|
||||
let user_agent = user_agent.map(|ua| ua.to_string());
|
||||
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
|
||||
let (mut session, user) = match (password_manager.is_enabled(), input.credentials) {
|
||||
(
|
||||
true,
|
||||
|
@@ -237,7 +237,7 @@ async fn get_requester(
|
||||
return Err(RouteError::MissingScope);
|
||||
}
|
||||
|
||||
Requester::OAuth2Session(session, user)
|
||||
Requester::OAuth2Session(Box::new((session, user)))
|
||||
} else {
|
||||
let maybe_session = session_info.load_session(&mut repo).await?;
|
||||
|
||||
|
@@ -14,13 +14,14 @@
|
||||
|
||||
use axum::{extract::State, response::IntoResponse, Json, TypedHeader};
|
||||
use chrono::Duration;
|
||||
use headers::{CacheControl, Pragma, UserAgent};
|
||||
use headers::{CacheControl, Pragma};
|
||||
use hyper::StatusCode;
|
||||
use mas_axum_utils::{
|
||||
client_authorization::{ClientAuthorization, CredentialsVerificationError},
|
||||
http_client_factory::HttpClientFactory,
|
||||
sentry::SentryEventID,
|
||||
};
|
||||
use mas_data_model::UserAgent;
|
||||
use mas_keystore::Encrypter;
|
||||
use mas_router::UrlBuilder;
|
||||
use mas_storage::{oauth2::OAuth2DeviceCodeGrantParams, BoxClock, BoxRepository, BoxRng};
|
||||
@@ -84,7 +85,7 @@ pub(crate) async fn post(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
mut repo: BoxRepository,
|
||||
user_agent: Option<TypedHeader<UserAgent>>,
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
State(http_client_factory): State<HttpClientFactory>,
|
||||
@@ -125,7 +126,7 @@ pub(crate) async fn post(
|
||||
|
||||
let expires_in = Duration::minutes(20);
|
||||
|
||||
let user_agent = user_agent.map(|ua| ua.0.to_string());
|
||||
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
|
||||
let ip_address = activity_tracker.ip();
|
||||
|
||||
let device_code = Alphanumeric.sample_string(&mut rng, 32);
|
||||
|
@@ -21,7 +21,9 @@ use mas_axum_utils::{
|
||||
http_client_factory::HttpClientFactory,
|
||||
sentry::SentryEventID,
|
||||
};
|
||||
use mas_data_model::{AuthorizationGrantStage, Client, Device, DeviceCodeGrantState, TokenType};
|
||||
use mas_data_model::{
|
||||
AuthorizationGrantStage, Client, Device, DeviceCodeGrantState, TokenType, UserAgent,
|
||||
};
|
||||
use mas_keystore::{Encrypter, Keystore};
|
||||
use mas_oidc_client::types::scope::ScopeToken;
|
||||
use mas_policy::Policy;
|
||||
@@ -233,7 +235,7 @@ pub(crate) async fn post(
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
client_authorization: ClientAuthorization<AccessTokenRequest>,
|
||||
) -> Result<impl IntoResponse, RouteError> {
|
||||
let user_agent = user_agent.map(|ua| ua.to_string());
|
||||
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
|
||||
let client = client_authorization
|
||||
.credentials
|
||||
.fetch(&mut repo)
|
||||
@@ -335,7 +337,7 @@ async fn authorization_code_grant(
|
||||
url_builder: &UrlBuilder,
|
||||
site_config: &SiteConfig,
|
||||
mut repo: BoxRepository,
|
||||
user_agent: Option<String>,
|
||||
user_agent: Option<UserAgent>,
|
||||
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
|
||||
// Check that the client is allowed to use this grant type
|
||||
if !client.grant_types.contains(&GrantType::AuthorizationCode) {
|
||||
@@ -504,7 +506,7 @@ async fn refresh_token_grant(
|
||||
client: &Client,
|
||||
site_config: &SiteConfig,
|
||||
mut repo: BoxRepository,
|
||||
user_agent: Option<String>,
|
||||
user_agent: Option<UserAgent>,
|
||||
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
|
||||
// Check that the client is allowed to use this grant type
|
||||
if !client.grant_types.contains(&GrantType::RefreshToken) {
|
||||
@@ -587,7 +589,7 @@ async fn client_credentials_grant(
|
||||
site_config: &SiteConfig,
|
||||
mut repo: BoxRepository,
|
||||
mut policy: Policy,
|
||||
user_agent: Option<String>,
|
||||
user_agent: Option<UserAgent>,
|
||||
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
|
||||
// Check that the client is allowed to use this grant type
|
||||
if !client.grant_types.contains(&GrantType::ClientCredentials) {
|
||||
@@ -656,7 +658,7 @@ async fn device_code_grant(
|
||||
url_builder: &UrlBuilder,
|
||||
site_config: &SiteConfig,
|
||||
mut repo: BoxRepository,
|
||||
user_agent: Option<String>,
|
||||
user_agent: Option<UserAgent>,
|
||||
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
|
||||
// Check that the client is allowed to use this grant type
|
||||
if !client.grant_types.contains(&GrantType::DeviceCode) {
|
||||
|
@@ -24,7 +24,7 @@ use mas_axum_utils::{
|
||||
sentry::SentryEventID,
|
||||
FancyError, SessionInfoExt,
|
||||
};
|
||||
use mas_data_model::User;
|
||||
use mas_data_model::{User, UserAgent};
|
||||
use mas_jose::jwt::Jwt;
|
||||
use mas_policy::Policy;
|
||||
use mas_router::UrlBuilder;
|
||||
@@ -200,7 +200,7 @@ pub(crate) async fn get(
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
Path(link_id): Path<Ulid>,
|
||||
) -> Result<impl IntoResponse, RouteError> {
|
||||
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
|
||||
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
|
||||
let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar);
|
||||
let (session_id, post_auth_action) = sessions_cookie
|
||||
.lookup_link(link_id)
|
||||
@@ -481,7 +481,7 @@ pub(crate) async fn post(
|
||||
Path(link_id): Path<Ulid>,
|
||||
Form(form): Form<ProtectedForm<FormData>>,
|
||||
) -> Result<Response, RouteError> {
|
||||
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
|
||||
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
|
||||
let form = cookie_jar.verify_form(&clock, form)?;
|
||||
|
||||
let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar);
|
||||
|
@@ -17,14 +17,13 @@ use axum::{
|
||||
response::{Html, IntoResponse, Response},
|
||||
TypedHeader,
|
||||
};
|
||||
use headers::UserAgent;
|
||||
use hyper::StatusCode;
|
||||
use mas_axum_utils::{
|
||||
cookies::CookieJar,
|
||||
csrf::{CsrfExt, CsrfToken, ProtectedForm},
|
||||
FancyError, SessionInfoExt,
|
||||
};
|
||||
use mas_data_model::BrowserSession;
|
||||
use mas_data_model::{BrowserSession, UserAgent};
|
||||
use mas_i18n::DataLocale;
|
||||
use mas_router::{UpstreamOAuth2Authorize, UrlBuilder};
|
||||
use mas_storage::{
|
||||
@@ -123,10 +122,10 @@ pub(crate) async fn post(
|
||||
activity_tracker: BoundActivityTracker,
|
||||
Query(query): Query<OptionalPostAuthAction>,
|
||||
cookie_jar: CookieJar,
|
||||
user_agent: Option<TypedHeader<UserAgent>>,
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
Form(form): Form<ProtectedForm<LoginForm>>,
|
||||
) -> Result<Response, FancyError> {
|
||||
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
|
||||
let user_agent = user_agent.map(|ua| UserAgent::parse(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());
|
||||
@@ -216,7 +215,7 @@ async fn login(
|
||||
clock: &impl Clock,
|
||||
username: &str,
|
||||
password: &str,
|
||||
user_agent: Option<String>,
|
||||
user_agent: Option<UserAgent>,
|
||||
) -> Result<BrowserSession, FormError> {
|
||||
// XXX: we're loosing the error context here
|
||||
// First, lookup the user
|
||||
|
@@ -19,7 +19,6 @@ use axum::{
|
||||
response::{Html, IntoResponse, Response},
|
||||
TypedHeader,
|
||||
};
|
||||
use headers::UserAgent;
|
||||
use hyper::StatusCode;
|
||||
use lettre::Address;
|
||||
use mas_axum_utils::{
|
||||
@@ -27,6 +26,7 @@ use mas_axum_utils::{
|
||||
csrf::{CsrfExt, CsrfToken, ProtectedForm},
|
||||
FancyError, SessionInfoExt,
|
||||
};
|
||||
use mas_data_model::UserAgent;
|
||||
use mas_i18n::DataLocale;
|
||||
use mas_policy::Policy;
|
||||
use mas_router::UrlBuilder;
|
||||
@@ -116,10 +116,10 @@ pub(crate) async fn post(
|
||||
activity_tracker: BoundActivityTracker,
|
||||
Query(query): Query<OptionalPostAuthAction>,
|
||||
cookie_jar: CookieJar,
|
||||
user_agent: Option<TypedHeader<UserAgent>>,
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
Form(form): Form<ProtectedForm<RegisterForm>>,
|
||||
) -> Result<Response, FancyError> {
|
||||
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
|
||||
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
|
||||
if !password_manager.is_enabled() {
|
||||
return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response());
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@
|
||||
//! A module containing PostgreSQL implementation of repositories for sessions
|
||||
|
||||
use async_trait::async_trait;
|
||||
use mas_data_model::{CompatSession, CompatSessionState, Device, Session, SessionState};
|
||||
use mas_data_model::{CompatSession, CompatSessionState, Device, Session, SessionState, UserAgent};
|
||||
use mas_storage::{
|
||||
app_session::{AppSession, AppSessionFilter, AppSessionRepository},
|
||||
Page, Pagination,
|
||||
@@ -84,6 +84,7 @@ use priv_::{AppSessionLookup, AppSessionLookupIden};
|
||||
impl TryFrom<AppSessionLookup> for AppSession {
|
||||
type Error = DatabaseError;
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn try_from(value: AppSessionLookup) -> Result<Self, Self::Error> {
|
||||
// This is annoying to do, but we have to match on all the fields to determine
|
||||
// whether it's a compat session or an oauth2 session
|
||||
@@ -104,6 +105,7 @@ impl TryFrom<AppSessionLookup> for AppSession {
|
||||
last_active_ip,
|
||||
} = value;
|
||||
|
||||
let user_agent = user_agent.map(UserAgent::parse);
|
||||
let user_session_id = user_session_id.map(Ulid::from);
|
||||
|
||||
match (
|
||||
|
@@ -28,7 +28,7 @@ pub use self::{
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::Duration;
|
||||
use mas_data_model::Device;
|
||||
use mas_data_model::{Device, UserAgent};
|
||||
use mas_storage::{
|
||||
clock::MockClock,
|
||||
compat::{
|
||||
@@ -133,7 +133,7 @@ mod tests {
|
||||
assert!(session_lookup.user_agent.is_none());
|
||||
let session = repo
|
||||
.compat_session()
|
||||
.record_user_agent(session_lookup, "Mozilla/5.0".to_owned())
|
||||
.record_user_agent(session_lookup, UserAgent::parse("Mozilla/5.0".to_owned()))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(session.user_agent.as_deref(), Some("Mozilla/5.0"));
|
||||
|
@@ -18,7 +18,7 @@ use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use mas_data_model::{
|
||||
BrowserSession, CompatSession, CompatSessionState, CompatSsoLogin, CompatSsoLoginState, Device,
|
||||
User,
|
||||
User, UserAgent,
|
||||
};
|
||||
use mas_storage::{
|
||||
compat::{CompatSessionFilter, CompatSessionRepository},
|
||||
@@ -90,7 +90,7 @@ impl TryFrom<CompatSessionLookup> for CompatSession {
|
||||
device,
|
||||
created_at: value.created_at,
|
||||
is_synapse_admin: value.is_synapse_admin,
|
||||
user_agent: value.user_agent,
|
||||
user_agent: value.user_agent.map(UserAgent::parse),
|
||||
last_active_at: value.last_active_at,
|
||||
last_active_ip: value.last_active_ip,
|
||||
};
|
||||
@@ -145,7 +145,7 @@ impl TryFrom<CompatSessionAndSsoLoginLookup> for (CompatSession, Option<CompatSs
|
||||
user_session_id: value.user_session_id.map(Ulid::from),
|
||||
created_at: value.created_at,
|
||||
is_synapse_admin: value.is_synapse_admin,
|
||||
user_agent: value.user_agent,
|
||||
user_agent: value.user_agent.map(UserAgent::parse),
|
||||
last_active_at: value.last_active_at,
|
||||
last_active_ip: value.last_active_ip,
|
||||
};
|
||||
@@ -575,7 +575,7 @@ impl<'c> CompatSessionRepository for PgCompatSessionRepository<'c> {
|
||||
async fn record_user_agent(
|
||||
&mut self,
|
||||
mut compat_session: CompatSession,
|
||||
user_agent: String,
|
||||
user_agent: UserAgent,
|
||||
) -> Result<CompatSession, Self::Error> {
|
||||
let res = sqlx::query!(
|
||||
r#"
|
||||
@@ -584,7 +584,7 @@ impl<'c> CompatSessionRepository for PgCompatSessionRepository<'c> {
|
||||
WHERE compat_session_id = $1
|
||||
"#,
|
||||
Uuid::from(compat_session.id),
|
||||
user_agent,
|
||||
&*user_agent,
|
||||
)
|
||||
.traced()
|
||||
.execute(&mut *self.conn)
|
||||
|
@@ -16,7 +16,7 @@ use std::net::IpAddr;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use mas_data_model::{BrowserSession, DeviceCodeGrant, DeviceCodeGrantState, Session};
|
||||
use mas_data_model::{BrowserSession, DeviceCodeGrant, DeviceCodeGrantState, Session, UserAgent};
|
||||
use mas_storage::{
|
||||
oauth2::{OAuth2DeviceCodeGrantParams, OAuth2DeviceCodeGrantRepository},
|
||||
Clock,
|
||||
@@ -140,7 +140,7 @@ impl TryFrom<OAuth2DeviceGrantLookup> for DeviceCodeGrant {
|
||||
created_at,
|
||||
expires_at,
|
||||
ip_address,
|
||||
user_agent,
|
||||
user_agent: user_agent.map(UserAgent::parse),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -32,7 +32,7 @@ pub use self::{
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::Duration;
|
||||
use mas_data_model::AuthorizationCode;
|
||||
use mas_data_model::{AuthorizationCode, UserAgent};
|
||||
use mas_storage::{
|
||||
clock::MockClock,
|
||||
oauth2::{OAuth2DeviceCodeGrantParams, OAuth2SessionFilter, OAuth2SessionRepository},
|
||||
@@ -371,7 +371,7 @@ mod tests {
|
||||
assert!(session.user_agent.is_none());
|
||||
let session = repo
|
||||
.oauth2_session()
|
||||
.record_user_agent(session, "Mozilla/5.0".to_owned())
|
||||
.record_user_agent(session, UserAgent::parse("Mozilla/5.0".to_owned()))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(session.user_agent.as_deref(), Some("Mozilla/5.0"));
|
||||
|
@@ -16,7 +16,7 @@ use std::net::IpAddr;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use mas_data_model::{BrowserSession, Client, Session, SessionState, User};
|
||||
use mas_data_model::{BrowserSession, Client, Session, SessionState, User, UserAgent};
|
||||
use mas_storage::{
|
||||
oauth2::{OAuth2SessionFilter, OAuth2SessionRepository},
|
||||
Clock, Page, Pagination,
|
||||
@@ -94,7 +94,7 @@ impl TryFrom<OAuthSessionLookup> for Session {
|
||||
user_id: value.user_id.map(Ulid::from),
|
||||
user_session_id: value.user_session_id.map(Ulid::from),
|
||||
scope,
|
||||
user_agent: value.user_agent,
|
||||
user_agent: value.user_agent.map(UserAgent::parse),
|
||||
last_active_at: value.last_active_at,
|
||||
last_active_ip: value.last_active_ip,
|
||||
})
|
||||
@@ -444,14 +444,14 @@ impl<'c> OAuth2SessionRepository for PgOAuth2SessionRepository<'c> {
|
||||
%session.id,
|
||||
%session.scope,
|
||||
client.id = %session.client_id,
|
||||
session.user_agent = %user_agent,
|
||||
session.user_agent = %user_agent.raw,
|
||||
),
|
||||
err,
|
||||
)]
|
||||
async fn record_user_agent(
|
||||
&mut self,
|
||||
mut session: Session,
|
||||
user_agent: String,
|
||||
user_agent: UserAgent,
|
||||
) -> Result<Session, Self::Error> {
|
||||
let res = sqlx::query!(
|
||||
r#"
|
||||
@@ -460,7 +460,7 @@ impl<'c> OAuth2SessionRepository for PgOAuth2SessionRepository<'c> {
|
||||
WHERE oauth2_session_id = $1
|
||||
"#,
|
||||
Uuid::from(session.id),
|
||||
user_agent,
|
||||
&*user_agent,
|
||||
)
|
||||
.traced()
|
||||
.execute(&mut *self.conn)
|
||||
|
@@ -18,7 +18,7 @@ use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use mas_data_model::{
|
||||
Authentication, AuthenticationMethod, BrowserSession, Password,
|
||||
UpstreamOAuthAuthorizationSession, User,
|
||||
UpstreamOAuthAuthorizationSession, User, UserAgent,
|
||||
};
|
||||
use mas_storage::{user::BrowserSessionRepository, Clock, Page, Pagination};
|
||||
use rand::RngCore;
|
||||
@@ -86,7 +86,7 @@ impl TryFrom<SessionLookup> for BrowserSession {
|
||||
user,
|
||||
created_at: value.user_session_created_at,
|
||||
finished_at: value.user_session_finished_at,
|
||||
user_agent: value.user_session_user_agent,
|
||||
user_agent: value.user_session_user_agent.map(UserAgent::parse),
|
||||
last_active_at: value.user_session_last_active_at,
|
||||
last_active_ip: value.user_session_last_active_ip,
|
||||
})
|
||||
@@ -189,7 +189,7 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
user: &User,
|
||||
user_agent: Option<String>,
|
||||
user_agent: Option<UserAgent>,
|
||||
) -> Result<BrowserSession, Self::Error> {
|
||||
let created_at = clock.now();
|
||||
let id = Ulid::from_datetime_with_source(created_at.into(), rng);
|
||||
@@ -203,7 +203,7 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
|
||||
Uuid::from(id),
|
||||
Uuid::from(user.id),
|
||||
created_at,
|
||||
user_agent,
|
||||
user_agent.as_deref(),
|
||||
)
|
||||
.traced()
|
||||
.execute(&mut *self.conn)
|
||||
|
@@ -16,7 +16,7 @@ use std::net::IpAddr;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use mas_data_model::{BrowserSession, CompatSession, CompatSsoLogin, Device, User};
|
||||
use mas_data_model::{BrowserSession, CompatSession, CompatSsoLogin, Device, User, UserAgent};
|
||||
use rand_core::RngCore;
|
||||
use ulid::Ulid;
|
||||
|
||||
@@ -266,7 +266,7 @@ pub trait CompatSessionRepository: Send + Sync {
|
||||
async fn record_user_agent(
|
||||
&mut self,
|
||||
compat_session: CompatSession,
|
||||
user_agent: String,
|
||||
user_agent: UserAgent,
|
||||
) -> Result<CompatSession, Self::Error>;
|
||||
}
|
||||
|
||||
@@ -305,6 +305,6 @@ repository_impl!(CompatSessionRepository:
|
||||
async fn record_user_agent(
|
||||
&mut self,
|
||||
compat_session: CompatSession,
|
||||
user_agent: String,
|
||||
user_agent: UserAgent,
|
||||
) -> Result<CompatSession, Self::Error>;
|
||||
);
|
||||
|
@@ -16,7 +16,7 @@ use std::net::IpAddr;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::Duration;
|
||||
use mas_data_model::{BrowserSession, Client, DeviceCodeGrant, Session};
|
||||
use mas_data_model::{BrowserSession, Client, DeviceCodeGrant, Session, UserAgent};
|
||||
use oauth2_types::scope::Scope;
|
||||
use rand_core::RngCore;
|
||||
use ulid::Ulid;
|
||||
@@ -44,7 +44,7 @@ pub struct OAuth2DeviceCodeGrantParams<'a> {
|
||||
pub ip_address: Option<IpAddr>,
|
||||
|
||||
/// The user agent from which the request was made
|
||||
pub user_agent: Option<String>,
|
||||
pub user_agent: Option<UserAgent>,
|
||||
}
|
||||
|
||||
/// An [`OAuth2DeviceCodeGrantRepository`] helps interacting with
|
||||
|
@@ -16,7 +16,7 @@ use std::net::IpAddr;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use mas_data_model::{BrowserSession, Client, Session, User};
|
||||
use mas_data_model::{BrowserSession, Client, Session, User, UserAgent};
|
||||
use oauth2_types::scope::Scope;
|
||||
use rand_core::RngCore;
|
||||
use ulid::Ulid;
|
||||
@@ -296,7 +296,7 @@ pub trait OAuth2SessionRepository: Send + Sync {
|
||||
async fn record_user_agent(
|
||||
&mut self,
|
||||
session: Session,
|
||||
user_agent: String,
|
||||
user_agent: UserAgent,
|
||||
) -> Result<Session, Self::Error>;
|
||||
}
|
||||
|
||||
@@ -349,6 +349,6 @@ repository_impl!(OAuth2SessionRepository:
|
||||
async fn record_user_agent(
|
||||
&mut self,
|
||||
session: Session,
|
||||
user_agent: String,
|
||||
user_agent: UserAgent,
|
||||
) -> Result<Session, Self::Error>;
|
||||
);
|
||||
|
@@ -17,7 +17,7 @@ use std::net::IpAddr;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use mas_data_model::{
|
||||
Authentication, BrowserSession, Password, UpstreamOAuthAuthorizationSession, User,
|
||||
Authentication, BrowserSession, Password, UpstreamOAuthAuthorizationSession, User, UserAgent,
|
||||
};
|
||||
use rand_core::RngCore;
|
||||
use ulid::Ulid;
|
||||
@@ -127,7 +127,7 @@ pub trait BrowserSessionRepository: Send + Sync {
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
user: &User,
|
||||
user_agent: Option<String>,
|
||||
user_agent: Option<UserAgent>,
|
||||
) -> Result<BrowserSession, Self::Error>;
|
||||
|
||||
/// Finish a [`BrowserSession`]
|
||||
@@ -254,7 +254,7 @@ repository_impl!(BrowserSessionRepository:
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
user: &User,
|
||||
user_agent: Option<String>,
|
||||
user_agent: Option<UserAgent>,
|
||||
) -> Result<BrowserSession, Self::Error>;
|
||||
async fn finish(
|
||||
&mut self,
|
||||
|
@@ -25,7 +25,7 @@ use chrono::{DateTime, Duration, Utc};
|
||||
use http::{Method, Uri, Version};
|
||||
use mas_data_model::{
|
||||
AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState,
|
||||
DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, User, UserEmail,
|
||||
DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, User, UserAgent, UserEmail,
|
||||
UserEmailVerification,
|
||||
};
|
||||
use mas_i18n::DataLocale;
|
||||
@@ -1164,7 +1164,7 @@ impl TemplateContext for DeviceConsentContext {
|
||||
created_at: now - Duration::minutes(5),
|
||||
expires_at: now + Duration::minutes(25),
|
||||
ip_address: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
|
||||
user_agent: Some("Mozilla/5.0".to_owned()),
|
||||
user_agent: Some(UserAgent::parse("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36".to_owned())),
|
||||
};
|
||||
Self { grant, client }
|
||||
})
|
||||
|
Reference in New Issue
Block a user