1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-11-20 12:02:22 +03:00

Axum migration: /account/* routes

This commit is contained in:
Quentin Gliech
2022-03-28 17:04:40 +02:00
parent b4d0906e75
commit 9cb5650167
9 changed files with 233 additions and 261 deletions

View File

@@ -114,7 +114,10 @@ pub trait CsrfExt {
impl<K> CsrfExt for PrivateCookieJar<K> {
fn csrf_token(self) -> (CsrfToken, Self) {
let jar = self;
let cookie = jar.get("csrf").unwrap_or_else(|| Cookie::new("csrf", ""));
let mut cookie = jar.get("csrf").unwrap_or_else(|| Cookie::new("csrf", ""));
cookie.set_path("/");
cookie.set_http_only(true);
let new_token = cookie
.decode()
.ok()

View File

@@ -76,9 +76,11 @@ pub trait SessionInfoExt {
impl<K> SessionInfoExt for PrivateCookieJar<K> {
fn session_info(self) -> (SessionInfo, Self) {
let jar = self;
let cookie = jar
let mut cookie = jar
.get("session")
.unwrap_or_else(|| Cookie::new("session", ""));
cookie.set_path("/");
cookie.set_http_only(true);
let session_info = cookie.decode().unwrap_or_default();
let cookie = cookie.encode(&session_info);

View File

@@ -39,7 +39,7 @@ mod health;
mod oauth2;
mod views;
use self::{oauth2::filter as oauth2, views::filter as views};
use self::oauth2::filter as oauth2;
#[must_use]
pub fn root(
@@ -47,20 +47,11 @@ pub fn root(
templates: &Templates,
key_store: &Arc<StaticKeystore>,
encrypter: &Encrypter,
mailer: &Mailer,
config: &RootConfig,
) -> BoxedFilter<(impl Reply,)> {
let oauth2 = oauth2(pool, templates, key_store, encrypter, &config.http);
let views = views(
pool,
templates,
mailer,
encrypter,
&config.http,
&config.csrf,
);
let filter = views.or(oauth2);
let filter = oauth2;
filter.with(warp::log(module_path!())).boxed()
}
@@ -96,6 +87,15 @@ where
get(self::views::register::get).post(self::views::register::post),
)
.route("/verify/:code", get(self::views::verify::get))
.route("/account", get(self::views::account::get))
.route(
"/account/password",
get(self::views::account::password::get).post(self::views::account::password::post),
)
.route(
"/account/emails",
get(self::views::account::emails::get).post(self::views::account::emails::post),
)
.fallback(mas_static_files::Assets)
.layer(Extension(pool.clone()))
.layer(Extension(templates.clone()))

View File

@@ -12,8 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use axum::{
extract::{Extension, Form},
response::{Html, IntoResponse, Redirect, Response},
};
use lettre::{message::Mailbox, Address};
use mas_config::{CsrfConfig, Encrypter, HttpConfig};
use mas_axum_utils::{
csrf::{CsrfExt, ProtectedForm},
fancy_error, FancyError, PrivateCookieJar, SessionInfoExt, UrlBuilder,
};
use mas_config::Encrypter;
use mas_data_model::{BrowserSession, User, UserEmail};
use mas_email::Mailer;
use mas_storage::{
@@ -24,99 +32,70 @@ use mas_storage::{
PostgresqlBackend,
};
use mas_templates::{AccountEmailsContext, EmailVerificationContext, TemplateContext, Templates};
use mas_warp_utils::{
errors::WrapError,
filters::{
self,
cookies::{encrypted_cookie_saver, EncryptedCookieSaver},
csrf::{protected_form, updated_csrf_token},
database::{connection, transaction},
session::session,
url_builder::{url_builder, UrlBuilder},
with_templates, CsrfToken,
},
};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::Deserialize;
use sqlx::{pool::PoolConnection, PgExecutor, PgPool, Postgres, Transaction};
use sqlx::{PgExecutor, PgPool};
use tracing::info;
use warp::{filters::BoxedFilter, reply::html, Filter, Rejection, Reply};
pub(super) fn filter(
pool: &PgPool,
templates: &Templates,
mailer: &Mailer,
encrypter: &Encrypter,
http_config: &HttpConfig,
csrf_config: &CsrfConfig,
) -> BoxedFilter<(Box<dyn Reply>,)> {
let mailer = mailer.clone();
let get = with_templates(templates)
.and(filters::trace::name("GET /account/emails"))
.and(encrypted_cookie_saver(encrypter))
.and(updated_csrf_token(encrypter, csrf_config))
.and(session(pool, encrypter))
.and(connection(pool))
.and_then(get);
let post = with_templates(templates)
.and(filters::trace::name("POST /account/emails"))
.and(warp::any().map(move || mailer.clone()))
.and(url_builder(http_config))
.and(encrypted_cookie_saver(encrypter))
.and(updated_csrf_token(encrypter, csrf_config))
.and(session(pool, encrypter))
.and(transaction(pool))
.and(protected_form(encrypter))
.and_then(post);
let get = warp::get().and(get);
let post = warp::post().and(post);
let filter = get.or(post).unify();
warp::path!("emails").and(filter).boxed()
}
use crate::views::LoginRequest;
#[derive(Deserialize, Debug)]
#[serde(tag = "action", rename_all = "snake_case")]
enum Form {
pub enum ManagementForm {
Add { email: String },
ResendConfirmation { data: String },
SetPrimary { data: String },
Remove { data: String },
}
async fn get(
templates: Templates,
cookie_saver: EncryptedCookieSaver,
csrf_token: CsrfToken,
session: BrowserSession<PostgresqlBackend>,
mut conn: PoolConnection<Postgres>,
) -> Result<Box<dyn Reply>, Rejection> {
render(templates, cookie_saver, csrf_token, session, &mut conn).await
pub(crate) async fn get(
Extension(templates): Extension<Templates>,
Extension(pool): Extension<PgPool>,
cookie_jar: PrivateCookieJar<Encrypter>,
) -> Result<Response, FancyError> {
let mut conn = pool
.acquire()
.await
.map_err(fancy_error(templates.clone()))?;
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info
.load_session(&mut conn)
.await
.map_err(fancy_error(templates.clone()))?;
if let Some(session) = maybe_session {
render(templates, session, cookie_jar, &mut conn).await
} else {
let login = LoginRequest::default();
let login = login.build_uri().map_err(fancy_error(templates.clone()))?;
Ok((cookie_jar.headers(), Redirect::to(login)).into_response())
}
}
async fn render(
templates: Templates,
cookie_saver: EncryptedCookieSaver,
csrf_token: CsrfToken,
session: BrowserSession<PostgresqlBackend>,
cookie_jar: PrivateCookieJar<Encrypter>,
executor: impl PgExecutor<'_>,
) -> Result<Box<dyn Reply>, Rejection> {
) -> Result<Response, FancyError> {
let (csrf_token, cookie_jar) = cookie_jar.csrf_token();
let emails = get_user_emails(executor, &session.user)
.await
.wrap_error()?;
.map_err(fancy_error(templates.clone()))?;
let ctx = AccountEmailsContext::new(emails)
.with_session(session)
.with_csrf(csrf_token.form_value());
let content = templates.render_account_emails(&ctx).await?;
let reply = html(content);
let reply = cookie_saver.save_encrypted(&csrf_token, reply)?;
let content = templates
.render_account_emails(&ctx)
.await
.map_err(fancy_error(templates))?;
Ok(Box::new(reply))
Ok((cookie_jar.headers(), Html(content)).into_response())
}
async fn start_email_verification(
@@ -150,59 +129,80 @@ async fn start_email_verification(
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn post(
templates: Templates,
mailer: Mailer,
url_builder: UrlBuilder,
cookie_saver: EncryptedCookieSaver,
csrf_token: CsrfToken,
mut session: BrowserSession<PostgresqlBackend>,
mut txn: Transaction<'_, Postgres>,
form: Form,
) -> Result<Box<dyn Reply>, Rejection> {
pub(crate) async fn post(
Extension(templates): Extension<Templates>,
Extension(pool): Extension<PgPool>,
Extension(url_builder): Extension<UrlBuilder>,
Extension(mailer): Extension<Mailer>,
cookie_jar: PrivateCookieJar<Encrypter>,
Form(form): Form<ProtectedForm<ManagementForm>>,
) -> Result<Response, FancyError> {
let mut txn = pool.begin().await.map_err(fancy_error(templates.clone()))?;
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info
.load_session(&mut txn)
.await
.map_err(fancy_error(templates.clone()))?;
let mut session = if let Some(session) = maybe_session {
session
} else {
let login = LoginRequest::default();
let login = login.build_uri().map_err(fancy_error(templates.clone()))?;
return Ok((cookie_jar.headers(), Redirect::to(login)).into_response());
};
let form = cookie_jar
.verify_form(form)
.map_err(fancy_error(templates.clone()))?;
match form {
Form::Add { email } => {
ManagementForm::Add { email } => {
let user_email = add_user_email(&mut txn, &session.user, email)
.await
.wrap_error()?;
.map_err(fancy_error(templates.clone()))?;
start_email_verification(&mailer, &url_builder, &mut txn, &session.user, &user_email)
.await
.wrap_error()?;
.map_err(fancy_error(templates.clone()))?;
}
Form::Remove { data } => {
let id = data.parse().wrap_error()?;
ManagementForm::Remove { data } => {
let id = data.parse().map_err(fancy_error(templates.clone()))?;
let email = get_user_email(&mut txn, &session.user, id)
.await
.wrap_error()?;
remove_user_email(&mut txn, email).await.wrap_error()?;
.map_err(fancy_error(templates.clone()))?;
remove_user_email(&mut txn, email)
.await
.map_err(fancy_error(templates.clone()))?;
}
Form::ResendConfirmation { data } => {
let id: i64 = data.parse().wrap_error()?;
ManagementForm::ResendConfirmation { data } => {
let id = data.parse().map_err(fancy_error(templates.clone()))?;
let user_email = get_user_email(&mut txn, &session.user, id)
.await
.wrap_error()?;
.map_err(fancy_error(templates.clone()))?;
start_email_verification(&mailer, &url_builder, &mut txn, &session.user, &user_email)
.await
.wrap_error()?;
.map_err(fancy_error(templates.clone()))?;
}
Form::SetPrimary { data } => {
let id = data.parse().wrap_error()?;
ManagementForm::SetPrimary { data } => {
let id = data.parse().map_err(fancy_error(templates.clone()))?;
let email = get_user_email(&mut txn, &session.user, id)
.await
.wrap_error()?;
.map_err(fancy_error(templates.clone()))?;
set_user_email_as_primary(&mut txn, &email)
.await
.wrap_error()?;
.map_err(fancy_error(templates.clone()))?;
session.user.primary_email = Some(email);
}
};
let reply = render(templates, cookie_saver, csrf_token, session, &mut txn).await?;
let reply = render(templates.clone(), session, cookie_jar, &mut txn).await?;
txn.commit().await.wrap_error()?;
txn.commit().await.map_err(fancy_error(templates.clone()))?;
Ok(reply)
}

View File

@@ -1,4 +1,4 @@
// Copyright 2021-2022 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.
@@ -12,81 +12,63 @@
// See the License for the specific language governing permissions and
// limitations under the License.
mod emails;
mod password;
pub mod emails;
pub mod password;
use mas_config::{CsrfConfig, Encrypter, HttpConfig};
use mas_data_model::BrowserSession;
use mas_email::Mailer;
use mas_storage::{
user::{count_active_sessions, get_user_emails},
PostgresqlBackend,
use axum::{
extract::Extension,
response::{Html, IntoResponse, Redirect, Response},
};
use mas_axum_utils::{csrf::CsrfExt, fancy_error, FancyError, PrivateCookieJar, SessionInfoExt};
use mas_config::Encrypter;
use mas_storage::user::{count_active_sessions, get_user_emails};
use mas_templates::{AccountContext, TemplateContext, Templates};
use mas_warp_utils::{
errors::WrapError,
filters::{
self,
cookies::{encrypted_cookie_saver, EncryptedCookieSaver},
csrf::updated_csrf_token,
database::connection,
session::session,
with_templates, CsrfToken,
},
use sqlx::PgPool;
use super::LoginRequest;
pub(crate) async fn get(
Extension(templates): Extension<Templates>,
Extension(pool): Extension<PgPool>,
cookie_jar: PrivateCookieJar<Encrypter>,
) -> Result<Response, FancyError> {
let mut conn = pool
.acquire()
.await
.map_err(fancy_error(templates.clone()))?;
let (csrf_token, cookie_jar) = cookie_jar.csrf_token();
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info
.load_session(&mut conn)
.await
.map_err(fancy_error(templates.clone()))?;
let session = if let Some(session) = maybe_session {
session
} else {
let login = LoginRequest::default();
let login = login.build_uri().map_err(fancy_error(templates.clone()))?;
return Ok((cookie_jar.headers(), Redirect::to(login)).into_response());
};
use sqlx::{pool::PoolConnection, PgPool, Postgres};
use warp::{filters::BoxedFilter, reply::html, Filter, Rejection, Reply};
use self::{emails::filter as emails, password::filter as password};
pub(super) fn filter(
pool: &PgPool,
templates: &Templates,
mailer: &Mailer,
encrypter: &Encrypter,
http_config: &HttpConfig,
csrf_config: &CsrfConfig,
) -> BoxedFilter<(Box<dyn Reply>,)> {
let get = warp::get()
.and(filters::trace::name("GET /account"))
.and(with_templates(templates))
.and(encrypted_cookie_saver(encrypter))
.and(updated_csrf_token(encrypter, csrf_config))
.and(session(pool, encrypter))
.and(connection(pool))
.and_then(get);
let index = warp::path::end().and(get);
let password = password(pool, templates, encrypter, csrf_config);
let emails = emails(pool, templates, mailer, encrypter, http_config, csrf_config);
let filter = index.or(password).unify().or(emails).unify();
warp::path::path("account").and(filter).boxed()
}
async fn get(
templates: Templates,
cookie_saver: EncryptedCookieSaver,
csrf_token: CsrfToken,
session: BrowserSession<PostgresqlBackend>,
mut conn: PoolConnection<Postgres>,
) -> Result<Box<dyn Reply>, Rejection> {
let active_sessions = count_active_sessions(&mut conn, &session.user)
.await
.wrap_error()?;
.map_err(fancy_error(templates.clone()))?;
let emails = get_user_emails(&mut conn, &session.user)
.await
.wrap_error()?;
.map_err(fancy_error(templates.clone()))?;
let ctx = AccountContext::new(active_sessions, emails)
.with_session(session)
.with_csrf(csrf_token.form_value());
let content = templates.render_account_index(&ctx).await?;
let reply = html(content);
let reply = cookie_saver.save_encrypted(&csrf_token, reply)?;
let content = templates
.render_account_index(&ctx)
.await
.map_err(fancy_error(templates.clone()))?;
Ok(Box::new(reply))
Ok((cookie_jar.headers(), Html(content)).into_response())
}

View File

@@ -13,117 +13,122 @@
// limitations under the License.
use argon2::Argon2;
use mas_config::{CsrfConfig, Encrypter};
use axum::{
extract::{Extension, Form},
response::{Html, IntoResponse, Redirect, Response},
};
use mas_axum_utils::{
csrf::{CsrfExt, ProtectedForm},
fancy_error, FancyError, PrivateCookieJar, SessionInfoExt,
};
use mas_config::Encrypter;
use mas_data_model::BrowserSession;
use mas_storage::{
user::{authenticate_session, set_password},
PostgresqlBackend,
};
use mas_templates::{EmptyContext, TemplateContext, Templates};
use mas_warp_utils::{
errors::WrapError,
filters::{
self,
cookies::{encrypted_cookie_saver, EncryptedCookieSaver},
csrf::{protected_form, updated_csrf_token},
database::transaction,
session::session,
with_templates, CsrfToken,
},
};
use serde::Deserialize;
use sqlx::{PgPool, Postgres, Transaction};
use warp::{filters::BoxedFilter, reply::html, Filter, Rejection, Reply};
use sqlx::PgPool;
pub(super) fn filter(
pool: &PgPool,
templates: &Templates,
encrypter: &Encrypter,
csrf_config: &CsrfConfig,
) -> BoxedFilter<(Box<dyn Reply>,)> {
let get = with_templates(templates)
.and(encrypted_cookie_saver(encrypter))
.and(updated_csrf_token(encrypter, csrf_config))
.and(session(pool, encrypter))
.and_then(get);
let post = with_templates(templates)
.and(encrypted_cookie_saver(encrypter))
.and(updated_csrf_token(encrypter, csrf_config))
.and(session(pool, encrypter))
.and(transaction(pool))
.and(protected_form(encrypter))
.and_then(post);
let get = warp::get()
.and(get)
.and(filters::trace::name("GET /account/passwords"));
let post = warp::post()
.and(post)
.and(filters::trace::name("POST /account/passwords"));
let filter = get.or(post).unify();
warp::path!("password").and(filter).boxed()
}
use crate::views::LoginRequest;
#[derive(Deserialize)]
struct Form {
pub struct ChangeForm {
current_password: String,
new_password: String,
new_password_confirm: String,
}
async fn get(
templates: Templates,
cookie_saver: EncryptedCookieSaver,
csrf_token: CsrfToken,
session: BrowserSession<PostgresqlBackend>,
) -> Result<Box<dyn Reply>, Rejection> {
render(templates, cookie_saver, csrf_token, session).await
pub(crate) async fn get(
Extension(templates): Extension<Templates>,
Extension(pool): Extension<PgPool>,
cookie_jar: PrivateCookieJar<Encrypter>,
) -> Result<Response, FancyError> {
let mut conn = pool
.acquire()
.await
.map_err(fancy_error(templates.clone()))?;
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info
.load_session(&mut conn)
.await
.map_err(fancy_error(templates.clone()))?;
if let Some(session) = maybe_session {
render(templates, session, cookie_jar).await
} else {
let login = LoginRequest::default();
let login = login.build_uri().map_err(fancy_error(templates.clone()))?;
Ok((cookie_jar.headers(), Redirect::to(login)).into_response())
}
}
async fn render(
templates: Templates,
cookie_saver: EncryptedCookieSaver,
csrf_token: CsrfToken,
session: BrowserSession<PostgresqlBackend>,
) -> Result<Box<dyn Reply>, Rejection> {
cookie_jar: PrivateCookieJar<Encrypter>,
) -> Result<Response, FancyError> {
let (csrf_token, cookie_jar) = cookie_jar.csrf_token();
let ctx = EmptyContext
.with_session(session)
.with_csrf(csrf_token.form_value());
let content = templates.render_account_password(&ctx).await?;
let reply = html(content);
let reply = cookie_saver.save_encrypted(&csrf_token, reply)?;
let content = templates
.render_account_password(&ctx)
.await
.map_err(fancy_error(templates))?;
Ok(Box::new(reply))
Ok((cookie_jar.headers(), Html(content)).into_response())
}
async fn post(
templates: Templates,
cookie_saver: EncryptedCookieSaver,
csrf_token: CsrfToken,
mut session: BrowserSession<PostgresqlBackend>,
mut txn: Transaction<'_, Postgres>,
form: Form,
) -> Result<Box<dyn Reply>, Rejection> {
pub(crate) async fn post(
Extension(templates): Extension<Templates>,
Extension(pool): Extension<PgPool>,
cookie_jar: PrivateCookieJar<Encrypter>,
Form(form): Form<ProtectedForm<ChangeForm>>,
) -> Result<Response, FancyError> {
let mut txn = pool.begin().await.map_err(fancy_error(templates.clone()))?;
let form = cookie_jar
.verify_form(form)
.map_err(fancy_error(templates.clone()))?;
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info
.load_session(&mut txn)
.await
.map_err(fancy_error(templates.clone()))?;
let mut session = if let Some(session) = maybe_session {
session
} else {
let login = LoginRequest::default();
let login = login.build_uri().map_err(fancy_error(templates.clone()))?;
return Ok((cookie_jar.headers(), Redirect::to(login)).into_response());
};
authenticate_session(&mut txn, &mut session, form.current_password)
.await
.wrap_error()?;
.map_err(fancy_error(templates.clone()))?;
// TODO: display nice form errors
if form.new_password != form.new_password_confirm {
return Err(anyhow::anyhow!("password mismatch")).wrap_error();
return Err(anyhow::anyhow!("password mismatch")).map_err(fancy_error(templates.clone()));
}
let phf = Argon2::default();
set_password(&mut txn, phf, &session.user, &form.new_password)
.await
.wrap_error()?;
.map_err(fancy_error(templates.clone()))?;
let reply = render(templates, cookie_saver, csrf_token, session).await?;
let reply = render(templates.clone(), session, cookie_jar).await?;
txn.commit().await.wrap_error()?;
txn.commit().await.map_err(fancy_error(templates.clone()))?;
Ok(reply)
}

View File

@@ -30,7 +30,7 @@ use sqlx::PgPool;
use super::{shared::PostAuthAction, RegisterRequest};
#[derive(Deserialize)]
#[derive(Deserialize, Default)]
pub(crate) struct LoginRequest {
#[serde(flatten)]
post_auth_action: Option<PostAuthAction>,

View File

@@ -1,4 +1,4 @@
// Copyright 2021-2022 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.
@@ -12,12 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use mas_config::{CsrfConfig, Encrypter, HttpConfig};
use mas_email::Mailer;
use mas_templates::Templates;
use sqlx::PgPool;
use warp::{filters::BoxedFilter, Filter, Reply};
pub mod account;
pub mod index;
pub mod login;
@@ -27,20 +21,6 @@ pub mod register;
pub mod shared;
pub mod verify;
use self::account::filter as account;
pub(crate) use self::{
login::LoginRequest, reauth::ReauthRequest, register::RegisterRequest, shared::PostAuthAction,
};
pub(super) fn filter(
pool: &PgPool,
templates: &Templates,
mailer: &Mailer,
encrypter: &Encrypter,
http_config: &HttpConfig,
csrf_config: &CsrfConfig,
) -> BoxedFilter<(Box<dyn Reply>,)> {
let account = account(pool, templates, mailer, encrypter, http_config, csrf_config);
account.boxed()
}

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.