You've already forked authentication-service
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:
@@ -404,6 +404,10 @@ where
|
|||||||
mas_router::UpstreamOAuth2Link::route(),
|
mas_router::UpstreamOAuth2Link::route(),
|
||||||
get(self::upstream_oauth2::link::get).post(self::upstream_oauth2::link::post),
|
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(
|
.layer(AndThenLayer::new(
|
||||||
move |response: axum::response::Response| async move {
|
move |response: axum::response::Response| async move {
|
||||||
if response.status().is_server_error() {
|
if response.status().is_server_error() {
|
||||||
|
102
crates/handlers/src/oauth2/device/link.rs
Normal file
102
crates/handlers/src/oauth2/device/link.rs
Normal 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(¶ms);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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)))
|
||||||
|
}
|
15
crates/handlers/src/oauth2/device/mod.rs
Normal file
15
crates/handlers/src/oauth2/device/mod.rs
Normal 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;
|
@@ -32,6 +32,7 @@ use thiserror::Error;
|
|||||||
|
|
||||||
pub mod authorization;
|
pub mod authorization;
|
||||||
pub mod consent;
|
pub mod consent;
|
||||||
|
pub mod device;
|
||||||
pub mod discovery;
|
pub mod discovery;
|
||||||
pub mod introspection;
|
pub mod introspection;
|
||||||
pub mod keys;
|
pub mod keys;
|
||||||
|
@@ -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`
|
/// `GET /assets`
|
||||||
pub struct StaticAsset {
|
pub struct StaticAsset {
|
||||||
path: String,
|
path: String,
|
||||||
|
@@ -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");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with 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
|
/// Context with a specified locale in it
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, Debug)]
|
||||||
pub struct WithLanguage<T> {
|
pub struct WithLanguage<T> {
|
||||||
lang: String,
|
lang: String,
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ impl<T: TemplateContext> TemplateContext for WithLanguage<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Context with a CSRF token in it
|
/// Context with a CSRF token in it
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, Debug)]
|
||||||
pub struct WithCsrf<T> {
|
pub struct WithCsrf<T> {
|
||||||
csrf_token: String,
|
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
|
/// Context used by the `form_post.html` template
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct FormPostContext<T> {
|
pub struct FormPostContext<T> {
|
||||||
|
@@ -42,13 +42,13 @@ mod macros;
|
|||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
context::{
|
context::{
|
||||||
AppContext, CompatSsoContext, ConsentContext, EmailAddContext, EmailVerificationContext,
|
AppContext, CompatSsoContext, ConsentContext, DeviceLinkContext, DeviceLinkFormField,
|
||||||
EmailVerificationPageContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
|
EmailAddContext, EmailVerificationContext, EmailVerificationPageContext, EmptyContext,
|
||||||
LoginContext, LoginFormField, NotFoundContext, PolicyViolationContext, PostAuthContext,
|
ErrorContext, FormPostContext, IndexContext, LoginContext, LoginFormField, NotFoundContext,
|
||||||
PostAuthContextInner, ReauthContext, ReauthFormField, RegisterContext, RegisterFormField,
|
PolicyViolationContext, PostAuthContext, PostAuthContextInner, ReauthContext,
|
||||||
SiteBranding, TemplateContext, UpstreamExistingLinkContext, UpstreamRegister,
|
ReauthFormField, RegisterContext, RegisterFormField, SiteBranding, TemplateContext,
|
||||||
UpstreamRegisterFormField, UpstreamSuggestLink, WithCsrf, WithLanguage,
|
UpstreamExistingLinkContext, UpstreamRegister, UpstreamRegisterFormField,
|
||||||
WithOptionalSession, WithSession,
|
UpstreamSuggestLink, WithCsrf, WithLanguage, WithOptionalSession, WithSession,
|
||||||
},
|
},
|
||||||
forms::{FieldError, FormError, FormField, FormState, ToFormState},
|
forms::{FieldError, FormError, FormField, FormState, ToFormState},
|
||||||
};
|
};
|
||||||
@@ -365,6 +365,9 @@ register_templates! {
|
|||||||
|
|
||||||
/// Render the upstream register screen
|
/// Render the upstream register screen
|
||||||
pub fn render_upstream_oauth2_do_register(WithLanguage<WithCsrf<UpstreamRegister>>) { "pages/upstream_oauth2/do_register.html" }
|
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 {
|
impl Templates {
|
||||||
|
53
templates/pages/device_link.html
Normal file
53
templates/pages/device_link.html
Normal 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 %}
|
||||||
|
|
Reference in New Issue
Block a user