diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs index 33bc364a8..67bd87acf 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/util.rs @@ -252,6 +252,8 @@ pub fn site_config_from_config( dangerous_hard_limit_eviction: c.dangerous_hard_limit_eviction, }), device_code_grant_enabled: oauth_config.device_code_grant_enabled, + device_code_user_code_auto_fill_enabled: oauth_config + .device_code_user_code_auto_fill_enabled, }) } diff --git a/crates/config/src/sections/oauth.rs b/crates/config/src/sections/oauth.rs index 032222506..315fd36df 100644 --- a/crates/config/src/sections/oauth.rs +++ b/crates/config/src/sections/oauth.rs @@ -1,3 +1,4 @@ +// Copyright 2025, 2026 Element Creations Ltd. // Copyright 2025 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial @@ -12,11 +13,20 @@ const fn default_true() -> bool { true } +const fn default_false() -> bool { + false +} + #[expect(clippy::trivially_copy_pass_by_ref)] const fn is_default_true(value: &bool) -> bool { *value == default_true() } +#[expect(clippy::trivially_copy_pass_by_ref)] +const fn is_default_false(value: &bool) -> bool { + *value == default_false() +} + /// Configuration section for OAuth 2.0 protocol options #[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] pub struct OAuthConfig { @@ -30,12 +40,24 @@ pub struct OAuthConfig { /// rejected. #[serde(default = "default_true", skip_serializing_if = "is_default_true")] pub device_code_grant_enabled: bool, + + /// Whether the device authorization endpoint advertises a + /// `verification_uri_complete` that auto-fills the user code on the + /// `/link` page. Defaults to `false`. + /// + /// When disabled, the device authorization response will omit + /// `verification_uri_complete`, and the `/link` route will ignore any + /// `code` query parameter, forcing users to type their user code + /// manually. + #[serde(default = "default_false", skip_serializing_if = "is_default_false")] + pub device_code_user_code_auto_fill_enabled: bool, } impl Default for OAuthConfig { fn default() -> Self { Self { device_code_grant_enabled: default_true(), + device_code_user_code_auto_fill_enabled: default_false(), } } } @@ -44,6 +66,7 @@ impl OAuthConfig { /// Returns true if the configuration is the default one pub(crate) fn is_default(&self) -> bool { is_default_true(&self.device_code_grant_enabled) + && is_default_false(&self.device_code_user_code_auto_fill_enabled) } } diff --git a/crates/data-model/src/site_config.rs b/crates/data-model/src/site_config.rs index 98a95dd27..3a913cf3b 100644 --- a/crates/data-model/src/site_config.rs +++ b/crates/data-model/src/site_config.rs @@ -117,4 +117,9 @@ pub struct SiteConfig { /// Whether the Device Authorization Grant (RFC 8628) is enabled. pub device_code_grant_enabled: bool, + + /// Whether the device authorization endpoint advertises a + /// `verification_uri_complete` and whether `/link` accepts a `code` + /// query parameter to auto-fill the user code. + pub device_code_user_code_auto_fill_enabled: bool, } diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index ba34577d5..e55c788fb 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -459,7 +459,7 @@ where ) .route( mas_router::DeviceCodeLink::route(), - get(self::oauth2::device::link::get), + get(self::oauth2::device::link::get).post(self::oauth2::device::link::post), ) .route( mas_router::DeviceCodeConsent::route(), diff --git a/crates/handlers/src/oauth2/device/authorize.rs b/crates/handlers/src/oauth2/device/authorize.rs index fb2c61913..2ae28cb39 100644 --- a/crates/handlers/src/oauth2/device/authorize.rs +++ b/crates/handlers/src/oauth2/device/authorize.rs @@ -178,11 +178,15 @@ pub(crate) async fn post( repo.save().await?; + let verification_uri_complete = site_config + .device_code_user_code_auto_fill_enabled + .then(|| url_builder.device_code_link_full(device_code.user_code.clone())); + let response = DeviceAuthorizationResponse { device_code: device_code.device_code, - user_code: device_code.user_code.clone(), + user_code: device_code.user_code, verification_uri: url_builder.device_code_link(), - verification_uri_complete: Some(url_builder.device_code_link_full(device_code.user_code)), + verification_uri_complete, expires_in, interval: Some(Duration::microseconds(5 * 1000 * 1000)), }; @@ -242,6 +246,51 @@ mod tests { let response: DeviceAuthorizationResponse = response.json(); assert_eq!(response.device_code.len(), 32); assert_eq!(response.user_code.len(), 6); + assert!(response.verification_uri_complete.is_some()); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_device_code_request_no_auto_fill(pool: PgPool) { + setup(); + let state = TestState::from_pool_with_site_config( + pool, + SiteConfig { + device_code_user_code_auto_fill_enabled: false, + ..test_site_config() + }, + ) + .await + .unwrap(); + + // Provision a client + let request = + Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({ + "client_uri": "https://example.com/", + "token_endpoint_auth_method": "none", + "grant_types": ["urn:ietf:params:oauth:grant-type:device_code"], + "response_types": [], + })); + + let response = state.request(request).await; + response.assert_status(StatusCode::CREATED); + + let response: ClientRegistrationResponse = response.json(); + let client_id = response.client_id; + + // The endpoint omits verification_uri_complete from the response + let request = Request::post(mas_router::OAuth2DeviceAuthorizationEndpoint::PATH).form( + serde_json::json!({ + "client_id": client_id, + "scope": "openid", + }), + ); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + + let response: DeviceAuthorizationResponse = response.json(); + assert_eq!(response.device_code.len(), 32); + assert_eq!(response.user_code.len(), 6); + assert!(response.verification_uri_complete.is_none()); } #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] diff --git a/crates/handlers/src/oauth2/device/consent.rs b/crates/handlers/src/oauth2/device/consent.rs index 8d1d3b722..4799d503f 100644 --- a/crates/handlers/src/oauth2/device/consent.rs +++ b/crates/handlers/src/oauth2/device/consent.rs @@ -43,6 +43,10 @@ enum Action { #[derive(Deserialize, Debug)] pub(crate) struct ConsentForm { action: Action, + + // HTML form checkboxes are only sent when ticked, hence the Option. + #[serde(default)] + confirm_device: Option, } #[tracing::instrument(name = "handlers.oauth2.device.consent.get", skip_all)] @@ -291,6 +295,15 @@ pub(crate) async fn post( let grant = if grant.is_pending() { match form.action { Action::Consent => { + // The user must explicitly tick the "confirm this is my device" box. + // The browser enforces `required` client-side; this is the + // server-side safety net. + if form.confirm_device.is_none() { + return Err(InternalError::from_anyhow(anyhow::anyhow!( + "The device must be confirmed before consent can be granted" + ))); + } + repo.oauth2_device_code_grant() .fulfill(&clock, grant, &session) .await? diff --git a/crates/handlers/src/oauth2/device/link.rs b/crates/handlers/src/oauth2/device/link.rs index 8f988a600..dedd68abb 100644 --- a/crates/handlers/src/oauth2/device/link.rs +++ b/crates/handlers/src/oauth2/device/link.rs @@ -5,12 +5,18 @@ // Please see LICENSE files in the repository root for full details. use axum::{ + Form, extract::State, - response::{Html, IntoResponse}, + response::{Html, IntoResponse, Response}, }; use axum_extra::extract::Query; -use mas_axum_utils::{InternalError, cookies::CookieJar}; -use mas_data_model::BoxClock; +use mas_axum_utils::{ + InternalError, + cookies::CookieJar, + csrf::{CsrfExt, ProtectedForm}, +}; +use mas_data_model::{BoxClock, BoxRng}; +use mas_i18n::DataLocale; use mas_router::UrlBuilder; use mas_storage::BoxRepository; use mas_templates::{ @@ -28,25 +34,87 @@ pub struct Params { #[tracing::instrument(name = "handlers.oauth2.device.link.get", skip_all)] pub(crate) async fn get( + mut rng: BoxRng, clock: BoxClock, - mut repo: BoxRepository, + repo: BoxRepository, PreferredLanguage(locale): PreferredLanguage, State(templates): State, State(url_builder): State, State(site_config): State, cookie_jar: CookieJar, - Query(query): Query, -) -> Result { + Query(mut query): Query, +) -> Result { if !site_config.device_code_grant_enabled { return Err(InternalError::from_anyhow(anyhow::anyhow!( "The Device Authorization Grant is disabled" ))); } - let mut form_state = FormState::from_form(&query); + // When the auto-fill flow is disabled, ignore the `code` query parameter + // entirely — users must type their user code into the form. + if !site_config.device_code_user_code_auto_fill_enabled { + query.code = None; + } - // If we have a code in query, find it in the database - if let Some(code) = &query.code { - // Find the code in the database + handle_code( + &mut rng, + &clock, + repo, + &locale, + &templates, + &url_builder, + cookie_jar, + query, + ) + .await +} + +#[tracing::instrument(name = "handlers.oauth2.device.link.post", skip_all)] +pub(crate) async fn post( + mut rng: BoxRng, + clock: BoxClock, + repo: BoxRepository, + PreferredLanguage(locale): PreferredLanguage, + State(templates): State, + State(url_builder): State, + State(site_config): State, + cookie_jar: CookieJar, + Form(form): Form>, +) -> Result { + if !site_config.device_code_grant_enabled { + return Err(InternalError::from_anyhow(anyhow::anyhow!( + "The Device Authorization Grant is disabled" + ))); + } + + let form = cookie_jar.verify_form(&clock, form)?; + + handle_code( + &mut rng, + &clock, + repo, + &locale, + &templates, + &url_builder, + cookie_jar, + form, + ) + .await +} + +async fn handle_code( + rng: &mut BoxRng, + clock: &BoxClock, + mut repo: BoxRepository, + locale: &DataLocale, + templates: &Templates, + url_builder: &UrlBuilder, + cookie_jar: CookieJar, + params: Params, +) -> Result { + let mut form_state = FormState::from_form(¶ms); + + // If we have a code, find it in the database + if let Some(code) = ¶ms.code { let code = code.to_uppercase(); let grant = repo .oauth2_device_code_grant() @@ -68,10 +136,13 @@ pub(crate) async fn get( form_state = form_state.with_error_on_field(DeviceLinkFormField::Code, FieldError::Invalid); } - // Rendre the form + let (csrf_token, cookie_jar) = cookie_jar.csrf_token(clock, rng); + + // Render the form let ctx = DeviceLinkContext::new() .with_form_state(form_state) - .with_language(locale); + .with_csrf(csrf_token.form_value()) + .with_language(locale.clone()); let content = templates.render_device_link(&ctx)?; diff --git a/crates/handlers/src/test_utils.rs b/crates/handlers/src/test_utils.rs index 264d1fa7a..a9e183754 100644 --- a/crates/handlers/src/test_utils.rs +++ b/crates/handlers/src/test_utils.rs @@ -151,6 +151,7 @@ pub fn test_site_config() -> SiteConfig { plan_management_iframe_uri: None, session_limit: None, device_code_grant_enabled: true, + device_code_user_code_auto_fill_enabled: true, } } diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs index 9088f9224..24316e5dd 100644 --- a/crates/templates/src/lib.rs +++ b/crates/templates/src/lib.rs @@ -453,7 +453,7 @@ register_templates! { pub fn render_upstream_oauth2_do_register(WithLanguage>) { "pages/upstream_oauth2/do_register.html" } /// Render the device code link page - pub fn render_device_link(WithLanguage) { "pages/device_link.html" } + pub fn render_device_link(WithLanguage>) { "pages/device_link.html" } /// Render the device code consent page pub fn render_device_consent(WithLanguage>>) { "pages/device_consent.html" } diff --git a/docs/config.schema.json b/docs/config.schema.json index 055c5504e..7cf6089b0 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -2828,6 +2828,10 @@ "device_code_grant_enabled": { "description": "Whether the Device Authorization Grant (RFC 8628) is enabled. Defaults\n to `true`.\n\n When disabled, the device authorization endpoint will reject requests,\n the discovery metadata will not advertise the device authorization\n endpoint, and dynamic client registrations requesting the\n `urn:ietf:params:oauth:grant-type:device_code` grant type will be\n rejected.", "type": "boolean" + }, + "device_code_user_code_auto_fill_enabled": { + "description": "Whether the device authorization endpoint advertises a\n `verification_uri_complete` that auto-fills the user code on the\n `/link` page. Defaults to `false`.\n\n When disabled, the device authorization response will omit\n `verification_uri_complete`, and the `/link` route will ignore any\n `code` query parameter, forcing users to type their user code\n manually.", + "type": "boolean" } } }, diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index e2d721db7..d025765f9 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -871,6 +871,16 @@ oauth: # `urn:ietf:params:oauth:grant-type:device_code` grant type will be # rejected. device_code_grant_enabled: true + + # Whether the device authorization endpoint advertises a + # `verification_uri_complete` that auto-fills the user code on the + # `/link` page. Defaults to `true`. + # + # When disabled, the device authorization response will omit + # `verification_uri_complete`, and the `/link` route will ignore any + # `code` query parameter, forcing users to type their user code + # manually. + device_code_user_code_auto_fill_enabled: true ``` ## `experimental` diff --git a/frontend/src/components/PageHeading/PageHeading.module.css b/frontend/src/components/PageHeading/PageHeading.module.css index 42e0b4774..8a4c555ec 100644 --- a/frontend/src/components/PageHeading/PageHeading.module.css +++ b/frontend/src/components/PageHeading/PageHeading.module.css @@ -55,8 +55,8 @@ text-align: center; & .title { - font: var(--cpd-font-heading-md-semibold); - letter-spacing: var(--cpd-font-letter-spacing-heading-xl); + font: var(--cpd-font-heading-lg-semibold); + letter-spacing: var(--cpd-font-letter-spacing-heading-lg); color: var(--cpd-color-text-primary); text-wrap: balance; } diff --git a/frontend/src/entrypoints/templates.css b/frontend/src/entrypoints/templates.css index f9676e4fb..b72f5d470 100644 --- a/frontend/src/entrypoints/templates.css +++ b/frontend/src/entrypoints/templates.css @@ -5,6 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ +@import "../styles/cpd-alert.css"; @import "../styles/cpd-button.css"; @import "../styles/cpd-form.css"; @import "../styles/cpd-link.css"; diff --git a/frontend/src/styles/cpd-alert.css b/frontend/src/styles/cpd-alert.css new file mode 100644 index 000000000..c81d78b50 --- /dev/null +++ b/frontend/src/styles/cpd-alert.css @@ -0,0 +1,85 @@ +/* Copyright 2024, 2025 New Vector Ltd. + * Copyright 2023, 2024 The Matrix.org Foundation C.I.C. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +.cpd-alert { + container-type: inline-size; + container-name: cpd-alert; + display: flex; + align-items: start; + justify-content: start; + gap: var(--cpd-space-3x); + padding: var(--cpd-space-4x); + border-radius: 8px; + border: 1px solid; +} + +.cpd-alert[data-type="success"] { + background-color: var(--cpd-color-green-200); + border-color: var(--cpd-color-green-500); +} + +.cpd-alert[data-type="critical"] { + background-color: var(--cpd-color-red-200); + border-color: var(--cpd-color-red-500); +} + +.cpd-alert[data-type="info"] { + background-color: var(--cpd-color-blue-200); + border-color: var(--cpd-color-blue-500); +} + +.cpd-alert-content { + flex: 1; + display: flex; + flex-direction: row; + gap: var(--cpd-space-3x); +} + +.cpd-alert-text-content { + flex: 1 1 0; +} + +.cpd-alert[data-type="success"] :is(.cpd-alert-title, .cpd-alert-icon) { + color: var(--cpd-color-green-900); +} + +.cpd-alert[data-type="critical"] :is(.cpd-alert-title, .cpd-alert-icon) { + color: var(--cpd-color-red-900); +} + +.cpd-alert[data-type="info"] :is(.cpd-alert-title, .cpd-alert-icon) { + color: var(--cpd-color-blue-900); +} + +.cpd-alert p { + margin: 0; +} + +.cpd-alert-actions { + flex: 0; + display: flex; + flex-direction: row; + gap: var(--cpd-space-1x); + align-self: center; +} + +.cpd-alert-icon { + flex-shrink: 0; +} + +/* @TODO 600px break should be a token */ + +/* wrap actions into a stacked layout when the alert is <=600px */ +@container cpd-alert (max-width: 600px) { + .cpd-alert-content { + flex-wrap: wrap; + } + + .cpd-alert-text-content { + flex: 1 0 100%; + } +} diff --git a/frontend/src/styles/cpd-form.css b/frontend/src/styles/cpd-form.css index 07cd073f1..be69e4dd2 100644 --- a/frontend/src/styles/cpd-form.css +++ b/frontend/src/styles/cpd-form.css @@ -28,7 +28,7 @@ .cpd-form-inline-field { display: flex; flex-direction: row; - gap: var(--cpd-space-5x); + gap: var(--cpd-space-3x); } .cpd-form-inline-field-body { diff --git a/templates/pages/device_consent.html b/templates/pages/device_consent.html index 28113b235..e546684d8 100644 --- a/templates/pages/device_consent.html +++ b/templates/pages/device_consent.html @@ -17,6 +17,14 @@ Please see LICENSE files in the repository root for full details. {% set user_agent = grant.user_agent | parse_user_agent() %} {% if grant.state == "pending" %} + {% set initial -%} + {%- if matrix_user.display_name -%} + {{- matrix_user.display_name[0] | upper -}} + {%- else -%} + {{- matrix_user.mxid[1] | upper -}} + {%- endif -%} + {%- endset %} +
{% if client.logo_uri %} @@ -30,72 +38,96 @@ Please see LICENSE files in the repository root for full details.

