1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-08-09 04:22:45 +03:00

Setup the device link form page

This commit is contained in:
Quentin Gliech
2023-12-06 18:57:38 +01:00
parent ae05cbc1f1
commit 4301fd9378
8 changed files with 257 additions and 10 deletions

View File

@@ -404,6 +404,10 @@ where
mas_router::UpstreamOAuth2Link::route(),
get(self::upstream_oauth2::link::get).post(self::upstream_oauth2::link::post),
)
.route(
mas_router::DeviceCodeLink::route(),
get(self::oauth2::device::link::get).post(self::oauth2::device::link::post),
)
.layer(AndThenLayer::new(
move |response: axum::response::Response| async move {
if response.status().is_server_error() {

View File

@@ -0,0 +1,102 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use axum::{
extract::{Query, State},
response::IntoResponse,
Form,
};
use axum_extra::response::Html;
use mas_axum_utils::{
cookies::CookieJar,
csrf::{CsrfExt, ProtectedForm},
FancyError,
};
use mas_storage::{BoxClock, BoxRng};
use mas_templates::{
DeviceLinkContext, DeviceLinkFormField, FieldError, FormState, TemplateContext, Templates,
};
use serde::{Deserialize, Serialize};
use crate::PreferredLanguage;
// We use this struct for both the form and the query parameters. This is useful
// to build a form state from the query parameters. The query parameter is only
// really used when the `verification_uri_complete` feature of the device code
// grant is used.
#[derive(Serialize, Deserialize)]
pub struct Params {
code: String,
}
#[tracing::instrument(name = "handlers.oauth2.device.link.get", skip_all, err)]
pub(crate) async fn get(
mut rng: BoxRng,
clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>,
cookie_jar: CookieJar,
query: Option<Query<Params>>,
) -> Result<impl IntoResponse, FancyError> {
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let mut form_state = FormState::default();
// XXX: right now we just get the code from the query to pre-fill the form. We
// may want to make the form readonly instead at some point? tbd
if let Some(Query(params)) = query {
// Validate that it's a full code
if params.code.len() == 6 && params.code.chars().all(|c| c.is_ascii_alphanumeric()) {
form_state = FormState::from_form(&params);
}
};
let ctx = DeviceLinkContext::new()
.with_form_state(form_state)
.with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_device_link(&ctx)?;
Ok((cookie_jar, Html(content)))
}
#[tracing::instrument(name = "handlers.oauth2.device.link.post", skip_all, err)]
pub(crate) async fn post(
mut rng: BoxRng,
clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>,
cookie_jar: CookieJar,
Form(form): Form<ProtectedForm<Params>>,
) -> Result<impl IntoResponse, FancyError> {
let form = cookie_jar.verify_form(&clock, form)?;
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let form_state = FormState::from_form(&form)
.with_error_on_field(DeviceLinkFormField::Code, FieldError::Required);
// TODO: find the device code grant in the database
// and redirect to /oauth2/device/link/:id
// That then will trigger a login if we don't have a session
let ctx = DeviceLinkContext::new()
.with_form_state(form_state)
.with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_device_link(&ctx)?;
Ok((cookie_jar, Html(content)))
}

View File

@@ -0,0 +1,15 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub mod link;

View File

@@ -32,6 +32,7 @@ use thiserror::Error;
pub mod authorization;
pub mod consent;
pub mod device;
pub mod discovery;
pub mod introspection;
pub mod keys;

View File

@@ -689,6 +689,23 @@ impl Route for UpstreamOAuth2Link {
}
}
/// `GET|POST /link`
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
pub struct DeviceCodeLink {
code: Option<String>,
}
impl Route for DeviceCodeLink {
type Query = DeviceCodeLink;
fn route() -> &'static str {
"/link"
}
fn query(&self) -> Option<&Self::Query> {
Some(self)
}
}
/// `GET /assets`
pub struct StaticAsset {
path: String,

View File

@@ -1,4 +1,4 @@
// Copyright 2021 The Matrix.org Foundation C.I.C.
// Copyright 2021-2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -104,7 +104,7 @@ impl TemplateContext for () {
}
/// Context with a specified locale in it
#[derive(Serialize)]
#[derive(Serialize, Debug)]
pub struct WithLanguage<T> {
lang: String,
@@ -143,7 +143,7 @@ impl<T: TemplateContext> TemplateContext for WithLanguage<T> {
}
/// Context with a CSRF token in it
#[derive(Serialize)]
#[derive(Serialize, Debug)]
pub struct WithCsrf<T> {
csrf_token: String,
@@ -1023,6 +1023,58 @@ impl TemplateContext for UpstreamRegister {
}
}
/// Form fields on the device link page
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DeviceLinkFormField {
/// The device code field
Code,
}
impl FormField for DeviceLinkFormField {
fn keep(&self) -> bool {
match self {
Self::Code => true,
}
}
}
/// Context used by the `device_link.html` template
#[derive(Serialize, Default, Debug)]
pub struct DeviceLinkContext {
form_state: FormState<DeviceLinkFormField>,
}
impl DeviceLinkContext {
/// Constructs a new context with an existing linked user
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Set the form state
#[must_use]
pub fn with_form_state(mut self, form_state: FormState<DeviceLinkFormField>) -> Self {
self.form_state = form_state;
self
}
}
impl TemplateContext for DeviceLinkContext {
fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
where
Self: Sized,
{
vec![
Self::new(),
Self::new().with_form_state(
FormState::default()
.with_error_on_field(DeviceLinkFormField::Code, FieldError::Required),
),
]
}
}
/// Context used by the `form_post.html` template
#[derive(Serialize)]
pub struct FormPostContext<T> {

View File

@@ -42,13 +42,13 @@ mod macros;
pub use self::{
context::{
AppContext, CompatSsoContext, ConsentContext, EmailAddContext, EmailVerificationContext,
EmailVerificationPageContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
LoginContext, LoginFormField, NotFoundContext, PolicyViolationContext, PostAuthContext,
PostAuthContextInner, ReauthContext, ReauthFormField, RegisterContext, RegisterFormField,
SiteBranding, TemplateContext, UpstreamExistingLinkContext, UpstreamRegister,
UpstreamRegisterFormField, UpstreamSuggestLink, WithCsrf, WithLanguage,
WithOptionalSession, WithSession,
AppContext, CompatSsoContext, ConsentContext, DeviceLinkContext, DeviceLinkFormField,
EmailAddContext, EmailVerificationContext, EmailVerificationPageContext, EmptyContext,
ErrorContext, FormPostContext, IndexContext, LoginContext, LoginFormField, NotFoundContext,
PolicyViolationContext, PostAuthContext, PostAuthContextInner, ReauthContext,
ReauthFormField, RegisterContext, RegisterFormField, SiteBranding, TemplateContext,
UpstreamExistingLinkContext, UpstreamRegister, UpstreamRegisterFormField,
UpstreamSuggestLink, WithCsrf, WithLanguage, WithOptionalSession, WithSession,
},
forms::{FieldError, FormError, FormField, FormState, ToFormState},
};
@@ -365,6 +365,9 @@ register_templates! {
/// Render the upstream register screen
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<WithCsrf<DeviceLinkContext>>) { "pages/device_link.html" }
}
impl Templates {

View File

@@ -0,0 +1,53 @@
{#
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
#}
{% extends "base.html" %}
{% block content %}
<header class="page-heading">
<div class="icon">
{{ icon.link() }}
</div>
<div class="header">
<h1 class="title">Link a device</h1>
<p class="text">Enter the code displayed on your device</p>
</div>
</header>
<form method="POST" class="cpd-form-root">
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{% 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>
{% for _ in range(6) %}
<div class="cpd-mfa-digit" aria-hidden="true"></div>
{% endfor %}
</div>
{% endcall %}
{{ button.button(text=_("action.continue")) }}
</form>
{% endblock content %}