1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-29 22:01:14 +03:00

Axum migration: CSRF token and login page

This commit is contained in:
Quentin Gliech
2022-03-25 15:38:30 +01:00
parent 5d3b4aa182
commit 5e95c705d4
5 changed files with 130 additions and 98 deletions

View File

@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize};
use serde_with::{serde_as, TimestampSeconds};
use thiserror::Error;
use crate::{CookieExt, PrivateCookieJar};
use crate::{cookies::CookieDecodeError, CookieExt, PrivateCookieJar};
/// Failed to validate CSRF token
#[derive(Debug, Error)]
@ -28,6 +28,14 @@ pub enum CsrfError {
#[error("CSRF token mismatch")]
Mismatch,
/// The token in the form did not match the token in the cookie
#[error("Missing CSRF cookie")]
Missing,
/// Failed to decode the token
#[error("could not decode CSRF cookie")]
DecodeCookie(#[from] CookieDecodeError),
/// The token expired
#[error("CSRF token expired")]
Expired,
@ -89,8 +97,18 @@ impl CsrfToken {
}
}
// A CSRF-protected form
#[derive(Deserialize)]
pub struct ProtectedForm<T> {
csrf: String,
#[serde(flatten)]
inner: T,
}
pub trait CsrfExt {
fn csrf_token(self) -> (CsrfToken, Self);
fn verify_form<T>(&self, form: ProtectedForm<T>) -> Result<T, CsrfError>;
}
impl<K> CsrfExt for PrivateCookieJar<K> {
@ -108,4 +126,12 @@ impl<K> CsrfExt for PrivateCookieJar<K> {
let jar = jar.add(cookie);
(new_token, jar)
}
fn verify_form<T>(&self, form: ProtectedForm<T>) -> Result<T, CsrfError> {
let cookie = self.get("csrf").ok_or(CsrfError::Missing)?;
let token: CsrfToken = cookie.decode()?;
let token = token.verify_expiration()?;
token.verify_form_value(&form.csrf)?;
Ok(form.inner)
}
}

View File

@ -50,7 +50,9 @@ where
}
}
pub fn fancy_error<E: Error + 'static>(templates: Templates) -> impl Fn(E) -> FancyError {
pub fn fancy_error<E: std::fmt::Display + 'static>(
templates: Templates,
) -> impl Fn(E) -> FancyError {
move |error: E| FancyError {
templates: Some(templates.clone()),
error: Box::new(error),
@ -69,7 +71,7 @@ where
pub struct FancyError {
templates: Option<Templates>,
error: Box<dyn Error>,
error: Box<dyn std::fmt::Display>,
}
impl IntoResponse for FancyError {
@ -99,4 +101,3 @@ impl IntoResponse for FancyError {
res
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2021 The Matrix.org Foundation C.I.C.
// Copyright 2021, 2022 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.
@ -21,7 +21,7 @@
use std::sync::Arc;
use axum::{extract::Extension, routing::get, Router};
use axum::{body::HttpBody, extract::Extension, routing::get, Router};
use mas_axum_utils::UrlBuilder;
use mas_config::{Encrypter, RootConfig};
use mas_email::Mailer;
@ -61,17 +61,26 @@ pub fn root(
}
#[must_use]
pub fn router<B: Send + 'static>(
pub fn router<B>(
pool: &PgPool,
templates: &Templates,
key_store: &Arc<StaticKeystore>,
encrypter: &Encrypter,
mailer: &Mailer,
url_builder: &UrlBuilder,
) -> Router<B> {
) -> Router<B>
where
B: HttpBody + Send + 'static,
<B as HttpBody>::Data: Send,
<B as HttpBody>::Error: std::error::Error + Send + Sync,
{
Router::new()
.route("/", get(self::views::index::get))
.route("/health", get(self::health::get))
.route(
"/login",
get(self::views::login::get).post(self::views::login::post),
)
.fallback(mas_static_files::Assets)
.layer(Extension(pool.clone()))
.layer(Extension(templates.clone()))

View File

@ -1,4 +1,4 @@
// Copyright 2021 The Matrix.org Foundation C.I.C.
// Copyright 2021, 2022 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,25 +14,21 @@
#![allow(clippy::trait_duplication_in_bounds)]
use hyper::http::uri::{Parts, PathAndQuery, Uri};
use mas_config::{CsrfConfig, Encrypter};
use mas_data_model::{errors::WrapFormError, BrowserSession};
use mas_storage::{user::login, PostgresqlBackend};
use mas_templates::{LoginContext, LoginFormField, TemplateContext, Templates};
use mas_warp_utils::{
errors::WrapError,
filters::{
self,
cookies::{encrypted_cookie_saver, EncryptedCookieSaver},
csrf::{protected_form, updated_csrf_token},
database::connection,
session::{optional_session, SessionCookie},
with_templates, CsrfToken,
},
use axum::{
extract::{Extension, Form, Query},
response::{Html, IntoResponse, Redirect, Response},
};
use hyper::http::uri::{Parts, PathAndQuery, Uri};
use mas_axum_utils::{
csrf::{CsrfExt, ProtectedForm},
fancy_error, FancyError, PrivateCookieJar, SessionInfoExt,
};
use mas_config::Encrypter;
use mas_data_model::errors::WrapFormError;
use mas_storage::user::login;
use mas_templates::{LoginContext, LoginFormField, TemplateContext, Templates};
use serde::Deserialize;
use sqlx::{pool::PoolConnection, PgPool, Postgres};
use warp::{filters::BoxedFilter, reply::html, Filter, Rejection, Reply};
use sqlx::PgPool;
use super::{shared::PostAuthAction, RegisterRequest};
@ -66,100 +62,100 @@ impl LoginRequest {
Ok(uri)
}
fn redirect(self) -> Result<impl Reply, Rejection> {
let uri = self
.post_auth_action
.as_ref()
.map(PostAuthAction::build_uri)
.transpose()
.wrap_error()?
.unwrap_or_else(|| Uri::from_static("/"));
Ok(warp::redirect::see_other(uri))
fn redirect(self) -> Result<impl IntoResponse, anyhow::Error> {
let uri = if let Some(action) = self.post_auth_action {
action.build_uri()?
} else {
Uri::from_static("/")
};
Ok(Redirect::to(uri))
}
}
#[derive(Deserialize)]
struct LoginForm {
pub(crate) struct LoginForm {
username: String,
password: String,
}
pub(super) fn filter(
pool: &PgPool,
templates: &Templates,
encrypter: &Encrypter,
csrf_config: &CsrfConfig,
) -> BoxedFilter<(Box<dyn Reply>,)> {
let get = warp::get()
.and(filters::trace::name("GET /login"))
.and(with_templates(templates))
.and(connection(pool))
.and(encrypted_cookie_saver(encrypter))
.and(updated_csrf_token(encrypter, csrf_config))
.and(warp::query())
.and(optional_session(pool, encrypter))
.and_then(get);
pub(crate) async fn get(
Extension(templates): Extension<Templates>,
Extension(pool): Extension<PgPool>,
Query(query): Query<LoginRequest>,
cookie_jar: PrivateCookieJar<Encrypter>,
) -> Result<Response, FancyError> {
let mut conn = pool
.acquire()
.await
.map_err(fancy_error(templates.clone()))?;
let post = warp::post()
.and(filters::trace::name("POST /login"))
.and(with_templates(templates))
.and(connection(pool))
.and(encrypted_cookie_saver(encrypter))
.and(updated_csrf_token(encrypter, csrf_config))
.and(protected_form(encrypter))
.and(warp::query())
.and_then(post);
let (csrf_token, cookie_jar) = cookie_jar.csrf_token();
let (session_info, cookie_jar) = cookie_jar.session_info();
warp::path!("login").and(get.or(post).unify()).boxed()
}
let maybe_session = session_info
.load_session(&mut conn)
.await
.map_err(fancy_error(templates.clone()))?;
async fn get(
templates: Templates,
mut conn: PoolConnection<Postgres>,
cookie_saver: EncryptedCookieSaver,
csrf_token: CsrfToken,
query: LoginRequest,
maybe_session: Option<BrowserSession<PostgresqlBackend>>,
) -> Result<Box<dyn Reply>, Rejection> {
if maybe_session.is_some() {
Ok(Box::new(query.redirect()?))
let response = query
.redirect()
.map_err(fancy_error(templates.clone()))?
.into_response();
Ok(response)
} else {
let ctx = LoginContext::default();
let ctx = match query.post_auth_action {
Some(next) => {
let register_link = RegisterRequest::from(next.clone())
.build_uri()
.wrap_error()?;
let next = next.load_context(&mut conn).await.wrap_error()?;
.map_err(fancy_error(templates.clone()))?;
let next = next
.load_context(&mut conn)
.await
.map_err(fancy_error(templates.clone()))?;
ctx.with_post_action(next)
.with_register_link(register_link.to_string())
}
None => ctx,
};
let ctx = ctx.with_csrf(csrf_token.form_value());
let content = templates.render_login(&ctx).await?;
let reply = html(content);
let reply = cookie_saver.save_encrypted(&csrf_token, reply)?;
Ok(Box::new(reply))
let content = templates
.render_login(&ctx)
.await
.map_err(fancy_error(templates.clone()))?;
Ok((cookie_jar.headers(), Html(content)).into_response())
}
}
async fn post(
templates: Templates,
mut conn: PoolConnection<Postgres>,
cookie_saver: EncryptedCookieSaver,
csrf_token: CsrfToken,
form: LoginForm,
query: LoginRequest,
) -> Result<Box<dyn Reply>, Rejection> {
pub(crate) async fn post(
Extension(templates): Extension<Templates>,
Extension(pool): Extension<PgPool>,
Query(query): Query<LoginRequest>,
cookie_jar: PrivateCookieJar<Encrypter>,
Form(form): Form<ProtectedForm<LoginForm>>,
) -> Result<Response, FancyError> {
use mas_storage::user::LoginError;
let mut conn = pool
.acquire()
.await
.map_err(fancy_error(templates.clone()))?;
let form = cookie_jar
.verify_form(form)
.map_err(fancy_error(templates.clone()))?;
let (csrf_token, cookie_jar) = cookie_jar.csrf_token();
// TODO: recover
match login(&mut conn, &form.username, form.password).await {
Ok(session_info) => {
let session_cookie = SessionCookie::from_session(&session_info);
let reply = query.redirect()?;
let reply = cookie_saver.save_encrypted(&session_cookie, reply)?;
Ok(Box::new(reply))
let cookie_jar = cookie_jar.set_session(&session_info);
let reply = query.redirect().map_err(fancy_error(templates.clone()))?;
Ok((cookie_jar.headers(), reply).into_response())
}
Err(e) => {
let errored_form = match e {
@ -170,10 +166,13 @@ async fn post(
let ctx = LoginContext::default()
.with_form_error(errored_form)
.with_csrf(csrf_token.form_value());
let content = templates.render_login(&ctx).await?;
let reply = html(content);
let reply = cookie_saver.save_encrypted(&csrf_token, reply)?;
Ok(Box::new(reply))
let content = templates
.render_login(&ctx)
.await
.map_err(fancy_error(templates.clone()))?;
Ok((cookie_jar.headers(), Html(content)).into_response())
}
}
}

View File

@ -28,8 +28,8 @@ pub mod shared;
pub mod verify;
use self::{
account::filter as account, login::filter as login, logout::filter as logout,
reauth::filter as reauth, register::filter as register, verify::filter as verify,
account::filter as account, logout::filter as logout, reauth::filter as reauth,
register::filter as register, verify::filter as verify,
};
pub(crate) use self::{
login::LoginRequest, reauth::ReauthRequest, register::RegisterRequest, shared::PostAuthAction,
@ -44,15 +44,12 @@ pub(super) fn filter(
csrf_config: &CsrfConfig,
) -> BoxedFilter<(Box<dyn Reply>,)> {
let account = account(pool, templates, mailer, encrypter, http_config, csrf_config);
let login = login(pool, templates, encrypter, csrf_config);
let register = register(pool, templates, encrypter, csrf_config);
let logout = logout(pool, encrypter);
let reauth = reauth(pool, templates, encrypter, csrf_config);
let verify = verify(pool, templates, encrypter, csrf_config);
account
.or(login)
.unify()
.or(register)
.unify()
.or(logout)