1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-08-06 06:02:40 +03:00

Record the user agent and IP in the device code grant

This commit is contained in:
Quentin Gliech
2024-02-02 16:54:56 +01:00
parent d39a1d29df
commit 17e968f7cc
14 changed files with 129 additions and 20 deletions

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::net::IpAddr;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use oauth2_types::scope::Scope; use oauth2_types::scope::Scope;
use serde::Serialize; use serde::Serialize;
@@ -193,6 +195,12 @@ pub struct DeviceCodeGrant {
/// The time at which this device code grant will expire. /// The time at which this device code grant will expire.
pub expires_at: DateTime<Utc>, pub expires_at: DateTime<Utc>,
/// The IP address of the client which requested this device code grant.
pub ip_address: Option<IpAddr>,
/// The user agent used to request this device code grant.
pub user_agent: Option<String>,
} }
impl std::ops::Deref for DeviceCodeGrant { impl std::ops::Deref for DeviceCodeGrant {

View File

@@ -33,6 +33,12 @@ impl Bound {
Self { tracker, ip } Self { tracker, ip }
} }
/// Get the IP address bound to this activity tracker.
#[must_use]
pub fn ip(&self) -> Option<IpAddr> {
self.ip
}
/// Record activity in an OAuth 2.0 session. /// Record activity in an OAuth 2.0 session.
pub async fn record_oauth2_session(&self, clock: &dyn Clock, session: &Session) { pub async fn record_oauth2_session(&self, clock: &dyn Clock, session: &Session) {
self.tracker self.tracker

View File

@@ -14,7 +14,7 @@
use axum::{extract::State, response::IntoResponse, Json, TypedHeader}; use axum::{extract::State, response::IntoResponse, Json, TypedHeader};
use chrono::Duration; use chrono::Duration;
use headers::{CacheControl, Pragma}; use headers::{CacheControl, Pragma, UserAgent};
use hyper::StatusCode; use hyper::StatusCode;
use mas_axum_utils::{ use mas_axum_utils::{
client_authorization::{ClientAuthorization, CredentialsVerificationError}, client_authorization::{ClientAuthorization, CredentialsVerificationError},
@@ -32,7 +32,7 @@ use oauth2_types::{
use rand::distributions::{Alphanumeric, DistString}; use rand::distributions::{Alphanumeric, DistString};
use thiserror::Error; use thiserror::Error;
use crate::impl_from_error_for_route; use crate::{impl_from_error_for_route, BoundActivityTracker};
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub(crate) enum RouteError { pub(crate) enum RouteError {
@@ -84,6 +84,8 @@ pub(crate) async fn post(
mut rng: BoxRng, mut rng: BoxRng,
clock: BoxClock, clock: BoxClock,
mut repo: BoxRepository, mut repo: BoxRepository,
user_agent: Option<TypedHeader<UserAgent>>,
activity_tracker: BoundActivityTracker,
State(url_builder): State<UrlBuilder>, State(url_builder): State<UrlBuilder>,
State(http_client_factory): State<HttpClientFactory>, State(http_client_factory): State<HttpClientFactory>,
State(encrypter): State<Encrypter>, State(encrypter): State<Encrypter>,
@@ -123,6 +125,9 @@ pub(crate) async fn post(
let expires_in = Duration::minutes(20); let expires_in = Duration::minutes(20);
let user_agent = user_agent.map(|ua| ua.0.to_string());
let ip_address = activity_tracker.ip();
let device_code = Alphanumeric.sample_string(&mut rng, 32); let device_code = Alphanumeric.sample_string(&mut rng, 32);
let user_code = Alphanumeric.sample_string(&mut rng, 6).to_uppercase(); let user_code = Alphanumeric.sample_string(&mut rng, 6).to_uppercase();
@@ -137,6 +142,8 @@ pub(crate) async fn post(
device_code, device_code,
user_code, user_code,
expires_in, expires_in,
user_agent,
ip_address,
}, },
) )
.await?; .await?;

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "\n INSERT INTO \"oauth2_device_code_grant\" \n ( oauth2_device_code_grant_id\n , oauth2_client_id\n , scope\n , device_code\n , user_code\n , created_at\n , expires_at\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7)\n ", "query": "\n INSERT INTO \"oauth2_device_code_grant\" \n ( oauth2_device_code_grant_id\n , oauth2_client_id\n , scope\n , device_code\n , user_code\n , created_at\n , expires_at\n , ip_address\n , user_agent\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"describe": { "describe": {
"columns": [], "columns": [],
"parameters": { "parameters": {
@@ -11,10 +11,12 @@
"Text", "Text",
"Text", "Text",
"Timestamptz", "Timestamptz",
"Timestamptz" "Timestamptz",
"Inet",
"Text"
] ]
}, },
"nullable": [] "nullable": []
}, },
"hash": "6a72c38cb718ac09b61e0fadd9703e4b7a984c46185cceea4eceff4655f4e81f" "hash": "9742df9a34fe64e294cae4fc4a18e261c03b2367adeaec8fd554ca6f52c2015e"
} }

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "\n SELECT oauth2_device_code_grant_id\n , oauth2_client_id\n , scope\n , device_code\n , user_code\n , created_at\n , expires_at\n , fulfilled_at\n , rejected_at\n , exchanged_at\n , user_session_id\n , oauth2_session_id\n FROM \n oauth2_device_code_grant\n\n WHERE device_code = $1\n ", "query": "\n SELECT oauth2_device_code_grant_id\n , oauth2_client_id\n , scope\n , device_code\n , user_code\n , created_at\n , expires_at\n , fulfilled_at\n , rejected_at\n , exchanged_at\n , user_session_id\n , oauth2_session_id\n , ip_address as \"ip_address: IpAddr\"\n , user_agent\n FROM \n oauth2_device_code_grant\n\n WHERE device_code = $1\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -62,6 +62,16 @@
"ordinal": 11, "ordinal": 11,
"name": "oauth2_session_id", "name": "oauth2_session_id",
"type_info": "Uuid" "type_info": "Uuid"
},
{
"ordinal": 12,
"name": "ip_address: IpAddr",
"type_info": "Inet"
},
{
"ordinal": 13,
"name": "user_agent",
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
@@ -81,8 +91,10 @@
true, true,
true, true,
true, true,
true,
true,
true true
] ]
}, },
"hash": "be25896189a30862a0aa0b6d1d6ba44278b98d4b8d027036e8871853f5d175c0" "hash": "aabefb019c9195b0588882fef562472d6117ff68f8f37d02b7609c94aefdb5d6"
} }

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "\n SELECT oauth2_device_code_grant_id\n , oauth2_client_id\n , scope\n , device_code\n , user_code\n , created_at\n , expires_at\n , fulfilled_at\n , rejected_at\n , exchanged_at\n , user_session_id\n , oauth2_session_id\n FROM \n oauth2_device_code_grant\n\n WHERE oauth2_device_code_grant_id = $1\n ", "query": "\n SELECT oauth2_device_code_grant_id\n , oauth2_client_id\n , scope\n , device_code\n , user_code\n , created_at\n , expires_at\n , fulfilled_at\n , rejected_at\n , exchanged_at\n , user_session_id\n , oauth2_session_id\n , ip_address as \"ip_address: IpAddr\"\n , user_agent\n FROM \n oauth2_device_code_grant\n\n WHERE oauth2_device_code_grant_id = $1\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -62,6 +62,16 @@
"ordinal": 11, "ordinal": 11,
"name": "oauth2_session_id", "name": "oauth2_session_id",
"type_info": "Uuid" "type_info": "Uuid"
},
{
"ordinal": 12,
"name": "ip_address: IpAddr",
"type_info": "Inet"
},
{
"ordinal": 13,
"name": "user_agent",
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
@@ -81,8 +91,10 @@
true, true,
true, true,
true, true,
true,
true,
true true
] ]
}, },
"hash": "61dc64c1980b5d1d2e2b52c8c55c91e1953595e413bedcec27eafbf87e42f1cd" "hash": "f0977fee9b3919707e9aa20537d836d741acd5b2426218a2f9f9dab4fb8a2ad0"
} }

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "\n SELECT oauth2_device_code_grant_id\n , oauth2_client_id\n , scope\n , device_code\n , user_code\n , created_at\n , expires_at\n , fulfilled_at\n , rejected_at\n , exchanged_at\n , user_session_id\n , oauth2_session_id\n FROM \n oauth2_device_code_grant\n\n WHERE user_code = $1\n ", "query": "\n SELECT oauth2_device_code_grant_id\n , oauth2_client_id\n , scope\n , device_code\n , user_code\n , created_at\n , expires_at\n , fulfilled_at\n , rejected_at\n , exchanged_at\n , user_session_id\n , oauth2_session_id\n , ip_address as \"ip_address: IpAddr\"\n , user_agent\n FROM \n oauth2_device_code_grant\n\n WHERE user_code = $1\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -62,6 +62,16 @@
"ordinal": 11, "ordinal": 11,
"name": "oauth2_session_id", "name": "oauth2_session_id",
"type_info": "Uuid" "type_info": "Uuid"
},
{
"ordinal": 12,
"name": "ip_address: IpAddr",
"type_info": "Inet"
},
{
"ordinal": 13,
"name": "user_agent",
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
@@ -81,8 +91,10 @@
true, true,
true, true,
true, true,
true,
true,
true true
] ]
}, },
"hash": "b83fd5c55a209151ce5053b56034c49b5972df523f21a17be76303bde4a88522" "hash": "fa5a57505066d03828015f79fdb1079eaea13cf7698073f2d5f74d4c50f7b094"
} }

