Commit 8517736e authored by Szymon Zimnowoda's avatar Szymon Zimnowoda
Browse files

added resend email endpoint

parent 9114bdd4
Showing with 305 additions and 35 deletions
+305 -35
......@@ -151,6 +151,16 @@ Create the Database in the POD, note the `owner` and `database` key requirements
This is the first step of POD Account registration. A mail shall be send to `login` address with the
unique code that needs to be provided in `v4/account/verify` endpoint
### POST /v4/account/resend_mail
```json
{
"login": "valid email",
"password": "string"
}
```
This endpoint allows to re-send a verification mail, valid login and password has to be provided.
New verification code is generated upon each call to that endpoint, but only last one is valid.
### POST /v4/account/verify
```json
{
......
......@@ -7,9 +7,10 @@ use crate::{
database_pool::{get_db_connection, remove_db, InitDb},
db_model::ItemBase,
error::Result,
file_api, forbidden, internal_error,
file_api, internal_error,
plugin_auth_crypto::DatabaseKey,
schema::{Schema, SchemaPropertyType},
unauthorized,
};
use lazy_static::lazy_static;
use rusqlite::types::ValueRef;
......@@ -360,7 +361,7 @@ pub fn check_owner(possible_owner: &str) -> Result<()> {
possible_owner,
hex::encode(possible_hash)
);
Err(forbidden! {
Err(unauthorized! {
"Unexpected owner",
})
}
......
......@@ -108,7 +108,6 @@ pub struct Oauth2Flow {
pub enum RegisterState {
VerifyEmailSent,
RegistrationComplete,
EnforcePasswordReset,
}
pub const POD_ACCOUNT: &str = "PodUserAccount";
......
......@@ -49,7 +49,7 @@ macro_rules! bad_request {
/// Shorthand for creating errors caused unauthorized access
#[macro_export]
macro_rules! forbidden {
macro_rules! unauthorized {
($($arg:tt)*) => {
$crate::error::Error {
context: None,
......
......@@ -11,7 +11,7 @@ use secrecy::{ExposeSecret, Secret, SecretString};
use serde_json::json;
use sha2::{Digest, Sha256};
use tokio::join;
use tracing::{debug, info, instrument};
use tracing::{debug, error, info, instrument};
use crate::api_model::{
AuthKey, ClientAuth, CreateItem, PodCredentials, PodOwner, RecoverRequest, RegisterResponse,
......@@ -29,9 +29,9 @@ use crate::error::{ErrorContext, Result};
use crate::plugin_auth_crypto::auth_to_database_key;
use crate::{
bad_request, database_api, database_utils, internal_api, internal_error, shared_state,
unauthorized,
};
#[instrument(fields(login=%body.login), skip_all)]
pub async fn register(
cli: &CliOptions,
init_db: &InitDb,
......@@ -39,10 +39,19 @@ pub async fn register(
) -> Result<RegisterResponse> {
let mut conn = shared_state::db_connection(init_db).await?;
if let Some(acc) = get_account_from_db(&mut conn, &body.login).await? {
Err(bad_request!(
"Account already exists, status {:?}",
error!(
"Trying to register on already existing account, state: {:?}",
acc.item.state
))
);
match acc.item.state {
RegisterState::VerifyEmailSent => Err(bad_request!(
"This email is already registered. Please verify the email using the code sent to your inbox."),
),
RegisterState::RegistrationComplete => Err(bad_request!(
"This email is already registered. Please log in."
)),
}
} else {
let mut rnd = rand::thread_rng();
......@@ -152,7 +161,62 @@ pub async fn verify(init_db: &InitDb, body: &RegisterVerifyAccountReq) -> Result
acc.item.state = RegisterState::RegistrationComplete;
update_account_in_db(&mut conn, &acc).await
} else {
Err(bad_request!("Invalid token provided"))
debug!(
"Invalid token provided '{}' vs '{}'",
body.code.expose_secret(),
acc.item.code
);
Err(unauthorized!("Invalid token provided"))
}
}
#[instrument(fields(login=%body.login), skip_all)]
pub async fn resend_verification_mail(
cli: &CliOptions,
init_db: &InitDb,
body: &UserAccountCredentials,
) -> Result<()> {
let mut conn = shared_state::db_connection(init_db).await?;
if let Some(mut acc) = get_account_from_db(&mut conn, &body.login).await? {
if acc.item.state != RegisterState::VerifyEmailSent {
return Err(bad_request!("Account is in state {:?}", acc.item.state));
}
validate_password(body.password.clone(), acc.item.password_hash.clone()).await?;
let mut rnd = rand::thread_rng();
let code: Vec<u32> = (0..6).map(|_| rnd.gen_range(0..10)).collect();
let code_str = format!(
"{}{}{}{}{}{}",
code[0], code[1], code[2], code[3], code[4], code[5]
);
let sending_mail = Instant::now();
let message = create_verification_mail_body(
&body.login,
"Verify your account in Memri",
cli,
&code.try_into().unwrap(),
)?;
send_email(message, cli).await?;
debug!(
"RE-Sending mail took {}ms",
sending_mail.elapsed().as_millis()
);
// TODO: there is obvious race condition with parallel requests for the same
// account - ensure 1 request at a time happens
// there is race condition with /verify too
// create a lock in a hash map, while trying to lock and fail -> return 409
// when verification completes, remove the lock from the hashmap
acc.item.code = code_str;
update_account_in_db(&mut conn, &acc).await?;
Ok(())
} else {
Err(bad_request!("No such account {}", body.login))
}
}
......@@ -166,11 +230,15 @@ pub async fn get_pod_keys(
let mut conn = shared_state::db_connection(init_db).await?;
let Some(acc) = get_account_from_db(&mut conn, &body.login).await? else {
return Err(bad_request!("Account does not exist"));
return Err(bad_request!("There is no account registered with this email. Please check your email or create an account."));
};
if acc.item.state != RegisterState::RegistrationComplete {
return Err(bad_request!("Account is not verified"));
error!(
"Account is not yet fully registered, state {:?}",
acc.item.state
);
return Err(bad_request!("This email is already registered. Please verify the email using the code sent to your inbox."));
}
validate_password(body.password.clone(), acc.item.password_hash.clone()).await?;
......@@ -229,7 +297,12 @@ async fn validate_password(password: Secret<String>, expected_hash: String) -> R
password.expose_secret().as_bytes(),
&PasswordHash::new(&expected_hash).expect("Invalid PHC string format"),
)
.map_err(|e| bad_request!("Invalid password {e}"))?;
.map_err(|e| match e {
argon2::password_hash::Error::Password => {
unauthorized!("Oops, wrong password! Please try again.")
}
_ => internal_error!("Error during password validation: {e}"),
})?;
Ok(())
})
.await?
......
use crate::actix_endpoints::{
account, account_derive_pod_keys, account_open_example_pod, account_recover_pod_keys,
account_register, account_verify, bulk, create_edge, create_item, delete_edge_by_source_target,
delete_item, delete_pod, get_edges, get_file, get_item, get_logs, graphql, not_found,
oauth1_access_token, oauth1_request_token, oauth2_access_token, oauth2_auth_url,
oauth2_authorize, open_pod, plugin_api, plugin_api_call, plugin_attach, plugins_status, search,
send_email, trace_filter, trigger_status, update_item, upload_file, upload_file_b, version,
account_register, account_send_verification_mail, account_verify, bulk, create_edge,
create_item, delete_edge_by_source_target, delete_item, delete_pod, get_edges, get_file,
get_item, get_logs, graphql, not_found, oauth1_access_token, oauth1_request_token,
oauth2_access_token, oauth2_auth_url, oauth2_authorize, open_pod, plugin_api, plugin_api_call,
plugin_attach, plugins_status, search, send_email, trace_filter, trigger_status, update_item,
upload_file, upload_file_b, version,
};
use actix_cors::Cors;
......@@ -127,6 +128,7 @@ async fn run_server_with_dependencies<S: 'static>(
.service(account_register)
.service(account_recover_pod_keys)
.service(account_verify)
.service(account_send_verification_mail)
.service(account_derive_pod_keys)
.service(account_open_example_pod),
);
......
......@@ -269,6 +269,20 @@ pub async fn account_verify(
respond_with_result(result)
}
#[instrument(fields(uid=trace_uid()), skip_all)]
#[post("/account/resend_mail")]
pub async fn account_send_verification_mail(
cli: web::Data<CliOptions>,
init_db: web::Data<InitDb>,
body: web::Bytes,
) -> actix_web::Result<impl Responder> {
let body = extract_json(&body)?;
let result =
pod_handlers::account_send_verification_mail(&cli, &init_db.into_inner(), body).await;
let result = result.map(|result| web::Json(serde_json::json!(result)));
respond_with_result(result)
}
#[instrument(fields(uid=trace_uid()), skip_all)]
#[post("/account/pod/derive_keys")]
pub async fn account_derive_pod_keys(
......@@ -547,8 +561,8 @@ pub fn respond_with_result<T: Serialize>(result: Result<T>) -> actix_web::Result
| libpod::error::ErrorType::BadRequest(_) => StatusCode::BAD_REQUEST,
libpod::error::ErrorType::Any { code, msg: _ } => *code,
libpod::error::ErrorType::Unauthorized(_) => StatusCode::FORBIDDEN,
libpod::error::ErrorType::UnauthorizedDatabaseAccess(_)
libpod::error::ErrorType::Unauthorized(_)
| libpod::error::ErrorType::UnauthorizedDatabaseAccess(_)
| libpod::error::ErrorType::AeadEncryption(_) => StatusCode::UNAUTHORIZED,
libpod::error::ErrorType::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
......
......@@ -263,6 +263,17 @@ pub async fn account_verify(init_db: &InitDb, body: RegisterVerifyAccountReq) ->
user_account::verify(init_db, &body).await
}
#[inline(always)]
pub async fn account_send_verification_mail(
cli: &CliOptions,
init_db: &InitDb,
body: UserAccountCredentials,
) -> Result<()> {
info!("Resend verification mail for account {:?}", body.login);
user_account::resend_verification_mail(cli, init_db, &body).await
}
#[inline(always)]
pub async fn account_derive_pod_keys(
init_db: &InitDb,
......
......@@ -5,6 +5,7 @@ use libpod::api_model::{PodCredentials, RegisterResponse};
use reqwest::StatusCode;
use secrecy::ExposeSecret;
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use std::ops::{Deref, DerefMut};
use test_context::{test_context, AsyncTestContext};
......@@ -12,13 +13,16 @@ use test_context::{test_context, AsyncTestContext};
#[tokio::test]
async fn test_account_creation_flow(ctx: &mut TestDataForCreateAccount) {
// Call register for fresh account
const LOGIN: &str = "anastasiia@example.com";
const PASS: &str = "bobx0x0";
let login_hash = hex::encode(Sha256::new_with_prefix(LOGIN.as_bytes()).finalize());
let res = ctx
.pod_client
.post_to(
json!({
"login": "anastasiia@example.com",
"login": LOGIN,
"password": PASS
}),
"account/register",
......@@ -35,7 +39,8 @@ async fn test_account_creation_flow(ctx: &mut TestDataForCreateAccount) {
.post_to_with_owner(
json!(
{
"type": "PodUserAccount"
"type": "PodUserAccount",
"loginHash": login_hash
}
),
"search",
......@@ -52,7 +57,7 @@ async fn test_account_creation_flow(ctx: &mut TestDataForCreateAccount) {
.pod_client
.post_to(
json!({
"login": "anastasiia@example.com",
"login": LOGIN,
"password": PASS
}),
"account/register",
......@@ -65,7 +70,7 @@ async fn test_account_creation_flow(ctx: &mut TestDataForCreateAccount) {
.json::<String>()
.await
.unwrap()
.contains("Account already exists"),);
.contains("This email is already registered."),);
// Login is case insensitive
let res = ctx
......@@ -85,7 +90,7 @@ async fn test_account_creation_flow(ctx: &mut TestDataForCreateAccount) {
.json::<String>()
.await
.unwrap()
.contains("Account already exists"),);
.contains("This email is already registered."),);
// Login is invalid mail
let res = ctx
......@@ -112,14 +117,14 @@ async fn test_account_creation_flow(ctx: &mut TestDataForCreateAccount) {
.pod_client
.post_to(
json!({
"login": "anastasiia@example.com",
"login": LOGIN,
"code": invalid_token
}),
"account/verify",
)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
assert!(res
.json::<String>()
......@@ -132,7 +137,7 @@ async fn test_account_creation_flow(ctx: &mut TestDataForCreateAccount) {
.pod_client
.post_to(
json!({
"login": "anastasiia@example.com",
"login": LOGIN,
"code": token
}),
"account/verify",
......@@ -147,27 +152,27 @@ async fn test_account_creation_flow(ctx: &mut TestDataForCreateAccount) {
.pod_client
.post_to(
json!({
"login": "anastasiia@example.com",
"login": LOGIN,
"password": "wrong"
}),
"account/pod/derive_keys",
)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
assert!(res
.json::<String>()
.await
.unwrap()
.contains("Invalid password"),);
.contains("Oops, wrong password! Please try again."),);
// with valid pass
let res = ctx
.pod_client
.post_to(
json!({
"login": "anastasiia@example.com",
"login": LOGIN,
"password": PASS
}),
"account/pod/derive_keys",
......@@ -194,7 +199,7 @@ async fn test_account_creation_flow(ctx: &mut TestDataForCreateAccount) {
.pod_client
.post_to(
json!({
"login": "anastasiia@example.com",
"login": LOGIN,
"recoveryPhrase": "steak oyster salt play nominee debris great identify ugly obey marble announce"
}),
"account/pod/recover",
......@@ -208,7 +213,7 @@ async fn test_account_creation_flow(ctx: &mut TestDataForCreateAccount) {
.pod_client
.post_to(
json!({
"login": "anastasiia@example.com",
"login": LOGIN,
"recoveryPhrase": register_response.recovery_phrase.expose_secret()
}),
"account/pod/recover",
......@@ -228,6 +233,161 @@ async fn test_account_creation_flow(ctx: &mut TestDataForCreateAccount) {
);
}
#[test_context(TestDataForCreateAccount)]
#[tokio::test]
async fn test_account_resend_email(ctx: &mut TestDataForCreateAccount) {
// Call register for fresh account
const LOGIN: &str = "bob@example.com";
const PASS: &str = "bobx0x0";
let login_hash = hex::encode(Sha256::new_with_prefix(LOGIN.as_bytes()).finalize());
let res = ctx
.pod_client
.post_to(
json!({
"login": LOGIN,
"password": PASS
}),
"account/register",
)
.await;
assert!(res.status().is_success());
let old_token = {
let res = ctx
.pod_client_to_shared_db
.post_to_with_owner(
json!(
{
"type": "PodUserAccount",
"loginHash": login_hash,
}
),
"search",
)
.await;
let user_account_data: Value = res.json().await.unwrap();
user_account_data[0]["code"].as_str().unwrap().to_string()
};
// re-send verification mail - generates new token
let res = ctx
.pod_client
.post_to(
json!({
"login": LOGIN,
"password": PASS
}),
"account/resend_mail",
)
.await;
assert!(
res.status().is_success(),
"The status is {}, body {:#}",
res.status(),
res.json::<Value>().await.unwrap()
);
// Verify
// with old token
let res = ctx
.pod_client
.post_to(
json!({
"login": "anastasiia@example.com",
"code": old_token
}),
"account/verify",
)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
// with valid token
let token = {
let res = ctx
.pod_client_to_shared_db
.post_to_with_owner(
json!(
{
"type": "PodUserAccount",
"loginHash": login_hash
}
),
"search",
)
.await;
let user_account_data: Value = res.json().await.unwrap();
user_account_data[0]["code"].as_str().unwrap().to_string()
};
let res = ctx
.pod_client
.post_to(
json!({
"login": LOGIN,
"code": token
}),
"account/verify",
)
.await;
assert_eq!(res.status(), StatusCode::OK);
// cannot resend mail, if account is not in verify state
let res = ctx
.pod_client
.post_to(
json!({
"login": LOGIN,
"password": PASS
}),
"account/resend_mail",
)
.await;
assert_eq!(
res.status(),
StatusCode::BAD_REQUEST,
"The status is {}, body {:#}",
res.status(),
res.json::<Value>().await.unwrap()
);
// Can generate pod keys
// with valid pass
let res = ctx
.pod_client
.post_to(
json!({
"login": LOGIN,
"password": PASS
}),
"account/pod/derive_keys",
)
.await;
assert_eq!(res.status(), StatusCode::OK);
let pod_credentials: PodCredentials = res.json().await.unwrap();
// Can open POD
let res = ctx
.pod_client
.post_to(
serde_json::to_value(pod_credentials.clone()).unwrap(),
"account/pod/open",
)
.await;
assert_eq!(res.status(), StatusCode::OK);
}
pub struct TestDataForCreateAccount(TestData);
#[async_trait::async_trait]
......
......@@ -165,5 +165,5 @@ async fn test_account_outside_allow_list(ctx: &mut TestData) {
// # 1 trying to use pod, while not yet registered
let res = use_pod(&user_outside_list).await;
assert_eq!(res.status(), StatusCode::FORBIDDEN);
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment