Skip to content

Commit 7c0eeec

Browse files
committed
Generate a device name based on the client name and user agent
1 parent fc94c75 commit 7c0eeec

File tree

7 files changed

+120
-26
lines changed

7 files changed

+120
-26
lines changed

crates/handlers/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ where
203203
Encrypter: FromRef<S>,
204204
reqwest::Client: FromRef<S>,
205205
SiteConfig: FromRef<S>,
206+
Templates: FromRef<S>,
206207
Arc<dyn HomeserverConnection>: FromRef<S>,
207208
BoxClock: FromRequestParts<S>,
208209
BoxRng: FromRequestParts<S>,

crates/handlers/src/oauth2/token.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use mas_axum_utils::{
1818
use mas_data_model::{
1919
AuthorizationGrantStage, Client, Device, DeviceCodeGrantState, SiteConfig, TokenType,
2020
};
21+
use mas_i18n::DataLocale;
2122
use mas_keystore::{Encrypter, Keystore};
2223
use mas_matrix::HomeserverConnection;
2324
use mas_oidc_client::types::scope::ScopeToken;
@@ -31,6 +32,7 @@ use mas_storage::{
3132
},
3233
user::BrowserSessionRepository,
3334
};
35+
use mas_templates::{DeviceNameContext, TemplateContext, Templates};
3436
use oauth2_types::{
3537
errors::{ClientError, ClientErrorCode},
3638
pkce::CodeChallengeError,
@@ -261,6 +263,8 @@ impl IntoResponse for RouteError {
261263
}
262264
}
263265

