diff --git a/crates/data-model/src/oauth2/device_code_grant.rs b/crates/data-model/src/oauth2/device_code_grant.rs index 203d7387..5077d918 100644 --- a/crates/data-model/src/oauth2/device_code_grant.rs +++ b/crates/data-model/src/oauth2/device_code_grant.rs @@ -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, + + /// The IP address of the client which requested this device code grant. + pub ip_address: Option, + + /// The user agent used to request this device code grant. + pub user_agent: Option, } impl std::ops::Deref for DeviceCodeGrant { diff --git a/crates/handlers/src/activity_tracker/bound.rs b/crates/handlers/src/activity_tracker/bound.rs index 419ba3a1..218c72f4 100644 --- a/crates/handlers/src/activity_tracker/bound.rs +++ b/crates/handlers/src/activity_tracker/bound.rs @@ -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 { + self.ip + } + /// Record activity in an OAuth 2.0 session. pub async fn record_oauth2_session(&self, clock: &dyn Clock, session: &Session) { self.tracker diff --git a/crates/handlers/src/oauth2/device/authorize.rs b/crates/handlers/src/oauth2/device/authorize.rs index 05d84a5c..ab639794 100644 --- a/crates/handlers/src/oauth2/device/authorize.rs +++ b/crates/handlers/src/oauth2/device/authorize.rs @@ -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>, + activity_tracker: BoundActivityTracker, State(url_builder): State, State(http_client_factory): State, State(encrypter): State, @@ -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?; diff --git a/crates/storage-pg/.sqlx/query-6a72c38cb718ac09b61e0fadd9703e4b7a984c46185cceea4eceff4655f4e81f.json b/crates/storage-pg/.sqlx/query-9742df9a34fe64e294cae4fc4a18e261c03b2367adeaec8fd554ca6f52c2015e.json similarity index 63% rename from crates/storage-pg/.sqlx/query-6a72c38cb718ac09b61e0fadd9703e4b7a984c46185cceea4eceff4655f4e81f.json rename to crates/storage-pg/.sqlx/query-9742df9a34fe64e294cae4fc4a18e261c03b2367adeaec8fd554ca6f52c2015e.json index 33b5e5dd..d70b3f88 100644 --- a/crates/storage-pg/.sqlx/query-6a72c38cb718ac09b61e0fadd9703e4b7a984c46185cceea4eceff4655f4e81f.json +++ b/crates/storage-pg/.sqlx/query-9742df9a34fe64e294cae4fc4a18e261c03b2367adeaec8fd554ca6f52c2015e.json @@ -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" } diff --git a/crates/storage-pg/.sqlx/query-be25896189a30862a0aa0b6d1d6ba44278b98d4b8d027036e8871853f5d175c0.json b/crates/storage-pg/.sqlx/query-aabefb019c9195b0588882fef562472d6117ff68f8f37d02b7609c94aefdb5d6.json similarity index 78% rename from crates/storage-pg/.sqlx/query-be25896189a30862a0aa0b6d1d6ba44278b98d4b8d027036e8871853f5d175c0.json rename to crates/storage-pg/.sqlx/query-aabefb019c9195b0588882fef562472d6117ff68f8f37d02b7609c94aefdb5d6.json index d99085a3..8cf4db46 100644 --- a/crates/storage-pg/.sqlx/query-be25896189a30862a0aa0b6d1d6ba44278b98d4b8d027036e8871853f5d175c0.json +++ b/crates/storage-pg/.sqlx/query-aabefb019c9195b0588882fef562472d6117ff68f8f37d02b7609c94aefdb5d6.json @@ -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" } diff --git a/crates/storage-pg/.sqlx/query-61dc64c1980b5d1d2e2b52c8c55c91e1953595e413bedcec27eafbf87e42f1cd.json b/crates/storage-pg/.sqlx/query-f0977fee9b3919707e9aa20537d836d741acd5b2426218a2f9f9dab4fb8a2ad0.json similarity index 77% rename from crates/storage-pg/.sqlx/query-61dc64c1980b5d1d2e2b52c8c55c91e1953595e413bedcec27eafbf87e42f1cd.json rename to crates/storage-pg/.sqlx/query-f0977fee9b3919707e9aa20537d836d741acd5b2426218a2f9f9dab4fb8a2ad0.json index 79e26fcd..59ea3ae1 100644 --- a/crates/storage-pg/.sqlx/query-61dc64c1980b5d1d2e2b52c8c55c91e1953595e413bedcec27eafbf87e42f1cd.json +++ b/crates/storage-pg/.sqlx/query-f0977fee9b3919707e9aa20537d836d741acd5b2426218a2f9f9dab4fb8a2ad0.json @@ -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" } diff --git a/crates/storage-pg/.sqlx/query-b83fd5c55a209151ce5053b56034c49b5972df523f21a17be76303bde4a88522.json b/crates/storage-pg/.sqlx/query-fa5a57505066d03828015f79fdb1079eaea13cf7698073f2d5f74d4c50f7b094.json similarity index 78% rename from crates/storage-pg/.sqlx/query-b83fd5c55a209151ce5053b56034c49b5972df523f21a17be76303bde4a88522.json rename to crates/storage-pg/.sqlx/query-fa5a57505066d03828015f79fdb1079eaea13cf7698073f2d5f74d4c50f7b094.json index eb02cf6b..d0ee1ad1 100644 --- a/crates/storage-pg/.sqlx/query-b83fd5c55a209151ce5053b56034c49b5972df523f21a17be76303bde4a88522.json +++ b/crates/storage-pg/.sqlx/query-fa5a57505066d03828015f79fdb1079eaea13cf7698073f2d5f74d4c50f7b094.json @@ -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" } diff --git a/crates/storage-pg/migrations/20231207090532_oauth_device_code_grant.sql b/crates/storage-pg/migrations/20231207090532_oauth_device_code_grant.sql index 7f471f7b..b90dd150 100644 --- a/crates/storage-pg/migrations/20231207090532_oauth_device_code_grant.sql +++ b/crates/storage-pg/migrations/20231207090532_oauth_device_code_grant.sql @@ -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 ); diff --git a/crates/storage-pg/src/oauth2/device_code_grant.rs b/crates/storage-pg/src/oauth2/device_code_grant.rs index 519c1dd2..480f605b 100644 --- a/crates/storage-pg/src/oauth2/device_code_grant.rs +++ b/crates/storage-pg/src/oauth2/device_code_grant.rs @@ -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>, user_session_id: Option, oauth2_session_id: Option, + ip_address: Option, + user_agent: Option, } impl TryFrom for DeviceCodeGrant { @@ -73,6 +77,8 @@ impl TryFrom for DeviceCodeGrant { exchanged_at, user_session_id, oauth2_session_id, + ip_address, + user_agent, }: OAuth2DeviceGrantLookup, ) -> Result { let id = Ulid::from(oauth2_device_code_grant_id); @@ -133,6 +139,8 @@ impl TryFrom 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<' ¶ms.user_code, created_at, expires_at, + params.ip_address as Option, + 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 diff --git a/crates/storage-pg/src/oauth2/mod.rs b/crates/storage-pg/src/oauth2/mod.rs index 4e0a5d40..7c37085b 100644 --- a/crates/storage-pg/src/oauth2/mod.rs +++ b/crates/storage-pg/src/oauth2/mod.rs @@ -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 diff --git a/crates/storage/src/oauth2/device_code_grant.rs b/crates/storage/src/oauth2/device_code_grant.rs index 4f8678a1..579b1524 100644 --- a/crates/storage/src/oauth2/device_code_grant.rs +++ b/crates/storage/src/oauth2/device_code_grant.rs @@ -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, + + /// The user agent from which the request was made + pub user_agent: Option, } /// An [`OAuth2DeviceCodeGrantRepository`] helps interacting with diff --git a/crates/templates/src/context.rs b/crates/templates/src/context.rs index 60cfddb5..06529d8a 100644 --- a/crates/templates/src/context.rs +++ b/crates/templates/src/context.rs @@ -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 } }) diff --git a/templates/pages/device_consent.html b/templates/pages/device_consent.html index fadc5424..3fda8d35 100644 --- a/templates/pages/device_consent.html +++ b/templates/pages/device_consent.html @@ -33,13 +33,18 @@ limitations under the License.

Allow access to your account?