Skip to content

Commit

Permalink
OTP via email. (#12)
Browse files Browse the repository at this point in the history
Feature to send an OTP code via email.
  • Loading branch information
9876691 authored Jun 14, 2021
1 parent 01f2300 commit 425d52f
Show file tree
Hide file tree
Showing 10 changed files with 266 additions and 69 deletions.
17 changes: 11 additions & 6 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ services:
- NODE_MAX_INSTANCES=4
- NODE_MAX_SESSION=4

# MailHog is an email testing tool for developers.
smtp:
image: mailhog/mailhog
ports:
- "1025:1025"
- "8025:8025"

# Devconatiner development
development:
build:
Expand All @@ -52,14 +59,11 @@ services:

environment:
DATABASE_URL: postgresql://vscode:testpassword@db:5432
# To capture emails use
# sudo apt-get update
# sudo apt-get install -y postfix
# smtp-sink -c -d "tmp/%M." 2525 1000
SMTP_HOST: localhost
SMTP_PORT: 2525
SMTP_HOST: smtp
SMTP_PORT: 1025
SMTP_USERNAME: thisisnotused
SMTP_PASSWORD: thisisnotused
SMTP_TLS_OFF: 'true'
RESET_DOMAIN: http://localhost:9095
RESET_FROM_EMAIL_ADDRESS: [email protected]
PORT: 9095
Expand All @@ -68,6 +72,7 @@ services:
FORWARD_URL: whoami
FORWARD_PORT: 80
REDIRECT_URL: /
ENABLE_EMAIL_OTP: 'true'

working_dir: /vscode

Expand Down
4 changes: 4 additions & 0 deletions migrations/2021-06-10-132403_add_otp/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ALTER TABLE sessions DROP COLUMN otp_code;
ALTER TABLE sessions DROP COLUMN otp_code_confirmed;
ALTER TABLE sessions DROP COLUMN otp_code_sent;
ALTER TABLE sessions DROP COLUMN otp_code_attempts;
4 changes: 4 additions & 0 deletions migrations/2021-06-10-132403_add_otp/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ALTER TABLE sessions ADD otp_code INTEGER NOT NULL DEFAULT (random() * 100000 + 1)::int;
ALTER TABLE sessions ADD otp_code_attempts INTEGER NOT NULL DEFAULT 0;
ALTER TABLE sessions ADD otp_code_confirmed BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE sessions ADD otp_code_sent BOOLEAN NOT NULL DEFAULT false;
149 changes: 149 additions & 0 deletions src/auth/email_otp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use crate::components::forms;
use crate::config;
use crate::custom_error::CustomError;
use crate::layouts;
use actix_web::{http, web, HttpResponse, Result};
use lettre::Message;
use serde::{Deserialize, Serialize};
use sqlx::{types::Uuid, PgPool};
use std::default::Default;
use validator::ValidationErrors;

#[derive(Serialize, Deserialize, Default)]
pub struct Otp {
pub code: i32,
#[serde(rename = "h-captcha-response")]
pub h_captcha_response: Option<String>,
}

#[derive(sqlx::FromRow, Debug)]
pub struct Session {
otp_code: i32,
otp_code_attempts: i32,
otp_code_sent: bool,
}

pub async fn email_otp(
config: web::Data<config::Config>,
session: Option<crate::Session>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, CustomError> {
if let Some(session) = session {
if let Ok(uuid) = Uuid::parse_str(&session.session_uuid) {
let db_session: Session = sqlx::query_as::<_, Session>(
"
SELECT otp_code, otp_code_attempts, otp_code_sent FROM sessions WHERE session_uuid = $1
",
)
.bind(uuid)
.fetch_one(pool.get_ref()) // -> Vec<Person>
.await?;

if !db_session.otp_code_sent {
sqlx::query(
"
UPDATE sessions SET otp_code_sent = true WHERE session_uuid = $1
",
)
.bind(uuid)
.execute(pool.get_ref())
.await?;

if let Some(smtp_config) = &config.smtp_config {
let email = Message::builder()
.from(smtp_config.from_email.clone())
.to("[email protected]".parse().unwrap())
.subject("Your confirmation code")
.body(
format!(
"
Your confirmation code is {}
",
db_session.otp_code
)
.trim()
.to_string(),
)
.unwrap();

crate::email::send_email(&config, email)
}
}
}
}

let body = OtpPage {
form: &Otp::default(),
errors: &ValidationErrors::default(),
};

Ok(layouts::session_layout("Login", &body.to_string()))
}

pub async fn process_otp(
config: web::Data<config::Config>,
pool: web::Data<PgPool>,
session: Option<crate::Session>,
form: web::Form<Otp>,
) -> Result<HttpResponse, CustomError> {
if let Some(session) = session {
if let Ok(uuid) = Uuid::parse_str(&session.session_uuid) {
if super::verify_hcaptcha(&config.hcaptcha_config, &form.h_captcha_response).await {
let db_session: Session = sqlx::query_as::<_, Session>(
"
SELECT otp_code, otp_code_attempts, otp_code_sent FROM sessions WHERE session_uuid = $1
",
)
.bind(uuid)
.fetch_one(pool.get_ref()) // -> Vec<Person>
.await?;

if db_session.otp_code == form.code {
sqlx::query(
"
UPDATE sessions SET otp_code_confirmed = true WHERE session_uuid = $1
",
)
.bind(uuid)
.execute(pool.get_ref())
.await?;

return Ok(HttpResponse::SeeOther()
.append_header((http::header::LOCATION, config.redirect_url.clone()))
.finish());
} else {
sqlx::query(
"
UPDATE sessions SET otp_code_attempts = otp_code_attempts + 1 WHERE session_uuid = $1
"
)
.bind(uuid )
.execute(pool.get_ref())
.await?;

return Ok(HttpResponse::SeeOther()
.append_header((http::header::LOCATION, crate::EMAIL_OTP_URL))
.finish());
}
}
}
}

return Ok(HttpResponse::SeeOther()
.append_header((http::header::LOCATION, crate::SIGN_IN_URL))
.finish());
}

markup::define! {
OtpPage<'a>(form: &'a Otp,
errors: &'a ValidationErrors) {
form.m_authentication[method = "post"] {

h1 { "Password Reset Request" }

@forms::NumberInput{ title: "Code", name: "code", value: &form.code.to_string(), help_text: "", errors }

button.a_button.success[type = "submit"] { "Submit Code" }
}
}
}
6 changes: 6 additions & 0 deletions src/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod change_password;
mod email_otp;
pub mod login;
mod registration;
mod reset_request;
Expand Down Expand Up @@ -104,4 +105,9 @@ pub fn routes(cfg: &mut web::ServiceConfig) {
.route(web::get().to(change_password::change_password))
.route(web::post().to(change_password::process_change)),
);
cfg.service(
web::resource(crate::EMAIL_OTP_URL)
.route(web::get().to(email_otp::email_otp))
.route(web::post().to(email_otp::process_otp)),
);
}
50 changes: 19 additions & 31 deletions src/auth/reset_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ use crate::config;
use crate::custom_error::CustomError;
use crate::layouts;
use actix_web::{http, web, HttpResponse, Result};
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};
use lettre::Message;
use serde::{Deserialize, Serialize};
use sqlx::{types::Uuid, PgPool};
use std::borrow::Cow;
Expand Down Expand Up @@ -58,37 +57,26 @@ pub async fn process_request(
.await?;

if let Some(smtp_config) = &config.smtp_config {
if let Some(rest_config) = &config.reset_config {
if users.len() == 1 {
let email = Message::builder()
.from(rest_config.from_email.clone())
.to(users[0].email.parse().unwrap())
.subject("Did you request a password reset?")
.body(format!(
if users.len() == 1 {
let email = Message::builder()
.from(smtp_config.from_email.clone())
.to(users[0].email.parse().unwrap())
.subject("Did you request a password reset?")
.body(
format!(
"
If you requested a password reset please follow this link {}/auth/change_password/{}
",
rest_config.domain,
If you requested a password reset please follow this link
\n{}/auth/change_password/{}
",
smtp_config.domain,
users[0].reset_password_token.to_string()
).trim().to_string())
.unwrap();

let creds = Credentials::new(
smtp_config.username.clone(),
smtp_config.password.clone(),
);

let sender = SmtpTransport::builder_dangerous(smtp_config.host.clone())
.port(smtp_config.port)
.credentials(creds)
.build();
sender.send(&email).unwrap();
// Send the email
match sender.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
}
)
.trim()
.to_string(),
)
.unwrap();

crate::email::send_email(&config, email)
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/components/forms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ markup::define! {
EmailInput<'a>(title: &'a str, name: &'a str, value: &'a str, help_text: &'a str, errors: &'a ValidationErrors) {
{ Input{ title, name, value, input_type: "email", help_text, errors } }
}
NumberInput<'a>(title: &'a str, name: &'a str, value: &'a str, help_text: &'a str, errors: &'a ValidationErrors) {
{ Input{ title, name, value, input_type: "number", help_text, errors } }
}
TextInput<'a>(title: &'a str, name: &'a str, value: &'a str, help_text: &'a str, errors: &'a ValidationErrors) {
{ Input{ title, name, value, input_type: "text", help_text, errors } }
}
Expand Down
61 changes: 30 additions & 31 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,45 +20,36 @@ pub struct SmtpConfig {
// Configure SMTP for email.
pub host: String,
pub port: u16,
pub tls_off: bool,
pub username: String,
pub password: String,
}

#[derive(Clone, Debug)]
pub struct ResetEmailConfig {
pub domain: String,
pub from_email: message::Mailbox,
}

impl ResetEmailConfig {
pub fn new() -> Option<ResetEmailConfig> {
if let Ok(domain) = env::var("RESET_DOMAIN") {
if let Ok(from_email) = env::var("RESET_FROM_EMAIL_ADDRESS") {
Some(ResetEmailConfig {
domain,
from_email: from_email.parse().unwrap(),
})
} else {
None
}
} else {
None
}
}
}

impl SmtpConfig {
pub fn new() -> Option<SmtpConfig> {
if let Ok(host) = env::var("SMTP_HOST") {
if let Ok(username) = env::var("SMTP_USERNAME") {
if let Ok(password) = env::var("SMTP_PASSWORD") {
if let Ok(smtp_port) = env::var("SMTP_PORT") {
Some(SmtpConfig {
host,
port: smtp_port.parse::<u16>().unwrap(),
username,
password,
})
if let Ok(domain) = env::var("RESET_DOMAIN") {
if let Ok(from_email) = env::var("RESET_FROM_EMAIL_ADDRESS") {
Some(SmtpConfig {
host,
port: smtp_port.parse::<u16>().unwrap(),
tls_off: env::var("SMTP_TLS_OFF").is_ok(),
username,
password,
domain,
from_email: from_email.parse().unwrap(),
})
} else {
None
}
} else {
None
}
} else {
None
}
Expand Down Expand Up @@ -95,11 +86,10 @@ pub struct Config {
pub skip_auth_for: Vec<String>,
pub hcaptcha_config: Option<HCaptchaConfig>,

pub email_otp_enabled: bool,

// Configure SMTP for email.
pub smtp_config: Option<SmtpConfig>,

// Reset email details
pub reset_config: Option<ResetEmailConfig>,
}

impl Config {
Expand Down Expand Up @@ -137,6 +127,15 @@ impl Config {
9090
};

let email_otp_enabled: bool = if env::var("ENABLE_EMAIL_OTP").is_ok() {
env::var("ENABLE_EMAIL_OTP")
.unwrap()
.parse::<bool>()
.unwrap()
} else {
false
};

let auth_type: AuthType = if env::var("AUTH_TYPE").is_ok() {
let t = env::var("AUTH_TYPE").unwrap();
if t.to_lowercase() == "bip38" {
Expand Down Expand Up @@ -167,8 +166,8 @@ impl Config {
forward_url,
skip_auth_for,
hcaptcha_config: None,
email_otp_enabled,
smtp_config: SmtpConfig::new(),
reset_config: ResetEmailConfig::new(),
}
}
}
Expand Down
Loading

0 comments on commit 425d52f

Please sign in to comment.