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

Make email verification mandatory

This commit is contained in:
Quentin Gliech
2022-05-24 15:53:35 +02:00
parent 89597dbf81
commit 125afd61c0
11 changed files with 310 additions and 25 deletions

View File

@ -65,6 +65,19 @@ pub async fn get(
return Ok((cookie_jar, login.go()).into_response());
};
// TODO: make that more generic
if session
.user
.primary_email
.as_ref()
.and_then(|e| e.confirmed_at)
.is_none()
{
let destination = mas_router::AccountAddEmail::default()
.and_then(PostAuthAction::ContinueCompatSsoLogin { data: id });
return Ok((cookie_jar, destination.go()).into_response());
}
let login = get_compat_sso_login_by_id(&mut conn, id).await?;
// Bail out if that login session is more than 30min old
@ -108,6 +121,19 @@ pub async fn post(
return Ok((cookie_jar, login.go()).into_response());
};
// TODO: make that more generic
if session
.user
.primary_email
.as_ref()
.and_then(|e| e.confirmed_at)
.is_none()
{
let destination = mas_router::AccountAddEmail::default()
.and_then(PostAuthAction::ContinueCompatSsoLogin { data: id });
return Ok((cookie_jar, destination.go()).into_response());
}
let login = get_compat_sso_login_by_id(&mut txn, id).await?;
// Bail out if that login session is more than 30min old

View File

@ -158,11 +158,6 @@ where
mas_router::Register::route(),
get(self::views::register::get).post(self::views::register::post),
)
.route(
mas_router::AccountVerifyEmail::route(),
get(self::views::account::emails::verify::get)
.post(self::views::account::emails::verify::post),
)
.route(mas_router::Account::route(), get(self::views::account::get))
.route(
mas_router::AccountPassword::route(),
@ -172,6 +167,16 @@ where
mas_router::AccountEmails::route(),
get(self::views::account::emails::get).post(self::views::account::emails::post),
)
.route(
mas_router::AccountVerifyEmail::route(),
get(self::views::account::emails::verify::get)
.post(self::views::account::emails::verify::post),
)
.route(
mas_router::AccountAddEmail::route(),
get(self::views::account::emails::add::get)
.post(self::views::account::emails::add::post),
)
.route(
mas_router::OAuth2AuthorizationEndpoint::route(),
get(self::oauth2::authorization::get),

View File

@ -0,0 +1,101 @@
// Copyright 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.
// 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::{Extension, Form, Query},
response::{Html, IntoResponse, Response},
};
use axum_extra::extract::PrivateCookieJar;
use mas_axum_utils::{
csrf::{CsrfExt, ProtectedForm},
FancyError, SessionInfoExt,
};
use mas_config::Encrypter;
use mas_email::Mailer;
use mas_router::Route;
use mas_storage::user::add_user_email;
use mas_templates::{EmailAddContext, TemplateContext, Templates};
use serde::Deserialize;
use sqlx::PgPool;
use super::start_email_verification;
use crate::views::shared::OptionalPostAuthAction;
#[derive(Deserialize, Debug)]
pub struct EmailForm {
email: String,
}
pub(crate) async fn get(
Extension(templates): Extension<Templates>,
Extension(pool): Extension<PgPool>,
cookie_jar: PrivateCookieJar<Encrypter>,
) -> Result<Response, FancyError> {
let mut conn = pool.begin().await?;
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?;
let session = if let Some(session) = maybe_session {
session
} else {
let login = mas_router::Login::default();
return Ok((cookie_jar, login.go()).into_response());
};
let ctx = EmailAddContext::new()
.with_session(session)
.with_csrf(csrf_token.form_value());
let content = templates.render_account_add_email(&ctx).await?;
Ok((cookie_jar, Html(content)).into_response())
}
pub(crate) async fn post(
Extension(pool): Extension<PgPool>,
Extension(mailer): Extension<Mailer>,
cookie_jar: PrivateCookieJar<Encrypter>,
Query(query): Query<OptionalPostAuthAction>,
Form(form): Form<ProtectedForm<EmailForm>>,
) -> Result<Response, FancyError> {
let mut txn = pool.begin().await?;
let form = cookie_jar.verify_form(form)?;
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info.load_session(&mut txn).await?;
let session = if let Some(session) = maybe_session {
session
} else {
let login = mas_router::Login::default();
return Ok((cookie_jar, login.go()).into_response());
};
let user_email = add_user_email(&mut txn, &session.user, form.email).await?;
let next = mas_router::AccountVerifyEmail::new(user_email.data);
let next = if let Some(action) = query.post_auth_action {
next.and_then(action)
} else {
next
};
start_email_verification(&mailer, &mut txn, &session.user, user_email).await?;
txn.commit().await?;
Ok((cookie_jar, next.go()).into_response())
}