View File

@@ -72,5 +72,11 @@ CREATE TABLE "oauth2_device_code_grant" (
-- The browser session ID that the user used to authenticate -- The browser session ID that the user used to authenticate
-- This means "fulfilled_at" or "rejected_at" has also been set -- This means "fulfilled_at" or "rejected_at" has also been set
"user_session_id" UUID "user_session_id" UUID
REFERENCES "user_sessions" ("user_session_id") REFERENCES "user_sessions" ("user_session_id"),
-- The IP address of the user when they authenticated
"ip_address" INET,
-- The user agent of the user when they authenticated
"user_agent" TEXT
); );

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::net::IpAddr;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use mas_data_model::{BrowserSession, DeviceCodeGrant, DeviceCodeGrantState, Session}; use mas_data_model::{BrowserSession, DeviceCodeGrant, DeviceCodeGrantState, Session};
@@ -54,6 +56,8 @@ struct OAuth2DeviceGrantLookup {
exchanged_at: Option<DateTime<Utc>>, exchanged_at: Option<DateTime<Utc>>,
user_session_id: Option<Uuid>, user_session_id: Option<Uuid>,
oauth2_session_id: Option<Uuid>, oauth2_session_id: Option<Uuid>,
ip_address: Option<IpAddr>,
user_agent: Option<String>,
} }
impl TryFrom<OAuth2DeviceGrantLookup> for DeviceCodeGrant { impl TryFrom<OAuth2DeviceGrantLookup> for DeviceCodeGrant {
@@ -73,6 +77,8 @@ impl TryFrom<OAuth2DeviceGrantLookup> for DeviceCodeGrant {
exchanged_at, exchanged_at,
user_session_id, user_session_id,
oauth2_session_id, oauth2_session_id,
ip_address,
user_agent,
}: OAuth2DeviceGrantLookup, }: OAuth2DeviceGrantLookup,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
let id = Ulid::from(oauth2_device_code_grant_id); let id = Ulid::from(oauth2_device_code_grant_id);
@@ -133,6 +139,8 @@ impl TryFrom<OAuth2DeviceGrantLookup> for DeviceCodeGrant {
device_code, device_code,
created_at, created_at,
expires_at, expires_at,
ip_address,
user_agent,
}) })
} }
} }
@@ -176,9 +184,11 @@ impl<'c> OAuth2DeviceCodeGrantRepository for PgOAuth2DeviceCodeGrantRepository<'
, user_code , user_code
, created_at , created_at
, expires_at , expires_at
, ip_address
, user_agent
) )
VALUES VALUES
($1, $2, $3, $4, $5, $6, $7) ($1, $2, $3, $4, $5, $6, $7, $8, $9)
"#, "#,
Uuid::from(id), Uuid::from(id),
Uuid::from(client_id), Uuid::from(client_id),
@@ -187,6 +197,8 @@ impl<'c> OAuth2DeviceCodeGrantRepository for PgOAuth2DeviceCodeGrantRepository<'
&params.user_code, &params.user_code,
created_at, created_at,
expires_at, expires_at,
params.ip_address as Option<IpAddr>,
params.user_agent.as_deref(),
) )
.traced() .traced()
.execute(&mut *self.conn) .execute(&mut *self.conn)
@@ -201,6 +213,8 @@ impl<'c> OAuth2DeviceCodeGrantRepository for PgOAuth2DeviceCodeGrantRepository<'
device_code: params.device_code, device_code: params.device_code,
created_at, created_at,
expires_at, expires_at,
ip_address: params.ip_address,
user_agent: params.user_agent,
}) })
} }
@@ -229,6 +243,8 @@ impl<'c> OAuth2DeviceCodeGrantRepository for PgOAuth2DeviceCodeGrantRepository<'
, exchanged_at , exchanged_at
, user_session_id , user_session_id
, oauth2_session_id , oauth2_session_id
, ip_address as "ip_address: IpAddr"
, user_agent
FROM FROM
oauth2_device_code_grant oauth2_device_code_grant
@@ -273,6 +289,8 @@ impl<'c> OAuth2DeviceCodeGrantRepository for PgOAuth2DeviceCodeGrantRepository<'
, exchanged_at , exchanged_at
, user_session_id , user_session_id
, oauth2_session_id , oauth2_session_id
, ip_address as "ip_address: IpAddr"
, user_agent
FROM FROM
oauth2_device_code_grant oauth2_device_code_grant
@@ -317,6 +335,8 @@ impl<'c> OAuth2DeviceCodeGrantRepository for PgOAuth2DeviceCodeGrantRepository<'
, exchanged_at , exchanged_at
, user_session_id , user_session_id
, oauth2_session_id , oauth2_session_id
, ip_address as "ip_address: IpAddr"
, user_agent
FROM FROM
oauth2_device_code_grant oauth2_device_code_grant

