mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-08-26 02:39:55 +00:00
Infer an initial name for the device for the device auth grant (#5750)
This commit is contained in:
@@ -193,6 +193,11 @@ pub struct DeviceCodeGrant {
|
||||
|
||||
/// The user agent used to request this device code grant.
|
||||
pub user_agent: Option<String>,
|
||||
|
||||
/// The locale detected from the browser which fulfilled this device code
|
||||
/// grant. Used to render a human-readable device name. [`None`] until the
|
||||
/// grant is fulfilled.
|
||||
pub locale: Option<String>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for DeviceCodeGrant {
|
||||
@@ -215,10 +220,12 @@ impl DeviceCodeGrant {
|
||||
pub fn fulfill(
|
||||
self,
|
||||
browser_session: &BrowserSession,
|
||||
locale: Option<String>,
|
||||
fulfilled_at: DateTime<Utc>,
|
||||
) -> Result<Self, InvalidTransitionError> {
|
||||
Ok(Self {
|
||||
state: self.state.fulfill(browser_session, fulfilled_at)?,
|
||||
locale,
|
||||
..self
|
||||
})
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ pub(crate) async fn post(
|
||||
}
|
||||
|
||||
repo.oauth2_device_code_grant()
|
||||
.fulfill(&clock, grant, &session)
|
||||
.fulfill(&clock, grant, &session, Some(locale.to_string()))
|
||||
.await?
|
||||
}
|
||||
Action::Reject => {
|
||||
|
||||
@@ -382,6 +382,7 @@ pub(crate) async fn post(
|
||||
&client,
|
||||
&key_store,
|
||||
&url_builder,
|
||||
&templates,
|
||||
&site_config,
|
||||
repo,
|
||||
&homeserver,
|
||||
@@ -870,6 +871,7 @@ async fn device_code_grant(
|
||||
client: &Client,
|
||||
key_store: &Keystore,
|
||||
url_builder: &UrlBuilder,
|
||||
templates: &Templates,
|
||||
site_config: &SiteConfig,
|
||||
mut repo: BoxRepository,
|
||||
homeserver: &Arc<dyn HomeserverConnection>,
|
||||
@@ -924,6 +926,12 @@ async fn device_code_grant(
|
||||
.await?
|
||||
.ok_or(RouteError::NoSuchBrowserSession(browser_session_id))?;
|
||||
|
||||
// Generate a device name, using the locale captured from the browser which
|
||||
// fulfilled the grant
|
||||
let lang: DataLocale = grant.locale.as_deref().unwrap_or("en").parse()?;
|
||||
let ctx = DeviceNameContext::new(client.clone(), user_agent.clone()).with_language(lang);
|
||||
let device_name = templates.render_device_name(&ctx)?;
|
||||
|
||||
// Start the session
|
||||
let mut session = repo
|
||||
.oauth2_session()
|
||||
@@ -1001,7 +1009,11 @@ async fn device_code_grant(
|
||||
// We're using an upsert so if the device already exists for some reason
|
||||
// (like when a concurrent device sync happening) it won't have any effect.
|
||||
homeserver
|
||||
.upsert_device(&browser_session.user.username, device.as_str(), None)
|
||||
.upsert_device(
|
||||
&browser_session.user.username,
|
||||
device.as_str(),
|
||||
Some(&device_name),
|
||||
)
|
||||
.await
|
||||
.map_err(RouteError::ProvisionDeviceFailed)?;
|
||||
}
|
||||
@@ -1787,7 +1799,7 @@ mod tests {
|
||||
// And fulfill it
|
||||
let grant = repo
|
||||
.oauth2_device_code_grant()
|
||||
.fulfill(&state.clock, grant, &browser_session)
|
||||
.fulfill(&state.clock, grant, &browser_session, Some("en".to_owned()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
+8
-2
@@ -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 , ip_address as \"ip_address: IpAddr\"\n , user_agent\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 , locale\n FROM\n oauth2_device_code_grant\n\n WHERE user_code = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -72,6 +72,11 @@
|
||||
"ordinal": 13,
|
||||
"name": "user_agent",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "locale",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -93,8 +98,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "75a62d170e4c959a14c5698f1da983113e7d1bc565d01e85c158856abb17ddc6"
|
||||
"hash": "89674770c8d7929382e266a88937df017289d5cabe62ad00b115a2a408f4936b"
|
||||
}
|
||||
+3
-2
@@ -1,16 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE oauth2_device_code_grant\n SET fulfilled_at = $1\n , user_session_id = $2\n WHERE oauth2_device_code_grant_id = $3\n ",
|
||||
"query": "\n UPDATE oauth2_device_code_grant\n SET fulfilled_at = $1\n , user_session_id = $2\n , locale = $3\n WHERE oauth2_device_code_grant_id = $4\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz",
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d26e42d9fd2b2ee3cf9702c1666d83e7cffa26b320ae1442c7f3e22376c4a4ee"
|
||||
"hash": "9aa97bc9530456bb3044f84c4d83f8fd8f85cce92c46b40afbcf1e48e7c80b2a"
|
||||
}
|
||||
+8
-2
@@ -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 , ip_address as \"ip_address: IpAddr\"\n , user_agent\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 , locale\n FROM\n oauth2_device_code_grant\n\n WHERE device_code = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -72,6 +72,11 @@
|
||||
"ordinal": 13,
|
||||
"name": "user_agent",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "locale",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -93,8 +98,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "bb141d28c0c82244f31d542038c314d05ceb3a7b8f35397c0faef3b36d2d14a7"
|
||||
"hash": "ca8db2854f873a17f0ead6adfefe658aa71cade3ee70d17ee09a334599edc561"
|
||||
}
|
||||
+8
-2
@@ -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 , 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 ",
|
||||
"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 , locale\n FROM\n oauth2_device_code_grant\n\n WHERE oauth2_device_code_grant_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -72,6 +72,11 @@
|
||||
"ordinal": 13,
|
||||
"name": "user_agent",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "locale",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -93,8 +98,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "89041298e272d15c21e2b7127bd16c5a4f48e2be87dc26e9d0e3a932c9c49dfb"
|
||||
"hash": "e0dd18421e3b19079f291ba0ae9ba64031c0e635e5b40684a26565d50b3905f2"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Copyright 2026 Element Creations Ltd.
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
-- Please see LICENSE files in the repository root for full details.
|
||||
|
||||
-- Stores the locale detected from the browser which fulfilled the device code
|
||||
-- grant, so that the token endpoint can render a human-readable device name.
|
||||
ALTER TABLE "oauth2_device_code_grant"
|
||||
ADD COLUMN "locale" TEXT;
|
||||
@@ -50,6 +50,7 @@ struct OAuth2DeviceGrantLookup {
|
||||
oauth2_session_id: Option<Uuid>,
|
||||
ip_address: Option<IpAddr>,
|
||||
user_agent: Option<String>,
|
||||
locale: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<OAuth2DeviceGrantLookup> for DeviceCodeGrant {
|
||||
@@ -71,6 +72,7 @@ impl TryFrom<OAuth2DeviceGrantLookup> for DeviceCodeGrant {
|
||||
oauth2_session_id,
|
||||
ip_address,
|
||||
user_agent,
|
||||
locale,
|
||||
}: OAuth2DeviceGrantLookup,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let id = Ulid::from(oauth2_device_code_grant_id);
|
||||
@@ -133,6 +135,7 @@ impl TryFrom<OAuth2DeviceGrantLookup> for DeviceCodeGrant {
|
||||
expires_at,
|
||||
ip_address,
|
||||
user_agent,
|
||||
locale,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -207,6 +210,7 @@ impl OAuth2DeviceCodeGrantRepository for PgOAuth2DeviceCodeGrantRepository<'_> {
|
||||
expires_at,
|
||||
ip_address: params.ip_address,
|
||||
user_agent: params.user_agent,
|
||||
locale: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -237,6 +241,7 @@ impl OAuth2DeviceCodeGrantRepository for PgOAuth2DeviceCodeGrantRepository<'_> {
|
||||
, oauth2_session_id
|
||||
, ip_address as "ip_address: IpAddr"
|
||||
, user_agent
|
||||
, locale
|
||||
FROM
|
||||
oauth2_device_code_grant
|
||||
|
||||
@@ -283,6 +288,7 @@ impl OAuth2DeviceCodeGrantRepository for PgOAuth2DeviceCodeGrantRepository<'_> {
|
||||
, oauth2_session_id
|
||||
, ip_address as "ip_address: IpAddr"
|
||||
, user_agent
|
||||
, locale
|
||||
FROM
|
||||
oauth2_device_code_grant
|
||||
|
||||
@@ -329,6 +335,7 @@ impl OAuth2DeviceCodeGrantRepository for PgOAuth2DeviceCodeGrantRepository<'_> {
|
||||
, oauth2_session_id
|
||||
, ip_address as "ip_address: IpAddr"
|
||||
, user_agent
|
||||
, locale
|
||||
FROM
|
||||
oauth2_device_code_grant
|
||||
|
||||
@@ -362,10 +369,11 @@ impl OAuth2DeviceCodeGrantRepository for PgOAuth2DeviceCodeGrantRepository<'_> {
|
||||
clock: &dyn Clock,
|
||||
device_code_grant: DeviceCodeGrant,
|
||||
browser_session: &BrowserSession,
|
||||
locale: Option<String>,
|
||||
) -> Result<DeviceCodeGrant, Self::Error> {
|
||||
let fulfilled_at = clock.now();
|
||||
let device_code_grant = device_code_grant
|
||||
.fulfill(browser_session, fulfilled_at)
|
||||
.fulfill(browser_session, locale, fulfilled_at)
|
||||
.map_err(DatabaseError::to_invalid_operation)?;
|
||||
|
||||
let res = sqlx::query!(
|
||||
@@ -373,10 +381,12 @@ impl OAuth2DeviceCodeGrantRepository for PgOAuth2DeviceCodeGrantRepository<'_> {
|
||||
UPDATE oauth2_device_code_grant
|
||||
SET fulfilled_at = $1
|
||||
, user_session_id = $2
|
||||
WHERE oauth2_device_code_grant_id = $3
|
||||
, locale = $3
|
||||
WHERE oauth2_device_code_grant_id = $4
|
||||
"#,
|
||||
fulfilled_at,
|
||||
Uuid::from(browser_session.id),
|
||||
device_code_grant.locale.as_deref(),
|
||||
Uuid::from(device_code_grant.id),
|
||||
)
|
||||
.traced()
|
||||
|
||||
@@ -1055,14 +1055,15 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(lookup.as_ref(), Some(&grant));
|
||||
|
||||
// Let's mark it as fulfilled
|
||||
// Let's mark it as fulfilled, with a locale captured from the browser
|
||||
let grant = repo
|
||||
.oauth2_device_code_grant()
|
||||
.fulfill(&clock, grant, &browser_session)
|
||||
.fulfill(&clock, grant, &browser_session, Some("en".to_owned()))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!grant.is_pending());
|
||||
assert!(grant.is_fulfilled());
|
||||
assert_eq!(grant.locale.as_deref(), Some("en"));
|
||||
|
||||
// Check that we can't mark it as rejected now
|
||||
let res = repo
|
||||
@@ -1079,10 +1080,13 @@ mod tests {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
// The locale was persisted
|
||||
assert_eq!(grant.locale.as_deref(), Some("en"));
|
||||
|
||||
// We can't mark it as fulfilled again
|
||||
let res = repo
|
||||
.oauth2_device_code_grant()
|
||||
.fulfill(&clock, grant, &browser_session)
|
||||
.fulfill(&clock, grant, &browser_session, None)
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
|
||||
@@ -1166,7 +1170,7 @@ mod tests {
|
||||
// We can't mark it as fulfilled
|
||||
let res = repo
|
||||
.oauth2_device_code_grant()
|
||||
.fulfill(&clock, grant, &browser_session)
|
||||
.fulfill(&clock, grant, &browser_session, None)
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
|
||||
|
||||
@@ -123,6 +123,8 @@ pub trait OAuth2DeviceCodeGrantRepository: Send + Sync {
|
||||
/// * `device_code_grant`: The device code grant to fulfill
|
||||
/// * `browser_session`: The browser session which was used to fulfill the
|
||||
/// device code grant
|
||||
/// * `locale`: The locale detected from the browser which fulfilled the
|
||||
/// grant, used later to render a human-readable device name
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
@@ -135,6 +137,7 @@ pub trait OAuth2DeviceCodeGrantRepository: Send + Sync {
|
||||
clock: &dyn Clock,
|
||||
device_code_grant: DeviceCodeGrant,
|
||||
browser_session: &BrowserSession,
|
||||
locale: Option<String>,
|
||||
) -> Result<DeviceCodeGrant, Self::Error>;
|
||||
|
||||
/// Mark the device code grant as rejected with the given browser session
|
||||
@@ -235,6 +238,7 @@ repository_impl!(OAuth2DeviceCodeGrantRepository:
|
||||
clock: &dyn Clock,
|
||||
device_code_grant: DeviceCodeGrant,
|
||||
browser_session: &BrowserSession,
|
||||
locale: Option<String>,
|
||||
) -> Result<DeviceCodeGrant, Self::Error>;
|
||||
|
||||
async fn reject(
|
||||
|
||||
@@ -835,6 +835,7 @@ impl TemplateContext for PolicyViolationContext {
|
||||
expires_at: now + Duration::try_minutes(25).unwrap(),
|
||||
ip_address: None,
|
||||
user_agent: None,
|
||||
locale: None,
|
||||
},
|
||||
client,
|
||||
Vec::new(),
|
||||
@@ -1856,6 +1857,7 @@ impl TemplateContext for DeviceConsentContext {
|
||||
expires_at: now + Duration::try_minutes(25).unwrap(),
|
||||
ip_address: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
||||
user_agent: Some("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()),
|
||||
locale: None,
|
||||
};
|
||||
Self {
|
||||
grant,
|
||||
|
||||
Reference in New Issue
Block a user