mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-09-27 11:30:20 +00:00
Disable the device code auto-filling by default and match the device authorization grant with the designs (#5704)
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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(¶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)?;
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user