View File

@@ -758,6 +758,8 @@ mod tests {
device_code: device_code.to_owned(), device_code: device_code.to_owned(),
user_code: user_code.to_owned(), user_code: user_code.to_owned(),
expires_in: Duration::minutes(5), expires_in: Duration::minutes(5),
ip_address: None,
user_agent: None,
}, },
) )
.await .await
@@ -861,6 +863,8 @@ mod tests {
device_code: "second_devicecode".to_owned(), device_code: "second_devicecode".to_owned(),
user_code: "second_usercode".to_owned(), user_code: "second_usercode".to_owned(),
expires_in: Duration::minutes(5), expires_in: Duration::minutes(5),
ip_address: None,
user_agent: None,
}, },
) )
.await .await

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::net::IpAddr;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::Duration; use chrono::Duration;
use mas_data_model::{BrowserSession, Client, DeviceCodeGrant, Session}; use mas_data_model::{BrowserSession, Client, DeviceCodeGrant, Session};
@@ -37,6 +39,12 @@ pub struct OAuth2DeviceCodeGrantParams<'a> {
/// After how long the device code expires /// After how long the device code expires
pub expires_in: Duration, pub expires_in: Duration,
/// IP address from which the request was made
pub ip_address: Option<IpAddr>,
/// The user agent from which the request was made
pub user_agent: Option<String>,
} }
/// An [`OAuth2DeviceCodeGrantRepository`] helps interacting with /// An [`OAuth2DeviceCodeGrantRepository`] helps interacting with