View File

@ -39,12 +39,14 @@ use serde::Deserialize;
use sqlx::{PgExecutor, PgPool};
use tracing::info;
pub mod add;
pub mod verify;
#[derive(Deserialize, Debug)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum ManagementForm {
Add { email: String },
ResendConfirmation { data: String },
SetPrimary { data: String },
Remove { data: String },
}
@ -140,7 +142,16 @@ pub(crate) async fn post(
match form {
ManagementForm::Add { email } => {
let user_email = add_user_email(&mut txn, &session.user, email).await?;
let next = mas_router::AccountVerifyEmail(user_email.data);
let next = mas_router::AccountVerifyEmail::new(user_email.data);
start_email_verification(&mailer, &mut txn, &session.user, user_email).await?;
txn.commit().await?;
return Ok((cookie_jar, next.go()).into_response());
}
ManagementForm::ResendConfirmation { data } => {
let id = data.parse()?;
let user_email = get_user_email(&mut txn, &session.user, id).await?;
let next = mas_router::AccountVerifyEmail::new(user_email.data);
start_email_verification(&mailer, &mut txn, &session.user, user_email).await?;
txn.commit().await?;
return Ok((cookie_jar, next.go()).into_response());

View File

@ -26,7 +26,7 @@ use mas_config::Encrypter;
use mas_router::Route;
use mas_storage::user::{
consume_email_verification, lookup_user_email_by_id, lookup_user_email_verification_code,
mark_user_email_as_verified,
mark_user_email_as_verified, set_user_email_as_primary,
};
use mas_templates::{EmailVerificationPageContext, TemplateContext, Templates};
use serde::Deserialize;
@ -46,12 +46,12 @@ pub(crate) async fn get(
Path(id): Path<i64>,
cookie_jar: PrivateCookieJar<Encrypter>,
) -> Result<Response, FancyError> {
let mut txn = pool.begin().await?;
let mut conn = pool.acquire().await?;
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 txn).await?;
let maybe_session = session_info.load_session(&mut conn).await?;
let session = if let Some(session) = maybe_session {
session
@ -60,7 +60,7 @@ pub(crate) async fn get(
return Ok((cookie_jar, login.go()).into_response());
};
let user_email = lookup_user_email_by_id(&mut txn, &session.user, id).await?;
let user_email = lookup_user_email_by_id(&mut conn, &session.user, id).await?;
if user_email.confirmed_at.is_some() {
// This email was already verified, skip
@ -72,9 +72,7 @@ pub(crate) async fn get(
.with_session(session)
.with_csrf(csrf_token.form_value());
let content = templates.render_email_verification_form(&ctx).await?;
txn.commit().await?;
let content = templates.render_account_verify_email(&ctx).await?;
Ok((cookie_jar, Html(content)).into_response())
}
@ -102,6 +100,10 @@ pub(crate) async fn post(
let email = lookup_user_email_by_id(&mut txn, &session.user, id).await?;
if session.user.primary_email.is_none() {
set_user_email_as_primary(&mut txn, &email).await?;
}
// TODO: make those 8 hours configurable
let verification =
lookup_user_email_verification_code(&mut txn, email, &form.code, Duration::hours(8))

View File

@ -315,18 +315,66 @@ impl From<Option<PostAuthAction>> for Register {
}
}
/// `GET /account/emails/verify/:id`
/// `GET|POST /account/emails/verify/:id`
#[derive(Debug, Clone)]
pub struct AccountVerifyEmail(pub i64);
pub struct AccountVerifyEmail {
id: i64,
post_auth_action: Option<PostAuthAction>,
}
impl AccountVerifyEmail {
#[must_use]
pub fn new(id: i64) -> Self {
Self {
id,
post_auth_action: None,
}
}
#[must_use]
pub fn and_then(mut self, action: PostAuthAction) -> Self {
self.post_auth_action = Some(action);
self
}
}
impl Route for AccountVerifyEmail {
type Query = ();
type Query = PostAuthAction;
fn route() -> &'static str {
"/account/emails/verify/:id"
}
fn path(&self) -> std::borrow::Cow<'static, str> {
format!("/account/emails/verify/{}", self.0).into()
format!("/account/emails/verify/{}", self.id).into()
}
fn query(&self) -> Option<&Self::Query> {
self.post_auth_action.as_ref()
}
}
/// `GET /account/emails/add`
#[derive(Default, Debug, Clone)]
pub struct AccountAddEmail {
post_auth_action: Option<PostAuthAction>,
}
impl Route for AccountAddEmail {
type Query = PostAuthAction;
fn route() -> &'static str {
"/account/emails/add"
}
fn query(&self) -> Option<&Self::Query> {
self.post_auth_action.as_ref()
}
}
impl AccountAddEmail {
#[must_use]
pub fn and_then(mut self, action: PostAuthAction) -> Self {
self.post_auth_action = Some(action);
self
}
}

View File

@ -641,6 +641,12 @@ impl EmailVerificationPageContext {
email: email.into(),
}
}
/// Set the form state
#[must_use]
pub fn with_form_state(self, form: FormState<EmailVerificationFormField>) -> Self {
Self { form, ..self }
}
}
impl TemplateContext for EmailVerificationPageContext {
@ -662,6 +668,51 @@ impl TemplateContext for EmailVerificationPageContext {
}
}
/// Fields of the account email add form
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EmailAddFormField {
/// The email
Email,
}
impl FormField for EmailAddFormField {
fn keep(&self) -> bool {
match self {
Self::Email => true,
}
}
}
/// Context used by the `pages/account/verify.html` templates
#[derive(Serialize, Default)]
pub struct EmailAddContext {
form: FormState<EmailAddFormField>,
}
impl EmailAddContext {
/// Constructs a context for the email add page
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Set the form state
#[must_use]
pub fn with_form_state(form: FormState<EmailAddFormField>) -> Self {
Self { form }
}
}
impl TemplateContext for EmailAddContext {
fn sample() -> Vec<Self>
where
Self: Sized,
{
vec![Self::default()]
}
}
/// Context used by the `form_post.html` template
#[derive(Serialize)]
pub struct FormPostContext<T> {

View File

@ -45,7 +45,7 @@ mod macros;
pub use self::{
context::{
AccountContext, AccountEmailsContext, CompatSsoContext, ConsentContext,
AccountContext, AccountEmailsContext, CompatSsoContext, ConsentContext, EmailAddContext,
EmailVerificationContext, EmailVerificationPageContext, EmptyContext, ErrorContext,
FormPostContext, IndexContext, LoginContext, LoginFormField, PostAuthContext,
ReauthContext, ReauthFormField, RegisterContext, RegisterFormField, TemplateContext,
@ -312,7 +312,13 @@ register_templates! {
pub fn render_account_password(WithCsrf<WithSession<EmptyContext>>) { "pages/account/password.html" }
/// Render the emails management
pub fn render_account_emails<T: StorageBackend>(WithCsrf<WithSession<AccountEmailsContext<T>>>) { "pages/account/emails.html" }
pub fn render_account_emails<T: StorageBackend>(WithCsrf<WithSession<AccountEmailsContext<T>>>) { "pages/account/emails/index.html" }
/// Render the email verification page
pub fn render_account_verify_email(WithCsrf<WithSession<EmailVerificationPageContext>>) { "pages/account/emails/verify.html" }
/// Render the email verification page
pub fn render_account_add_email(WithCsrf<WithSession<EmailAddContext>>) { "pages/account/emails/add.html" }
/// Render the re-authentication form
pub fn render_reauth(WithCsrf<WithSession<ReauthContext>>) { "pages/reauth.html" }
@ -331,9 +337,6 @@ register_templates! {
/// Render the email verification subject
pub fn render_email_verification_subject(EmailVerificationContext) { "emails/verification.subject" }
/// Render the email post-email verification page
pub fn render_email_verification_form(WithCsrf<WithSession<EmailVerificationPageContext>>) { "pages/account/verify.html" }
}
impl Templates {
@ -348,13 +351,14 @@ impl Templates {
check::render_account_index(self).await?;
check::render_account_password(self).await?;
check::render_account_emails::<()>(self).await?;
check::render_account_add_email(self).await?;
check::render_account_verify_email(self).await?;
check::render_reauth(self).await?;
check::render_form_post::<EmptyContext>(self).await?;
check::render_error(self).await?;
check::render_email_verification_txt(self).await?;
check::render_email_verification_html(self).await?;
check::render_email_verification_subject(self).await?;
check::render_email_verification_form(self).await?;
Ok(())
}
}

View File

@ -0,0 +1,39 @@
{#
Copyright 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.
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 %}
{{ navbar::top() }}
<section class="flex items-center justify-center flex-1">
<form method="POST" class="grid grid-cols-1 gap-6 w-96 m-2">
<div class="text-center">
<h1 class="text-lg text-center font-medium">Add an email address</h1>
</div>
{% if form.errors is not empty %}
{% for error in form.errors %}
<div class="text-alert font-medium">
{{ errors::form_error_message(error=error) }}
</div>
{% endfor %}
{% endif %}
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ field::input(label="Email", name="email", type="email", form_state=form, autocomplete="email") }}
{{ button::button(text="Next") }}
</section>
{% endblock content %}

View File

@ -38,5 +38,3 @@ limitations under the License.
{{ button::button(text="Submit") }}
</section>
{% endblock content %}