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

Skip the device code form when using the full verification URI

This changes the form to use a GET method, as it is only really doing
a redirect.
This commit is contained in:
Quentin Gliech
2024-05-07 10:24:23 +02:00
parent 2e84e226c7
commit 353815bc6f
5 changed files with 40 additions and 78 deletions

View File

@@ -412,7 +412,7 @@ where
)
.route(
mas_router::DeviceCodeLink::route(),
get(self::oauth2::device::link::get).post(self::oauth2::device::link::post),
get(self::oauth2::device::link::get),
)
.route(
mas_router::DeviceCodeConsent::route(),

View File

@@ -1,4 +1,4 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
// Copyright 2023, 2024 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.
@@ -14,17 +14,12 @@
use axum::{
extract::{Query, State},
response::{IntoResponse, Response},
Form,
response::IntoResponse,
};
use axum_extra::response::Html;
use mas_axum_utils::{
cookies::CookieJar,
csrf::{CsrfExt, ProtectedForm},
FancyError,
};
use mas_axum_utils::{cookies::CookieJar, FancyError};
use mas_router::UrlBuilder;
use mas_storage::{BoxClock, BoxRepository, BoxRng};
use mas_storage::{BoxClock, BoxRepository};
use mas_templates::{
DeviceLinkContext, DeviceLinkFormField, FieldError, FormState, TemplateContext, Templates,
};
@@ -32,10 +27,6 @@ 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,
@@ -43,76 +34,49 @@ pub struct Params {
#[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,
mut repo: BoxRepository,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
cookie_jar: CookieJar,
Form(form): Form<ProtectedForm<Params>>,
) -> Result<Response, FancyError> {
let form = cookie_jar.verify_form(&clock, form)?;
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
query: Option<Query<Params>>,
) -> Result<impl IntoResponse, FancyError> {
let mut form_state = FormState::default();
let code = form.code.to_uppercase();
let grant = repo
.oauth2_device_code_grant()
.find_by_user_code(&code)
.await?
// XXX: We should have different error messages for already exchanged and expired
.filter(|grant| grant.is_pending())
.filter(|grant| grant.expires_at > clock.now());
// If we have a code in query, find it in the database
if let Some(Query(params)) = query {
// Save the form state so that we echo back the code
form_state = FormState::from_form(&params);
let Some(grant) = grant else {
let form_state = FormState::from_form(&form)
.with_error_on_field(DeviceLinkFormField::Code, FieldError::Invalid);
// Find the code in the database
let code = params.code.to_uppercase();
let grant = repo
.oauth2_device_code_grant()
.find_by_user_code(&code)
.await?
// XXX: We should have different error messages for already exchanged and expired
.filter(|grant| grant.is_pending())
.filter(|grant| grant.expires_at > clock.now());
let ctx = DeviceLinkContext::new()
.with_form_state(form_state)
.with_csrf(csrf_token.form_value())
.with_language(locale);
if let Some(grant) = grant {
// This is a valid code, redirect to the consent page
// This will in turn redirect to the login page if the user is not logged in
let destination = url_builder.redirect(&mas_router::DeviceCodeConsent::new(grant.id));
let content = templates.render_device_link(&ctx)?;
return Ok((cookie_jar, destination).into_response());
}
return Ok((cookie_jar, Html(content)).into_response());
// The code isn't valid, set an error on the form
form_state = form_state.with_error_on_field(DeviceLinkFormField::Code, FieldError::Invalid);
};
// Redirect to the consent page
// This will in turn redirect to the login page if the user is not logged in
let destination = url_builder.redirect(&mas_router::DeviceCodeConsent::new(grant.id));
// Rendre the form
let ctx = DeviceLinkContext::new()
.with_form_state(form_state)
.with_language(locale);
Ok((cookie_jar, destination).into_response())
let content = templates.render_device_link(&ctx)?;
Ok((cookie_jar, Html(content)).into_response())
}

View File

@@ -377,7 +377,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<WithCsrf<DeviceLinkContext>>) { "pages/device_link.html" }
pub fn render_device_link(WithLanguage<DeviceLinkContext>) { "pages/device_link.html" }
/// Render the device code consent page
pub fn render_device_consent(WithLanguage<WithCsrf<WithSession<DeviceConsentContext>>>) { "pages/device_consent.html" }