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 {