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
// limitations under the License.
use std::net::IpAddr;
use chrono::{DateTime, Utc};
use oauth2_types::scope::Scope;
use serde::Serialize;
@@ -193,6 +195,12 @@ pub struct DeviceCodeGrant {
/// The time at which this device code grant will expire.
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 {

View File

@@ -33,6 +33,12 @@ impl Bound {
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.
pub async fn record_oauth2_session(&self, clock: &dyn Clock, session: &Session) {
self.tracker

View File

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

View File

@@ -1,6 +1,6 @@
{
"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": {
"columns": [],
"parameters": {
@@ -11,10 +11,12 @@
"Text",
"Text",
"Timestamptz",
"Timestamptz"
"Timestamptz",
"Inet",
"Text"
]
},
"nullable": []
},
"hash": "6a72c38cb718ac09b61e0fadd9703e4b7a984c46185cceea4eceff4655f4e81f"
"hash": "9742df9a34fe64e294cae4fc4a18e261c03b2367adeaec8fd554ca6f52c2015e"
}

View File

@@ -1,6 +1,6 @@
{
"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": {
"columns": [
{
@@ -62,6 +62,16 @@
"ordinal": 11,
"name": "oauth2_session_id",
"type_info": "Uuid"
},
{
"ordinal": 12,
"name": "ip_address: IpAddr",
"type_info": "Inet"
},
{
"ordinal": 13,
"name": "user_agent",
"type_info": "Text"
}
],
"parameters": {
@@ -81,8 +91,10 @@
true,
true,
true,
true,
true,
true
]
},
"hash": "be25896189a30862a0aa0b6d1d6ba44278b98d4b8d027036e8871853f5d175c0"
"hash": "aabefb019c9195b0588882fef562472d6117ff68f8f37d02b7609c94aefdb5d6"
}

View File

@@ -1,6 +1,6 @@
{
"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": {
"columns": [
{
@@ -62,6 +62,16 @@
"ordinal": 11,
"name": "oauth2_session_id",
"type_info": "Uuid"
},
{
"ordinal": 12,
"name": "ip_address: IpAddr",
"type_info": "Inet"
},
{
"ordinal": 13,
"name": "user_agent",
"type_info": "Text"
}
],
"parameters": {
@@ -81,8 +91,10 @@
true,
true,
true,
true,
true,
true
]
},
"hash": "61dc64c1980b5d1d2e2b52c8c55c91e1953595e413bedcec27eafbf87e42f1cd"
"hash": "f0977fee9b3919707e9aa20537d836d741acd5b2426218a2f9f9dab4fb8a2ad0"
}

View File

@@ -1,6 +1,6 @@
{
"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": {
"columns": [
{
@@ -62,6 +62,16 @@
"ordinal": 11,
"name": "oauth2_session_id",
"type_info": "Uuid"
},
{
"ordinal": 12,
"name": "ip_address: IpAddr",
"type_info": "Inet"
},
{
"ordinal": 13,
"name": "user_agent",
"type_info": "Text"
}
],
"parameters": {
@@ -81,8 +91,10 @@
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
-- This means "fulfilled_at" or "rejected_at" has also been set
"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
// limitations under the License.
use std::net::IpAddr;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use mas_data_model::{BrowserSession, DeviceCodeGrant, DeviceCodeGrantState, Session};
@@ -54,6 +56,8 @@ struct OAuth2DeviceGrantLookup {
exchanged_at: Option<DateTime<Utc>>,
user_session_id: Option<Uuid>,
oauth2_session_id: Option<Uuid>,
ip_address: Option<IpAddr>,
user_agent: Option<String>,
}
impl TryFrom<OAuth2DeviceGrantLookup> for DeviceCodeGrant {
@@ -73,6 +77,8 @@ impl TryFrom<OAuth2DeviceGrantLookup> for DeviceCodeGrant {
exchanged_at,
user_session_id,
oauth2_session_id,
ip_address,
user_agent,
}: OAuth2DeviceGrantLookup,
) -> Result<Self, Self::Error> {
let id = Ulid::from(oauth2_device_code_grant_id);
@@ -133,6 +139,8 @@ impl TryFrom<OAuth2DeviceGrantLookup> for DeviceCodeGrant {
device_code,
created_at,
expires_at,
ip_address,
user_agent,
})
}
}
@@ -176,9 +184,11 @@ impl<'c> OAuth2DeviceCodeGrantRepository for PgOAuth2DeviceCodeGrantRepository<'
, user_code
, created_at
, expires_at
, ip_address
, user_agent
)
VALUES
($1, $2, $3, $4, $5, $6, $7)
($1, $2, $3, $4, $5, $6, $7, $8, $9)
"#,
Uuid::from(id),
Uuid::from(client_id),
@@ -187,6 +197,8 @@ impl<'c> OAuth2DeviceCodeGrantRepository for PgOAuth2DeviceCodeGrantRepository<'
&params.user_code,
created_at,
expires_at,
params.ip_address as Option<IpAddr>,
params.user_agent.as_deref(),
)
.traced()
.execute(&mut *self.conn)
@@ -201,6 +213,8 @@ impl<'c> OAuth2DeviceCodeGrantRepository for PgOAuth2DeviceCodeGrantRepository<'
device_code: params.device_code,
created_at,
expires_at,
ip_address: params.ip_address,
user_agent: params.user_agent,
})
}
@@ -229,6 +243,8 @@ impl<'c> OAuth2DeviceCodeGrantRepository for PgOAuth2DeviceCodeGrantRepository<'
, exchanged_at
, user_session_id
, oauth2_session_id
, ip_address as "ip_address: IpAddr"
, user_agent
FROM
oauth2_device_code_grant
@@ -273,6 +289,8 @@ impl<'c> OAuth2DeviceCodeGrantRepository for PgOAuth2DeviceCodeGrantRepository<'
, exchanged_at
, user_session_id
, oauth2_session_id
, ip_address as "ip_address: IpAddr"
, user_agent
FROM
oauth2_device_code_grant
@@ -317,6 +335,8 @@ impl<'c> OAuth2DeviceCodeGrantRepository for PgOAuth2DeviceCodeGrantRepository<'
, exchanged_at
, user_session_id
, oauth2_session_id
, ip_address as "ip_address: IpAddr"
, user_agent
FROM
oauth2_device_code_grant

View File

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

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::net::IpAddr;
use async_trait::async_trait;
use chrono::Duration;
use mas_data_model::{BrowserSession, Client, DeviceCodeGrant, Session};
@@ -37,6 +39,12 @@ pub struct OAuth2DeviceCodeGrantParams<'a> {
/// After how long the device code expires
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

View File

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

View File

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

View File

@@ -2,11 +2,11 @@
"action": {
"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": {
"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": {
@@ -18,7 +18,7 @@
},
"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": {
@@ -246,7 +246,7 @@
},
"not_you": "Not %(username)s?",
"@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"
},
"or_separator": "Or",