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

Finish implementing email verification

Fixes #30
This commit is contained in:
Quentin Gliech
2022-01-21 18:22:02 +01:00
parent 54e9dc0712
commit 7b487e184a
6 changed files with 195 additions and 30 deletions

View File

@ -13,13 +13,13 @@
// limitations under the License.
use lettre::{message::Mailbox, Address};
use mas_config::{CookiesConfig, CsrfConfig};
use mas_data_model::BrowserSession;
use mas_config::{CookiesConfig, CsrfConfig, OAuth2Config};
use mas_data_model::{BrowserSession, User, UserEmail};
use mas_email::Mailer;
use mas_storage::{
user::{
add_user_email, get_user_email, get_user_emails, remove_user_email,
set_user_email_as_primary,
add_user_email, add_user_email_verification_code, get_user_email, get_user_emails,
remove_user_email, set_user_email_as_primary,
},
PostgresqlBackend,
};
@ -34,6 +34,7 @@ use mas_warp_utils::{
with_templates, CsrfToken,
},
};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::Deserialize;
use sqlx::{pool::PoolConnection, PgExecutor, PgPool, Postgres, Transaction};
use tracing::info;
@ -44,11 +45,14 @@ pub(super) fn filter(
pool: &PgPool,
templates: &Templates,
mailer: &Mailer,
oauth2_config: &OAuth2Config,
csrf_config: &CsrfConfig,
cookies_config: &CookiesConfig,
) -> BoxedFilter<(Box<dyn Reply>,)> {
let mailer = mailer.clone();
let base = oauth2_config.issuer.clone();
let get = with_templates(templates)
.and(encrypted_cookie_saver(cookies_config))
.and(updated_csrf_token(cookies_config, csrf_config))
@ -58,6 +62,7 @@ pub(super) fn filter(
let post = with_templates(templates)
.and(warp::any().map(move || mailer.clone()))
.and(warp::any().map(move || base.clone()))
.and(encrypted_cookie_saver(cookies_config))
.and(updated_csrf_token(cookies_config, csrf_config))
.and(session(pool, cookies_config))
@ -113,9 +118,43 @@ async fn render(
Ok(Box::new(reply))
}
async fn start_email_verification(
mailer: &Mailer,
base: &Url,
executor: impl PgExecutor<'_>,
user: &User<PostgresqlBackend>,
user_email: &UserEmail<PostgresqlBackend>,
) -> anyhow::Result<()> {
// First, generate a code
let code: String = thread_rng()
.sample_iter(&Alphanumeric)
.take(32)
.map(char::from)
.collect();
add_user_email_verification_code(executor, user_email, &code).await?;
// And send the verification email
let address: Address = user_email.email.parse()?;
let mailbox = Mailbox::new(Some(user.username.clone()), address);
let link = base.join("./verify/")?;
let link = link.join(&code)?;
let context = EmailVerificationContext::new(user.clone().into(), link);
mailer.send_verification_email(mailbox, &context).await?;
info!(email.id = user_email.data, "Verification email sent");
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn post(
templates: Templates,
mailer: Mailer,
base: Url,
cookie_saver: EncryptedCookieSaver,
csrf_token: CsrfToken,
mut session: BrowserSession<PostgresqlBackend>,
@ -124,9 +163,10 @@ async fn post(
) -> Result<Box<dyn Reply>, Rejection> {
match form {
Form::Add { email } => {
// TODO: verify email format
// TODO: send verification email
add_user_email(&mut txn, &session.user, email)
let user_email = add_user_email(&mut txn, &session.user, email)
.await
.wrap_error()?;
start_email_verification(&mailer, &base, &mut txn, &session.user, &user_email)
.await
.wrap_error()?;
}
@ -140,27 +180,13 @@ async fn post(
Form::ResendConfirmation { data } => {
let id: i64 = data.parse().wrap_error()?;
let email: Address = get_user_email(&mut txn, &session.user, id)
.await
.wrap_error()?
.email
.parse()
.wrap_error()?;
let mailbox = Mailbox::new(Some(session.user.username.clone()), email);
// TODO: actually generate a verification link
let context = EmailVerificationContext::new(
session.user.clone().into(),
Url::parse("https://example.com/verify").unwrap(),
);
mailer
.send_verification_email(mailbox, &context)
let user_email = get_user_email(&mut txn, &session.user, id)
.await
.wrap_error()?;
info!(email.id = id, "Verification email sent");
start_email_verification(&mailer, &base, &mut txn, &session.user, &user_email)
.await
.wrap_error()?;
}
Form::SetPrimary { data } => {
let id = data.parse().wrap_error()?;

View File

@ -15,7 +15,7 @@
mod emails;
mod password;
use mas_config::{CookiesConfig, CsrfConfig};
use mas_config::{CookiesConfig, CsrfConfig, OAuth2Config};
use mas_data_model::BrowserSession;
use mas_email::Mailer;
use mas_storage::{
@ -42,6 +42,7 @@ pub(super) fn filter(
pool: &PgPool,
templates: &Templates,
mailer: &Mailer,
oauth2_config: &OAuth2Config,
csrf_config: &CsrfConfig,
cookies_config: &CookiesConfig,
) -> BoxedFilter<(Box<dyn Reply>,)> {
@ -55,7 +56,14 @@ pub(super) fn filter(
let index = warp::path::end().and(get);
let password = password(pool, templates, csrf_config, cookies_config);
let emails = emails(pool, templates, mailer, csrf_config, cookies_config);
let emails = emails(
pool,
templates,
mailer,
oauth2_config,
csrf_config,
cookies_config,
);
let filter = index.or(password).unify().or(emails).unify();

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.
@ -25,10 +25,12 @@ mod logout;
mod reauth;
mod register;
mod shared;
mod verify;
use self::{
account::filter as account, index::filter as index, login::filter as login,
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,
@ -43,11 +45,19 @@ pub(super) fn filter(
cookies_config: &CookiesConfig,
) -> BoxedFilter<(Box<dyn Reply>,)> {
let index = index(pool, templates, oauth2_config, csrf_config, cookies_config);
let account = account(pool, templates, mailer, csrf_config, cookies_config);
let account = account(
pool,
templates,
mailer,
oauth2_config,
csrf_config,
cookies_config,
);
let login = login(pool, templates, csrf_config, cookies_config);
let register = register(pool, templates, csrf_config, cookies_config);
let logout = logout(pool, cookies_config);
let reauth = reauth(pool, templates, csrf_config, cookies_config);
let verify = verify(pool, templates, csrf_config, cookies_config);
index
.or(account)
@ -60,5 +70,7 @@ pub(super) fn filter(
.unify()
.or(reauth)
.unify()
.or(verify)
.unify()
.boxed()
}

View File

@ -0,0 +1,90 @@
// 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 chrono::Duration;
use mas_config::{CookiesConfig, CsrfConfig};
use mas_data_model::BrowserSession;
use mas_storage::{
user::{
consume_email_verification, lookup_user_email_verification_code,
mark_user_email_as_verified,
},
PostgresqlBackend,
};
use mas_templates::{EmptyContext, TemplateContext, Templates};
use mas_warp_utils::{
errors::WrapError,
filters::{
cookies::{encrypted_cookie_saver, EncryptedCookieSaver},
csrf::updated_csrf_token,
database::transaction,
session::optional_session,
with_templates, CsrfToken,
},
};
use sqlx::{PgPool, Postgres, Transaction};
use warp::{filters::BoxedFilter, reply::html, Filter, Rejection, Reply};
pub(super) fn filter(
pool: &PgPool,
templates: &Templates,
csrf_config: &CsrfConfig,
cookies_config: &CookiesConfig,
) -> BoxedFilter<(Box<dyn Reply>,)> {
warp::path!("verify" / String)
.and(warp::get())
.and(with_templates(templates))
.and(encrypted_cookie_saver(cookies_config))
.and(updated_csrf_token(cookies_config, csrf_config))
.and(optional_session(pool, cookies_config))
.and(transaction(pool))
.and_then(get)
.boxed()
}
async fn get(
code: String,
templates: Templates,
cookie_saver: EncryptedCookieSaver,
csrf_token: CsrfToken,
maybe_session: Option<BrowserSession<PostgresqlBackend>>,
mut txn: Transaction<'_, Postgres>,
) -> Result<Box<dyn Reply>, Rejection> {
// TODO: make those 8 hours configurable
let verification = lookup_user_email_verification_code(&mut txn, &code, Duration::hours(8))
.await
.wrap_error()?;
// TODO: display nice errors if the code was already consumed or expired
let verification = consume_email_verification(&mut txn, verification)
.await
.wrap_error()?;
let _email = mark_user_email_as_verified(&mut txn, verification.email)
.await
.wrap_error()?;
let ctx = EmptyContext
.maybe_with_session(maybe_session)
.with_csrf(csrf_token.form_value());
let content = templates.render_email_verification_done(&ctx).await?;
let reply = html(content);
let reply = cookie_saver.save_encrypted(&csrf_token, reply)?;
txn.commit().await.wrap_error()?;
Ok(Box::new(reply))
}

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.
@ -318,6 +318,9 @@ register_templates! {
/// Render the email verification email (plain text variant)
pub fn render_email_verification_html(EmailVerificationContext) { "emails/verification.html" }
/// Render the email post-email verification page
pub fn render_email_verification_done(WithCsrf<WithOptionalSession<EmptyContext>>) { "pages/verify.html" }
}
impl Templates {
@ -335,6 +338,7 @@ impl Templates {
check::render_error(self).await?;
check::render_email_verification_txt(self).await?;
check::render_email_verification_html(self).await?;
check::render_email_verification_done(self).await?;
Ok(())
}
}

View File

@ -0,0 +1,25 @@
{#
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 %}
<section class="flex items-center justify-center flex-1">
<p class="font-bold text-xl">Email verified!</p>
</section>
{% endblock content %}