266+
impl_from_error_for_route!(mas_i18n::DataError);
267+
impl_from_error_for_route!(mas_templates::TemplateError);
264268
impl_from_error_for_route!(mas_storage::RepositoryError);
265269
impl_from_error_for_route!(mas_policy::EvaluationError);
266270
impl_from_error_for_route!(super::IdTokenSignatureError);
@@ -281,6 +285,7 @@ pub(crate) async fn post(
281285
State(homeserver): State<Arc<dyn HomeserverConnection>>,
282286
State(site_config): State<SiteConfig>,
283287
State(encrypter): State<Encrypter>,
288+
State(templates): State<Templates>,
284289
policy: Policy,
285290
user_agent: Option<TypedHeader<headers::UserAgent>>,
286291
client_authorization: ClientAuthorization<AccessTokenRequest>,
@@ -334,6 +339,7 @@ pub(crate) async fn post(
334339
&site_config,
335340
repo,
336341
&homeserver,
342+
&templates,
337343
user_agent,
338344
)
339345
.await?
@@ -415,6 +421,7 @@ async fn authorization_code_grant(
415421
site_config: &SiteConfig,
416422
mut repo: BoxRepository,
417423
homeserver: &Arc<dyn HomeserverConnection>,
424+
templates: &Templates,
418425
user_agent: Option<String>,
419426
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
420427
// Check that the client is allowed to use this grant type
@@ -482,6 +489,11 @@ async fn authorization_code_grant(
482489
.await?
483490
.ok_or(RouteError::NoSuchOAuthSession(session_id))?;
484491

492+
// Generate a device name
493+
let lang: DataLocale = authz_grant.locale.as_deref().unwrap_or("en").parse()?;
494+
let ctx = DeviceNameContext::new(client.clone(), user_agent.clone()).with_language(lang);
495+
let device_name = templates.render_device_name(&ctx)?;
496+
485497
if let Some(user_agent) = user_agent {
486498
session = repo
487499
.oauth2_session()
@@ -567,7 +579,7 @@ async fn authorization_code_grant(
567579
for scope in &*session.scope {
568580
if let Some(device) = Device::from_scope_token(scope) {
569581
homeserver
570-
.create_device(&mxid, device.as_str(), None)
582+
.create_device(&mxid, device.as_str(), Some(&device_name))
571583
.await
572584
.map_err(RouteError::ProvisionDeviceFailed)?;
573585
}

crates/i18n/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ mod translator;
1111
pub use icu_calendar;
1212
pub use icu_datetime;
1313
pub use icu_locid::locale;
14-
pub use icu_provider::DataLocale;
14+
pub use icu_provider::{DataError, DataLocale};
1515

1616
pub use self::{
1717
sprintf::{Argument, ArgumentList, Message},

crates/templates/src/context.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1564,6 +1564,39 @@ impl TemplateContext for AccountInactiveContext {
15641564
}
15651565
}
15661566

1567+
/// Context used by the `device_name.txt` template
1568+
#[derive(Serialize)]
1569+
pub struct DeviceNameContext {
1570+
client: Client,
1571+
raw_user_agent: String,
1572+
}
1573+
1574+
impl DeviceNameContext {
1575+
/// Constructs a new context with a client and user agent
1576+
#[must_use]
1577+
pub fn new(client: Client, user_agent: Option<String>) -> Self {
1578+
Self {
1579+
client,
1580+
raw_user_agent: user_agent.unwrap_or_default(),
1581+
}
1582+
}
1583+
}
1584+
1585+
impl TemplateContext for DeviceNameContext {
1586+
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1587+
where
1588+
Self: Sized,
1589+
{
1590+
Client::samples(now, rng)
1591+
.into_iter()
1592+
.map(|client| DeviceNameContext {
1593+
client,
1594+
raw_user_agent: "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(),
1595+
})
1596+
.collect()
1597+
}
1598+
}
1599+
15671600
/// Context used by the `form_post.html` template
15681601
#[derive(Serialize)]
15691602
pub struct FormPostContext<T> {

crates/templates/src/lib.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ mod macros;
3535
pub use self::{
3636
context::{
3737
AccountInactiveContext, ApiDocContext, AppContext, CompatSsoContext, ConsentContext,
38-
DeviceConsentContext, DeviceLinkContext, DeviceLinkFormField, EmailRecoveryContext,
39-
EmailVerificationContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
40-
LoginContext, LoginFormField, NotFoundContext, PasswordRegisterContext,
41-
PolicyViolationContext, PostAuthContext, PostAuthContextInner, RecoveryExpiredContext,
42-
RecoveryFinishContext, RecoveryFinishFormField, RecoveryProgressContext,
43-
RecoveryStartContext, RecoveryStartFormField, RegisterContext, RegisterFormField,
44-
RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField,
38+
DeviceConsentContext, DeviceLinkContext, DeviceLinkFormField, DeviceNameContext,
39+
EmailRecoveryContext, EmailVerificationContext, EmptyContext, ErrorContext,
40+
FormPostContext, IndexContext, LoginContext, LoginFormField, NotFoundContext,
41+
PasswordRegisterContext, PolicyViolationContext, PostAuthContext, PostAuthContextInner,
42+
RecoveryExpiredContext, RecoveryFinishContext, RecoveryFinishFormField,
43+
RecoveryProgressContext, RecoveryStartContext, RecoveryStartFormField, RegisterContext,
44+
RegisterFormField, RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField,
4545
RegisterStepsEmailInUseContext, RegisterStepsVerifyEmailContext,
4646
RegisterStepsVerifyEmailFormField, SiteBranding, SiteConfigExt, SiteFeatures,
4747
TemplateContext, UpstreamExistingLinkContext, UpstreamRegister, UpstreamRegisterFormField,
@@ -417,6 +417,9 @@ register_templates! {
417417

418418
/// Render the 'account logged out' page
419419
pub fn render_account_logged_out(WithLanguage<WithCsrf<AccountInactiveContext>>) { "pages/account/logged_out.html" }
420+
421+
/// Render the automatic device name for OAuth 2.0 client
422+
pub fn render_device_name(WithLanguage<DeviceNameContext>) { "device_name.txt" }
420423
}
421424

422425
impl Templates {
@@ -459,6 +462,7 @@ impl Templates {
459462
check::render_upstream_oauth2_link_mismatch(self, now, rng)?;
460463
check::render_upstream_oauth2_suggest_link(self, now, rng)?;
461464
check::render_upstream_oauth2_do_register(self, now, rng)?;
465+
check::render_device_name(self, now, rng)?;
462466
Ok(())
463467
}
464468
}

templates/device_name.txt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{#
2+
Copyright 2024, 2025 New Vector Ltd.
3+
Copyright 2021-2024 The Matrix.org Foundation C.I.C.
4+
5+
SPDX-License-Identifier: AGPL-3.0-only
6+
Please see LICENSE in the repository root for full details.
7+
-#}
8+
9+
{%- set _ = translator(lang) -%}
10+
11+
{%- set client_name = client.client_name or client.client_id -%}
12+
{%- set user_agent = raw_user_agent | parse_user_agent() -%}
13+
14+
{%- set device_name -%}
15+
{%- if user_agent.model -%}
16+
{{- user_agent.model -}}
17+
{%- elif user_agent.name -%}
18+
{%- if user_agent.os -%}
19+
{{- _("mas.device_display_name.name_for_platform", name=user_agent.name, platform=user_agent.os) -}}
20+
{%- else -%}
21+
{{- user_agent.name -}}
22+
{%- endif -%}
23+
{%- else -%}
24+
{{- _("mas.device_display_name.unknown_device") -}}
25+
{%- endif -%}
26+
{%- endset -%}
27+
28+
{{- _("mas.device_display_name.client_on_device", client_name=client_name, device_name=device_name) -}}

translations/en.json

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
},
77
"cancel": "Cancel",
88
"@cancel": {
9-
"context": "pages/consent.html:69:11-29, pages/device_consent.html:126:13-31, pages/policy_violation.html:44:13-31"
9+
"context": "pages/consent.html:69:11-29, pages/device_consent.html:127:13-31, pages/policy_violation.html:44:13-31"
1010
},
1111
"continue": "Continue",
1212
"@continue": {
13-
"context": "form_post.html:25:28-48, pages/consent.html:57:28-48, pages/device_consent.html:123:13-33, pages/device_link.html:40:26-46, pages/login.html:68:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:74:26-46, pages/register/steps/display_name.html:43:28-48, pages/register/steps/verify_email.html:51:26-46, pages/sso.html:37:28-48"
13+
"context": "form_post.html:25:28-48, pages/consent.html:57:28-48, pages/device_consent.html:124:13-33, pages/device_link.html:40:26-46, pages/login.html:68:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:74:26-46, pages/register/steps/display_name.html:43:28-48, pages/register/steps/verify_email.html:51:26-46, pages/sso.html:37:28-48"
1414
},
1515
"create_account": "Create Account",
1616
"@create_account": {
@@ -22,7 +22,7 @@
2222
},
2323
"sign_out": "Sign out",
2424
"@sign_out": {
25-
"context": "pages/account/logged_out.html:22:28-48, pages/consent.html:65:28-48, pages/device_consent.html:135:30-50, pages/index.html:28:28-48, pages/policy_violation.html:38:28-48, pages/sso.html:45:28-48, pages/upstream_oauth2/link_mismatch.html:24:24-44, pages/upstream_oauth2/suggest_link.html:32:26-46"
25+
"context": "pages/account/logged_out.html:22:28-48, pages/consent.html:65:28-48, pages/device_consent.html:136:30-50, pages/index.html:28:28-48, pages/policy_violation.html:38:28-48, pages/sso.html:45:28-48, pages/upstream_oauth2/link_mismatch.html:24:24-44, pages/upstream_oauth2/suggest_link.html:32:26-46"
2626
},
2727
"skip": "Skip",
2828
"@skip": {
@@ -195,37 +195,37 @@
195195
},
196196
"heading": "Allow access to your account?",
197197
"@heading": {
198-
"context": "pages/consent.html:25:27-51, pages/device_consent.html:27:29-53"
198+
"context": "pages/consent.html:25:27-51, pages/device_consent.html:28:29-53"
199199
},
200200
"make_sure_you_trust": "Make sure that you trust <span>%(client_name)s</span>.",
201201
"@make_sure_you_trust": {
202-
"context": "pages/consent.html:38:81-142, pages/device_consent.html:103:83-144"
202+
"context": "pages/consent.html:38:81-142, pages/device_consent.html:104:83-144"
203203
},
204204
"this_will_allow": "This will allow <span>%(client_name)s</span> to:",
205205
"@this_will_allow": {
206-
"context": "pages/consent.html:28:11-68, pages/device_consent.html:93:13-70"
206+
"context": "pages/consent.html:28:11-68, pages/device_consent.html:94:13-70"
207207
},
208208
"you_may_be_sharing": "You may be sharing sensitive information with this site or app.",
209209
"@you_may_be_sharing": {
210-
"context": "pages/consent.html:39:7-42, pages/device_consent.html:104:9-44"
210+
"context": "pages/consent.html:39:7-42, pages/device_consent.html:105:9-44"
211211
}
212212
},
213213
"device_card": {
214214
"access_requested": "Access requested",
215215
"@access_requested": {
216-
"context": "pages/device_consent.html:81:34-71"
216+
"context": "pages/device_consent.html:82:34-71"
217217
},
218218
"device_code": "Code",
219219
"@device_code": {
220-
"context": "pages/device_consent.html:85:34-66"
220+
"context": "pages/device_consent.html:86:34-66"
221221
},
222222
"generic_device": "Device",
223223
"@generic_device": {
224-
"context": "pages/device_consent.html:69:22-57"
224+
"context": "pages/device_consent.html:70:22-57"
225225
},
226226
"ip_address": "IP address",
227227
"@ip_address": {
228-
"context": "pages/device_consent.html:76:36-67"
228+
"context": "pages/device_consent.html:77:36-67"
229229
}
230230
},
231231
"device_code_link": {
@@ -241,29 +241,45 @@
241241
"device_consent": {
242242
"another_device_access": "Another device wants to access your account.",
243243
"@another_device_access": {
244-
"context": "pages/device_consent.html:92:13-58"
244+
"context": "pages/device_consent.html:93:13-58"
245245
},
246246
"denied": {
247247
"description": "You denied access to %(client_name)s. You can close this window.",
248248
"@description": {
249-
"context": "pages/device_consent.html:146:27-94"
249+
"context": "pages/device_consent.html:147:27-94"
250250
},
251251
"heading": "Access denied",
252252
"@heading": {
253-
"context": "pages/device_consent.html:145:29-67"
253+
"context": "pages/device_consent.html:146:29-67"
254254
}
255255
},
256256
"granted": {
257257
"description": "You granted access to %(client_name)s. You can close this window.",
258258
"@description": {
259-
"context": "pages/device_consent.html:157:27-95"
259+
"context": "pages/device_consent.html:158:27-95"
260260
},
261261
"heading": "Access granted",
262262
"@heading": {
263-
"context": "pages/device_consent.html:156:29-68"
263+
"context": "pages/device_consent.html:157:29-68"
264264
}
265265
}
266266
},
267+
"device_display_name": {
268+
"client_on_device": "%(client_name)s on %(device_name)s",
269+
"@client_on_device": {
270+
"context": "device_name.txt:28:4-99",
271+
"description": "The automatic device name generated for a client, e.g. 'Element on iPhone'"
272+
},
273+
"name_for_platform": "%(name)s for %(platform)s",
274+
"@name_for_platform": {
275+
"context": "device_name.txt:19:10-102",
276+
"description": "Part of the automatic device name for the platfom, e.g. 'Safari for macOS'"
277+
},
278+
"unknown_device": "Unknown device",
279+
"@unknown_device": {
280+
"context": "device_name.txt:24:8-51"
281+
}
282+
},
267283
"email_in_use": {
268284
"description": "If you have forgotten your account credentials, you can recover your account. You can also start over and use a different email address.",
269285
"@description": {
@@ -469,7 +485,7 @@
469485
},
470486
"not_you": "Not %(username)s?",
471487
"@not_you": {
472-
"context": "pages/consent.html:62:11-67, pages/device_consent.html:132:13-69, pages/sso.html:42:11-67",
488+
"context": "pages/consent.html:62:11-67, pages/device_consent.html:133:13-69, pages/sso.html:42:11-67",
473489
"description": "Suggestions for the user to log in as a different user"
474490
},
475491
"or_separator": "Or",

0 commit comments

Comments
 (0)