1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-31 09:24:31 +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))
}