Disable the device code auto-filling by default and match the device authorization grant with the designs (#5704)

This commit is contained in:
Quentin Gliech
2026-05-29 15:56:16 +02:00
committed by GitHub
18 changed files with 464 additions and 130 deletions
+2
View File
@@ -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,
})
}
+23
View File
@@ -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)
}
}
+5
View File
@@ -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,
}
+1 -1
View File
@@ -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(),
+51 -2
View File
@@ -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")]
@@ -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<String>,
}
#[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?
+83 -12
View File
@@ -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<Templates>,
State(url_builder): State<UrlBuilder>,
State(site_config): State<SiteConfig>,
cookie_jar: CookieJar,
Query(query): Query<Params>,
) -> Result<impl IntoResponse, InternalError> {
Query(mut query): Query<Params>,
) -> Result<Response, InternalError> {
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<Templates>,
State(url_builder): State<UrlBuilder>,
State(site_config): State<SiteConfig>,
cookie_jar: CookieJar,
Form(form): Form<ProtectedForm<Params>>,
) -> Result<Response, InternalError> {
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<Response, InternalError> {
let mut form_state = FormState::from_form(&params);
// If we have a code, find it in the database
if let Some(code) = &params.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)?;
+1
View File
@@ -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,
}
}
+1 -1
View File
@@ -453,7 +453,7 @@ register_templates! {
pub fn render_upstream_oauth2_do_register(WithLanguage<WithCsrf<UpstreamRegister>>) { "pages/upstream_oauth2/do_register.html" }
/// Render the device code link page
pub fn render_device_link(WithLanguage<DeviceLinkContext>) { "pages/device_link.html" }
pub fn render_device_link(WithLanguage<WithCsrf<DeviceLinkContext>>) { "pages/device_link.html" }
/// Render the device code consent page
pub fn render_device_consent(WithLanguage<WithCsrf<WithSession<DeviceConsentContext>>>) { "pages/device_consent.html" }
+4
View File
@@ -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"
}
}
},
+10
View File
@@ -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`
@@ -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;
}
+1
View File
@@ -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";
+85
View File
@@ -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%;
}
}
+1 -1
View File
@@ -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 {
+103 -72
View File
@@ -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 %}
<header class="page-heading">
<div class="flex justify-center">
{% if client.logo_uri %}
@@ -30,72 +38,96 @@ Please see LICENSE files in the repository root for full details.
<div class="header">
<h1 class="title">
{{ _('mas.consent.continue_to', client_name=client_display_name) }}
{{ _('mas.device_consent.title') }}
</h1>
<p class="text [&>span]:whitespace-nowrap [&>span]:text-[var(--cpd-color-text-link-external)]">
{{ _("mas.device_consent.this_will_setup", client_name=client_display_name, client_uri=client_display_uri, server_name=branding.server_name) }}
{{ _("mas.device_consent.description", client_name=client_display_name, client_uri=client_display_uri, server_name=branding.server_name) }}
</p>
</div>
<div class="session-card mt-4">
<div class="card-header" {%- if user_agent %} title="{{ user_agent.raw }}"{% endif %}>
<div class="device-type-icon">
{% 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 %}
</div>
<section class="flex items-center p-4 gap-4 border border-[var(--cpd-color-gray-400)] rounded-xl">
<div class="avatar-placeholder" data-color="{{ matrix_user.mxid | id_color_hash }}">{{ initial }}</div>
<div class="flex flex-col">
<div class="text-primary cpd-text-body-lg-semibold">{{ matrix_user.display_name or current_session.user.username }}</div>
<div class="text-secondary cpd-text-body-md-regular">{{ matrix_user.mxid }}</div>
</div>
</section>
<div class="content auto">
{% if user_agent.model %}
<div>{{ user_agent.model }}</div>
{% endif %}
{% if user_agent.os %}
<div>
{{ user_agent.os }}
{% if user_agent.os_version %}
{{ user_agent.os_version }}
{% endif %}
</div>
{% 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 %}
<div>
{{ user_agent.name }}
{% if user_agent.version %}
{{ user_agent.version }}
{% endif %}
</div>
{% 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 %}
<div>{{ _("mas.device_card.generic_device") }}</div>
{% endif %}
</div>
<section class="cpd-alert" data-type="critical">
<div class="cpd-alert-icon">
{{ icon.error_solid() }}
</div>
<div class="cpd-alert-content">
<div class="cpd-alert-text-content">
<p class="cpd-alert-title cpd-text-body-md-semibold">
{{ _("mas.device_consent.warning.title") }}
</p>
<p class="cpd-text-body-sm-regular">
{{ _("mas.device_consent.warning.description") }}
</p>
</div>
<div class="metadata">
{% if grant.ip_address %}
</div>
</section>
<div class="session-card">
<div class="card-header" {%- if user_agent %} title="{{ user_agent.raw }}"{% endif %}>
<div class="device-type-icon">
{% 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 %}
</div>
<div class="content auto">
{% if user_agent.model %}
<div>{{ user_agent.model }}</div>
{% endif %}
{% if user_agent.os %}
<div>
<div class="key">{{ _("mas.device_card.ip_address") }}</div>
<div class="value">{{ grant.ip_address }}</div>
{{ user_agent.os }}
{% if user_agent.os_version %}
{{ user_agent.os_version }}
{% endif %}
</div>
{% 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 %}
<div>
{{ user_agent.name }}
{% if user_agent.version %}
{{ user_agent.version }}
{% endif %}
</div>
{% 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 %}
<div>{{ _("mas.device_card.generic_device") }}</div>
{% endif %}
</div>
</div>
<div class="metadata">
{% if grant.ip_address %}
<div>
<div class="key">{{ _("mas.device_card.access_requested") }}</div>
<div class="value">{{ _.relative_date(grant.created_at) | title }} {{ _.short_time(grant.created_at) }}</div>
</div>
<div>
<div class="key">{{ _("mas.device_card.device_code") }}</div>
<div class="value">{{ grant.user_code }}</div>
<div class="key">{{ _("mas.device_card.ip_address") }}</div>
<div class="value">{{ grant.ip_address }}</div>
</div>
{% endif %}
<div>
<div class="key">{{ _("mas.device_card.access_requested") }}</div>
<div class="value">{{ _.relative_date(grant.created_at) | title }} {{ _.short_time(grant.created_at) }}</div>
</div>
<div>
<div class="key">{{ _("mas.device_card.security_code") }}</div>
<div class="value">{{ grant.user_code }}</div>
</div>
</div>
</div>
@@ -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 %}
<section class="flex items-center p-4 gap-4 border border-[var(--cpd-color-gray-400)] rounded-xl">
<div class="avatar-placeholder" data-color="{{ matrix_user.mxid | id_color_hash }}">{{ initial }}</div>
<div class="flex flex-col">
<div class="text-primary cpd-text-body-lg-semibold">{{ matrix_user.display_name or current_session.user.username }}</div>
<div class="text-secondary cpd-text-body-md-regular">{{ matrix_user.mxid }}</div>
</div>
</section>
<section class="flex flex-col gap-6">
<form method="POST" class="cpd-form-root">
<form method="POST" class="flex flex-col gap-8">
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
<div class="cpd-form-inline-field">
<div class="cpd-form-inline-field-control">
<div class="cpd-checkbox-container">
<input class="cpd-checkbox-input" type="checkbox" name="confirm_device" id="confirm_device" required />
<div class="cpd-checkbox-ui">
{{ icon.check() }}
</div>
</div>
</div>
<div class="cpd-form-inline-field-body">
<label class="cpd-form-label" for="confirm_device">
{{ _("mas.device_consent.confirm_device") }}
</label>
</div>
</div>
<button type="submit" name="action" value="consent" class="cpd-button" data-kind="primary" data-size="lg">
{{ _("action.continue") }}
{{ _("mas.device_consent.grant_access") }}
</button>
</form>
+23 -18
View File
@@ -11,7 +11,7 @@ Please see LICENSE files in the repository root for full details.
{% block content %}
<header class="page-heading">
<div class="icon">
{{ icon.link() }}
{{ icon.mobile() }}
</div>
<div class="header">
@@ -20,23 +20,28 @@ Please see LICENSE files in the repository root for full details.
</div>
</header>
<form method="GET" class="cpd-form-root">
{% call(f) field.field(label="Device code", name="code", class="mb-4 self-center", form_state=form_state) %}
<div class="cpd-mfa-container">
<input {{ field.attributes(f) }}
id="mfa-code-input"
type="text"
minlength="0"
maxlength="6"
class="cpd-mfa-control uppercase"
required>
<section class="flex flex-col gap-5">
<form method="POST" class="cpd-form-root">
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{% call(f) field.field(label=_("mas.device_code_link.verification_code"), name="code", class="mb-4 self-center", form_state=form_state) %}
<div class="cpd-mfa-container">
<input {{ field.attributes(f) }}
id="mfa-code-input"
type="text"
minlength="0"
maxlength="6"
class="cpd-mfa-control uppercase"
required>
{% for _ in range(6) %}
<div class="cpd-mfa-digit" aria-hidden="true"></div>
{% endfor %}
</div>
{% endcall %}
{% for _ in range(6) %}
<div class="cpd-mfa-digit" aria-hidden="true"></div>
{% endfor %}
</div>
{% endcall %}
{{ button.button(text=_("action.continue")) }}
</form>
{{ button.button(text=_("action.continue")) }}
</form>
{{ button.link_tertiary(text=_("action.cancel"), href="/") }}
</section>
{% endblock content %}
+55 -21
View File
@@ -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 <span>%(client_name)s</span>?",
"@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 <span>%(client_name)s</span> 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 (<span>%(client_uri)s</span>) with your <span>%(server_name)s</span> 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": "<strong>Another device</strong> wants to link %(client_name)s (<span>%(client_uri)s</span>) with your <span>%(server_name)s</span> 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 (<span>%(client_uri)s</span>) with your <span>%(server_name)s</span> 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": {