- {{ _('mas.consent.continue_to', client_name=client_display_name) }} + {{ _('mas.device_consent.title') }}

+
-
-
-
- {% if user_agent.device_type == "mobile" %} - {{ icon.mobile() }} - {% elif user_agent.device_type == "tablet" %} - {{ icon.web_browser() }} - {% elif user_agent.device_type == "pc" %} - {{ icon.computer() }} - {% else %} - {{ icon.unknown_solid() }} - {% endif %} -
+
+
{{ initial }}
+
+
{{ matrix_user.display_name or current_session.user.username }}
+
{{ matrix_user.mxid }}
+
+
-
- {% if user_agent.model %} -
{{ user_agent.model }}
- {% endif %} - - {% if user_agent.os %} -
- {{ user_agent.os }} - {% if user_agent.os_version %} - {{ user_agent.os_version }} - {% endif %} -
- {% endif %} - - {# If we haven't detected a model, it's probably a browser, so show the name #} - {% if not user_agent.model and user_agent.name %} -
- {{ user_agent.name }} - {% if user_agent.version %} - {{ user_agent.version }} - {% endif %} -
- {% endif %} - - {# If we couldn't detect anything, show a generic "Device" #} - {% if not user_agent.model and not user_agent.name and not user_agent.os %} -
{{ _("mas.device_card.generic_device") }}
- {% endif %} -
+
+
+ {{ icon.error_solid() }} +
+
+
+

+ {{ _("mas.device_consent.warning.title") }} +

+

+ {{ _("mas.device_consent.warning.description") }} +

- +
+ +
+
+
+ {% if user_agent.device_type == "mobile" %} + {{ icon.mobile() }} + {% elif user_agent.device_type == "tablet" %} + {{ icon.web_browser() }} + {% elif user_agent.device_type == "pc" %} + {{ icon.computer() }} + {% else %} + {{ icon.unknown_solid() }} + {% endif %} +
+ +
+ {% if user_agent.model %} +
{{ user_agent.model }}
+ {% endif %} + + {% if user_agent.os %}
-
{{ _("mas.device_card.ip_address") }}
-
{{ grant.ip_address }}
+ {{ user_agent.os }} + {% if user_agent.os_version %} + {{ user_agent.os_version }} + {% endif %}
{% endif %} + + {# If we haven't detected a model, it's probably a browser, so show the name #} + {% if not user_agent.model and user_agent.name %} +
+ {{ user_agent.name }} + {% if user_agent.version %} + {{ user_agent.version }} + {% endif %} +
+ {% endif %} + + {# If we couldn't detect anything, show a generic "Device" #} + {% if not user_agent.model and not user_agent.name and not user_agent.os %} +
{{ _("mas.device_card.generic_device") }}
+ {% endif %} +
+
+
@@ -114,27 +146,26 @@ Please see LICENSE files in the repository root for full details. {% endif %} {% endcall %} - {% set initial -%} - {%- if matrix_user.display_name -%} - {{- matrix_user.display_name[0] | upper -}} - {%- else -%} - {{- matrix_user.mxid[1] | upper -}} - {%- endif -%} - {%- endset %} - -
-
{{ initial }}
-
-
{{ matrix_user.display_name or current_session.user.username }}
-
{{ matrix_user.mxid }}
-
-
-
-
+ +
+
+
+ +
+ {{ icon.check() }} +
+
+
+
+ +
+
diff --git a/templates/pages/device_link.html b/templates/pages/device_link.html index 851b1bf96..aaf018b1c 100644 --- a/templates/pages/device_link.html +++ b/templates/pages/device_link.html @@ -11,7 +11,7 @@ Please see LICENSE files in the repository root for full details. {% block content %}
- {{ icon.link() }} + {{ icon.mobile() }}
@@ -20,23 +20,28 @@ Please see LICENSE files in the repository root for full details.
-
- {% call(f) field.field(label="Device code", name="code", class="mb-4 self-center", form_state=form_state) %} -
- +
+ + + {% call(f) field.field(label=_("mas.device_code_link.verification_code"), name="code", class="mb-4 self-center", form_state=form_state) %} +
+ - {% for _ in range(6) %} - - {% endfor %} -
- {% endcall %} + {% for _ in range(6) %} + + {% endfor %} +
+ {% endcall %} - {{ button.button(text=_("action.continue")) }} -
+ {{ button.button(text=_("action.continue")) }} + + + {{ button.link_tertiary(text=_("action.cancel"), href="/") }} +
{% endblock content %} diff --git a/translations/en.json b/translations/en.json index 99ba7386e..a24e1a74c 100644 --- a/translations/en.json +++ b/translations/en.json @@ -6,11 +6,11 @@ }, "cancel": "Cancel", "@cancel": { - "context": "pages/consent.html:81:11-29, pages/device_consent.html:150:13-31, pages/policy_violation.html:70:15-33, pages/reauth.html:37:13-31" + "context": "pages/consent.html:81:11-29, pages/device_consent.html:181:13-31, pages/device_link.html:45:33-51, pages/policy_violation.html:70:15-33, pages/reauth.html:37:13-31" }, "continue": "Continue", "@continue": { - "context": "form_post.html:25:28-48, pages/consent.html:71:28-48, pages/device_consent.html:137: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:77:26-46, pages/register/steps/display_name.html:43:28-48, pages/register/steps/registration_token.html:41:28-48, pages/register/steps/verify_email.html:51:26-46, pages/sso.html:52:28-48" + "context": "form_post.html:25:28-48, pages/consent.html:71:28-48, pages/device_link.html:42:28-48, pages/login.html:68:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:77:26-46, pages/register/steps/display_name.html:43:28-48, pages/register/steps/registration_token.html:41:28-48, pages/register/steps/verify_email.html:51:26-46, pages/sso.html:52:28-48" }, "create_account": "Create Account", "@create_account": { @@ -189,11 +189,11 @@ "consent": { "continue_to": "Continue to %(client_name)s?", "@continue_to": { - "context": "pages/consent.html:31:11-72, pages/device_consent.html:33:13-74, pages/sso.html:25:11-64" + "context": "pages/consent.html:31:11-72, pages/sso.html:25:11-64" }, "scope_list_preface": "By continuing, you allow %(client_name)s to:", "@scope_list_preface": { - "context": "pages/consent.html:43:13-81, pages/device_consent.html:108:15-83" + "context": "pages/consent.html:43:13-81, pages/device_consent.html:140:15-83" }, "this_will_setup": "This will set up %(client_name)s (%(client_uri)s) with your %(server_name)s account.", "@this_will_setup": { @@ -201,61 +201,95 @@ }, "use_another_account": "Use another account", "@use_another_account": { - "context": "pages/consent.html:76:11-47, pages/device_consent.html:143:13-49, pages/sso.html:57:11-47" + "context": "pages/consent.html:76:11-47, pages/device_consent.html:174:13-49, pages/sso.html:57:11-47" } }, "device_card": { "access_requested": "Access requested", "@access_requested": { - "context": "pages/device_consent.html:92:34-71" - }, - "device_code": "Code", - "@device_code": { - "context": "pages/device_consent.html:96:34-66" + "context": "pages/device_consent.html:125:32-69" }, "generic_device": "Device", "@generic_device": { - "context": "pages/device_consent.html:80:22-57" + "context": "pages/device_consent.html:113:20-55" }, "ip_address": "IP address", "@ip_address": { - "context": "pages/device_consent.html:87:36-67" + "context": "pages/device_consent.html:120:34-65" + }, + "security_code": "Security code", + "@security_code": { + "context": "pages/device_consent.html:129:32-66", + "description": "On the device consent page, label for the six-character user code shown next to the requesting device's IP address and access time." } }, "device_code_link": { - "description": "Link a device", + "description": "Enter the security code shown on your other device", "@description": { "context": "pages/device_link.html:19:25-62" }, - "headline": "Enter the code displayed on your device", + "headline": "Link a new device to your account", "@headline": { "context": "pages/device_link.html:18:27-61" + }, + "verification_code": "Verification code", + "@verification_code": { + "context": "pages/device_link.html:26:35-78", + "description": "On the device link page, label of the text field where the user enters the verification code shown on the other device." } }, "device_consent": { + "confirm_device": "Yes, I confirm this is my device and I want to sign it in.", + "@confirm_device": { + "context": "pages/device_consent.html:163:17-55", + "description": "On the device consent page, label of the required checkbox the user must tick to confirm the device is theirs before granting access." + }, "denied": { "description": "You denied access to %(client_name)s. You can close this window.", "@description": { - "context": "pages/device_consent.html:162:27-102" + "context": "pages/device_consent.html:193:27-102" }, "heading": "Access denied", "@heading": { - "context": "pages/device_consent.html:161:29-67" + "context": "pages/device_consent.html:192:29-67" } }, + "description": "Another device wants to link %(client_name)s (%(client_uri)s) with your %(server_name)s account. Make sure you recognise this device.", + "@description": { + "context": "pages/device_consent.html:45:13-146", + "description": "On the device consent page, the body text below the title describing which client is asking to be linked with the user's homeserver account." + }, + "grant_access": "Grant access", + "@grant_access": { + "context": "pages/device_consent.html:168:13-49", + "description": "On the device consent page, label of the primary submit button which grants the requesting device access." + }, "granted": { "description": "You granted access to %(client_name)s. You can close this window.", "@description": { - "context": "pages/device_consent.html:173:27-103" + "context": "pages/device_consent.html:204:27-103" }, "heading": "Access granted", "@heading": { - "context": "pages/device_consent.html:172:29-68" + "context": "pages/device_consent.html:203:29-68" } }, - "this_will_setup": "Another device wants to set up %(client_name)s (%(client_uri)s) with your %(server_name)s account. Make sure you recognise that device.", - "@this_will_setup": { - "context": "pages/device_consent.html:37:13-150" + "title": "Give access to your account?", + "@title": { + "context": "pages/device_consent.html:41:13-42", + "description": "On the device consent page, the main heading asking the user whether to authorise the requesting device." + }, + "warning": { + "description": "Are you sure this is your device? Administrators or IT support would never ask you to accept this.", + "@description": { + "context": "pages/device_consent.html:67:17-60", + "description": "On the device consent page, the body of the critical alert reminding the user that administrators or IT support would never ask them to accept this." + }, + "title": "You are about to grant a remote device access to your account", + "@title": { + "context": "pages/device_consent.html:64:17-54", + "description": "On the device consent page, the title of the critical alert warning that a remote device is about to be granted access to the user's account." + } } }, "device_display_name": {