View File

@@ -16,7 +16,10 @@
mod branding; mod branding;
use std::fmt::Formatter; use std::{
fmt::Formatter,
net::{IpAddr, Ipv4Addr},
};
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use http::{Method, Uri, Version}; use http::{Method, Uri, Version};
@@ -622,6 +625,8 @@ impl TemplateContext for PolicyViolationContext {
device_code: Alphanumeric.sample_string(rng, 32), device_code: Alphanumeric.sample_string(rng, 32),
created_at: now - Duration::minutes(5), created_at: now - Duration::minutes(5),
expires_at: now + Duration::minutes(25), expires_at: now + Duration::minutes(25),
ip_address: None,
user_agent: None,
}, },
client, client,
); );
@@ -1152,6 +1157,8 @@ impl TemplateContext for DeviceConsentContext {
device_code: Alphanumeric.sample_string(rng, 32), device_code: Alphanumeric.sample_string(rng, 32),
created_at: now - Duration::minutes(5), created_at: now - Duration::minutes(5),
expires_at: now + Duration::minutes(25), 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()),
}; };
Self { grant, client } Self { grant, client }
}) })

View File

@@ -33,13 +33,18 @@ limitations under the License.
<h1 class="title">Allow access to your account?</h1> <h1 class="title">Allow access to your account?</h1>
<div class="consent-device-card"> <div class="consent-device-card">
<div class="device"> <div class="device" {%- if grant.user_agent %} title="{{ grant.user_agent }}"{% endif %}>
{{ icon.web_browser() }} {{ icon.web_browser() }}
{# TODO: Infer from the user agent #} {# TODO: Infer from the user agent #}
<div class="name">Device</div> <div class="name">Device</div>
</div> </div>
<div class="meta"> <div class="meta">
{# TODO: add the IP address #} {% if grant.ip_address %}
<div>
<div class="key">IP address</div>
<div class="value">{{ grant.ip_address }}</div>
</div>
{% endif %}
<div> <div>
<div class="key">Access requested</div> <div class="key">Access requested</div>
<div class="value">{{ _.relative_date(grant.created_at) | title }} {{ _.short_time(grant.created_at) }}</div> <div class="value">{{ _.relative_date(grant.created_at) | title }} {{ _.short_time(grant.created_at) }}</div>

View File

@@ -2,11 +2,11 @@
"action": { "action": {
"cancel": "Cancel", "cancel": "Cancel",
"@cancel": { "@cancel": {
"context": "pages/consent.html:72:11-29, pages/device_consent.html:76:13-31, pages/login.html:100:13-31, pages/policy_violation.html:52:13-31, pages/register.html:64:13-31" "context": "pages/consent.html:72:11-29, pages/device_consent.html:94:13-31, pages/login.html:100:13-31, pages/policy_violation.html:52:13-31, pages/register.html:64:13-31"
}, },
"continue": "Continue", "continue": "Continue",
"@continue": { "@continue": {
"context": "pages/account/emails/add.html:45:26-46, pages/account/emails/verify.html:60:26-46, pages/consent.html:60:28-48, pages/device_consent.html:73:13-33, pages/device_link.html:50:26-46, pages/login.html:62:30-50, pages/reauth.html:40:28-48, pages/register.html:59:28-48, pages/sso.html:45:28-48" "context": "pages/account/emails/add.html:45:26-46, pages/account/emails/verify.html:60:26-46, pages/consent.html:60:28-48, pages/device_consent.html:91:13-33, pages/device_link.html:50:26-46, pages/login.html:62:30-50, pages/reauth.html:40:28-48, pages/register.html:59:28-48, pages/sso.html:45:28-48"
}, },
"create_account": "Create Account", "create_account": "Create Account",
"@create_account": { "@create_account": {
@@ -18,7 +18,7 @@
}, },
"sign_out": "Sign out", "sign_out": "Sign out",
"@sign_out": { "@sign_out": {
"context": "pages/consent.html:68:28-48, pages/device_consent.html:85:30-50, pages/index.html:36:28-48, pages/policy_violation.html:46:28-48, pages/sso.html:53:28-48, pages/upstream_oauth2/link_mismatch.html:32:24-44, pages/upstream_oauth2/suggest_link.html:40:26-46" "context": "pages/consent.html:68:28-48, pages/device_consent.html:103:30-50, pages/index.html:36:28-48, pages/policy_violation.html:46:28-48, pages/sso.html:53:28-48, pages/upstream_oauth2/link_mismatch.html:32:24-44, pages/upstream_oauth2/suggest_link.html:40:26-46"
} }
}, },
"app": { "app": {
@@ -246,7 +246,7 @@
}, },
"not_you": "Not %(username)s?", "not_you": "Not %(username)s?",
"@not_you": { "@not_you": {
"context": "pages/consent.html:65:11-67, pages/device_consent.html:82:13-69, pages/sso.html:50:11-67", "context": "pages/consent.html:65:11-67, pages/device_consent.html:100:13-69, pages/sso.html:50:11-67",
"description": "Suggestions for the user to log in as a different user" "description": "Suggestions for the user to log in as a different user"
}, },
"or_separator": "Or", "or_separator": "Or",