This commit is contained in:
Edward Shen 2021-08-20 21:47:14 -04:00
parent f08b3e0bcb
commit 6a1cb04428
Signed by: edward
GPG key ID: 19182661E818369F
13 changed files with 282 additions and 208 deletions

163
Cargo.lock generated
View file

@ -25,7 +25,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dddf0af1514897dc80a11a084aa032ec7c1c042af454f502aa81c49af0a25fb" checksum = "9dddf0af1514897dc80a11a084aa032ec7c1c042af454f502aa81c49af0a25fb"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"base64 0.13.0", "base64",
"cookie", "cookie",
"rand", "rand",
"serde", "serde",
@ -66,7 +66,7 @@ dependencies = [
"actix-tls", "actix-tls",
"actix-utils", "actix-utils",
"ahash", "ahash",
"base64 0.13.0", "base64",
"bitflags", "bitflags",
"brotli2", "brotli2",
"bytes", "bytes",
@ -326,7 +326,7 @@ checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98"
dependencies = [ dependencies = [
"getrandom", "getrandom",
"once_cell", "once_cell",
"version_check 0.9.3", "version_check",
] ]
[[package]] [[package]]
@ -359,18 +359,23 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "ascii_utils"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
[[package]] [[package]]
name = "askama_escape" name = "askama_escape"
version = "0.10.1" version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90c108c1a94380c89d2215d0ac54ce09796823cca0fd91b299cfff3b33e346fb" checksum = "90c108c1a94380c89d2215d0ac54ce09796823cca0fd91b299cfff3b33e346fb"
[[package]]
name = "async-trait"
version = "0.1.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "atoi" name = "atoi"
version = "0.4.0" version = "0.4.0"
@ -392,15 +397,6 @@ version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
[[package]]
name = "base64"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e"
dependencies = [
"byteorder",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.13.0" version = "0.13.0"
@ -475,12 +471,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "bufstream"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8"
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.7.0" version = "3.7.0"
@ -569,7 +559,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d" checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"base64 0.13.0", "base64",
"hkdf", "hkdf",
"hmac", "hmac",
"percent-encoding", "percent-encoding",
@ -577,7 +567,7 @@ dependencies = [
"sha2", "sha2",
"subtle", "subtle",
"time", "time",
"version_check 0.9.3", "version_check",
] ]
[[package]] [[package]]
@ -761,12 +751,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
[[package]] [[package]]
name = "fast_chemail" name = "fastrand"
version = "0.9.6" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" checksum = "b394ed3d285a429378d3b384b9eb1285267e7df4b166df24b7a6939a04dc392e"
dependencies = [ dependencies = [
"ascii_utils", "instant",
] ]
[[package]] [[package]]
@ -939,7 +929,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
dependencies = [ dependencies = [
"typenum", "typenum",
"version_check 0.9.3", "version_check",
] ]
[[package]] [[package]]
@ -1061,12 +1051,13 @@ dependencies = [
[[package]] [[package]]
name = "hostname" name = "hostname"
version = "0.1.5" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e" checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
dependencies = [ dependencies = [
"libc", "libc",
"winutil", "match_cfg",
"winapi",
] ]
[[package]] [[package]]
@ -1092,6 +1083,12 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
[[package]]
name = "httpdate"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.2.3" version = "0.2.3"
@ -1160,20 +1157,31 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "lettre" name = "lettre"
version = "0.9.6" version = "0.10.0-rc.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86ed8677138975b573ab4949c35613931a4addeadd0a8a6aa0327e2a979660de" checksum = "d8697ded52353bdd6fec234b3135972433397e86d0493d9fc38fbf407b7c106a"
dependencies = [ dependencies = [
"base64 0.10.1", "async-trait",
"bufstream", "base64",
"fast_chemail", "fastrand",
"futures-io",
"futures-util",
"hostname", "hostname",
"log", "httpdate",
"idna",
"mime",
"native-tls", "native-tls",
"nom 4.2.3", "nom",
"once_cell",
"quoted_printable",
"r2d2",
"regex",
"rustls",
"serde", "serde",
"serde_derive", "tokio",
"serde_json", "tokio-rustls",
"webpki",
"webpki-roots",
] ]
[[package]] [[package]]
@ -1260,6 +1268,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "match_cfg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]] [[package]]
name = "matchers" name = "matchers"
version = "0.0.1" version = "0.0.1"
@ -1347,16 +1361,6 @@ dependencies = [
"tempfile", "tempfile",
] ]
[[package]]
name = "nom"
version = "4.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
dependencies = [
"memchr",
"version_check 0.1.5",
]
[[package]] [[package]]
name = "nom" name = "nom"
version = "6.1.2" version = "6.1.2"
@ -1367,7 +1371,7 @@ dependencies = [
"funty", "funty",
"lexical-core", "lexical-core",
"memchr", "memchr",
"version_check 0.9.3", "version_check",
] ]
[[package]] [[package]]
@ -1630,6 +1634,23 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "quoted_printable"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1238256b09923649ec89b08104c4dfe9f6cb2fea734a5db5384e44916d59e9c5"
[[package]]
name = "r2d2"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f"
dependencies = [
"log",
"parking_lot",
"scheduled-thread-pool",
]
[[package]] [[package]]
name = "radium" name = "radium"
version = "0.5.3" version = "0.5.3"
@ -1759,7 +1780,7 @@ version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7"
dependencies = [ dependencies = [
"base64 0.13.0", "base64",
"log", "log",
"ring", "ring",
"sct", "sct",
@ -1791,6 +1812,15 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "scheduled-thread-pool"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7"
dependencies = [
"parking_lot",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.1.0" version = "1.1.0"
@ -2023,7 +2053,7 @@ checksum = "6d86e3c77ff882a828346ba401a7ef4b8e440df804491c6064fe8295765de71c"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"maplit", "maplit",
"nom 6.1.2", "nom",
"regex", "regex",
"unicode_categories", "unicode_categories",
] ]
@ -2124,7 +2154,7 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff"
dependencies = [ dependencies = [
"version_check 0.9.3", "version_check",
] ]
[[package]] [[package]]
@ -2269,7 +2299,7 @@ dependencies = [
"standback", "standback",
"stdweb", "stdweb",
"time-macros", "time-macros",
"version_check 0.9.3", "version_check",
"winapi", "winapi",
] ]
@ -2480,7 +2510,7 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [ dependencies = [
"version_check 0.9.3", "version_check",
] ]
[[package]] [[package]]
@ -2550,12 +2580,6 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.3" version = "0.9.3"
@ -2703,15 +2727,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "winutil"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "wyz" name = "wyz"
version = "0.2.0" version = "0.2.0"

View file

@ -13,7 +13,7 @@ actix-session = "0.5.0-beta.2"
actix-web = { version = "4.0.0-beta.8", features = [ "rustls" ] } actix-web = { version = "4.0.0-beta.8", features = [ "rustls" ] }
anyhow = "1" anyhow = "1"
handlebars = { version = "4", features = [ "dir_source" ] } handlebars = { version = "4", features = [ "dir_source" ] }
lettre = { version = "0.9", features = [ "serde-impls" ] } lettre = { version = "0.10.0-rc.3", features = [ "serde", "tokio1-rustls-tls" ] }
once_cell = "1" once_cell = "1"
rand = { version = "0.8", features = [ "small_rng" ] } rand = { version = "0.8", features = [ "small_rng" ] }
serde = "1" serde = "1"

View file

@ -1,5 +1,5 @@
CREATE TABLE IF NOT EXISTS users( CREATE TABLE IF NOT EXISTS users(
email TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE PRIMARY KEY,
password BLOB NOT NULL password BLOB NOT NULL
); );

View file

@ -1,5 +1,15 @@
{ {
"db": "SQLite", "db": "SQLite",
"15fac42882fc4be06e0f99d3be97fcbf1570c6bd14fcd13cd96ee78892668489": {
"query": "CREATE TABLE IF NOT EXISTS users(\n email TEXT NOT NULL UNIQUE PRIMARY KEY,\n password BLOB NOT NULL\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS ix_email ON users(email);",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
}
},
"57f6668b1fb93316e1beff8c2189a59da3a34995afc962a4704bc7d196a159f3": { "57f6668b1fb93316e1beff8c2189a59da3a34995afc962a4704bc7d196a159f3": {
"query": "INSERT INTO users (email, password) VALUES (?, ?)", "query": "INSERT INTO users (email, password) VALUES (?, ?)",
"describe": { "describe": {
@ -27,15 +37,5 @@
false false
] ]
} }
},
"dd98414343cc029a0cc106382532de954050ded749f3a3f7d9dc077ed7515572": {
"query": "CREATE TABLE IF NOT EXISTS users(\n email TEXT PRIMARY KEY,\n password BLOB NOT NULL\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS ix_email ON users(email);",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
}
} }
} }

20
src/email.rs Normal file
View file

@ -0,0 +1,20 @@
use lettre::error::Error as EmailError;
use lettre::message::Mailbox;
use lettre::transport::stub::StubTransport;
use lettre::{Address, AsyncTransport, Message};
pub async fn send_registration_email(address: Address) -> Result<(), EmailError> {
let message = Message::builder()
.from(Mailbox::new(
Some("Some username".to_string()),
"foo@example.com".parse().unwrap(),
))
.to(Mailbox::new(None, address))
.subject("Registration for this website")
.body("hell world".to_string())
.unwrap();
StubTransport::new_ok().send(message).await.unwrap();
Ok(())
}

View file

@ -1,8 +1,12 @@
mod email;
mod password;
mod session; mod session;
use std::str::FromStr; use std::str::FromStr;
use session::Session; use crate::email::send_registration_email;
use crate::password::Password;
use crate::session::Session;
use actix_csrf::extractor::{CsrfCookie, CsrfToken}; use actix_csrf::extractor::{CsrfCookie, CsrfToken};
use actix_csrf::Csrf; use actix_csrf::Csrf;
@ -10,15 +14,14 @@ use actix_session::CookieSession;
use actix_web::cookie::SameSite; use actix_web::cookie::SameSite;
use actix_web::error::InternalError; use actix_web::error::InternalError;
use actix_web::http::header::LOCATION; use actix_web::http::header::LOCATION;
use actix_web::http::{Method, StatusCode}; use actix_web::http::{Method, StatusCode, Uri};
use actix_web::middleware::Logger; use actix_web::middleware::Logger;
use actix_web::web::{Form, Query}; use actix_web::web::{Form, Query};
use actix_web::{get, post, App, HttpRequest, HttpResponse, HttpServer, Responder}; use actix_web::{get, post, App, HttpRequest, HttpResponse, HttpServer, Responder};
use handlebars::Handlebars; use handlebars::Handlebars;
use lettre::EmailAddress; use lettre::Address as EmailAddress;
use once_cell::sync::{Lazy, OnceCell}; use once_cell::sync::{Lazy, OnceCell};
use rand::prelude::StdRng; use rand::prelude::StdRng;
use serde::de::Visitor;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sodiumoxide::crypto::pwhash::argon2id13::{self, HashedPassword, HASHEDPASSWORDBYTES}; use sodiumoxide::crypto::pwhash::argon2id13::{self, HashedPassword, HASHEDPASSWORDBYTES};
use sqlx::sqlite::SqliteConnectOptions; use sqlx::sqlite::SqliteConnectOptions;
@ -80,6 +83,7 @@ async fn main() -> std::io::Result<()> {
.service(login) .service(login)
.service(register_ui) .service(register_ui)
.service(register) .service(register)
.service(registration_confirmation)
.service(account_ui) .service(account_ui)
.service(logout) .service(logout)
.service(actix_files::Files::new("/static", "src/static")) .service(actix_files::Files::new("/static", "src/static"))
@ -89,20 +93,25 @@ async fn main() -> std::io::Result<()> {
.await .await
} }
#[derive(Deserialize, Serialize)] /// Macro meme to render a template without any context or with a provided one.
enum SessionState { macro_rules! render {
Anonymous, ($template_name:literal) => {
} render!($template_name, &())
};
#[get("/")] ($template_name:literal, $template_args:expr $(,)?) => {
async fn index() -> impl Responder { match TEMPLATE_ENGINE.render($template_name, $template_args) {
match TEMPLATE_ENGINE.render("index", &()) {
Ok(resp) => Ok(HttpResponse::Ok().body(resp)), Ok(resp) => Ok(HttpResponse::Ok().body(resp)),
Err(e) => { Err(e) => {
error!("{}", e); error!("{}", e);
Err(InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR)) Err(InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))
} }
} }
};
}
#[get("/")]
async fn index() -> impl Responder {
render!("index")
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -118,19 +127,13 @@ async fn login_ui(csrf: CsrfToken, mut query: Query<LoginQuery>) -> impl Respond
csrf: CsrfToken, csrf: CsrfToken,
} }
match TEMPLATE_ENGINE.render( render!(
"login", "login",
&TemplateArgs { &TemplateArgs {
error: query.error.take(), error: query.error.take(),
csrf, csrf,
}, },
) { )
Ok(resp) => Ok(HttpResponse::Ok().body(resp)),
Err(e) => {
error!("{}", e);
Err(InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))
}
}
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -140,42 +143,6 @@ struct Login {
password: Password, password: Password,
} }
#[derive(Serialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct Password(String);
impl<'de> Deserialize<'de> for Password {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{Error, Unexpected};
struct SecretDeserializer;
impl<'de> Visitor<'de> for SecretDeserializer {
type Value = Password;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a password between 8 and 64 bytes")
}
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
if v.len() < 8 || v.len() > 64 {
println!("password failed");
return Err(Error::invalid_value(
Unexpected::Str("password with invalid size"),
&"a password between 8 and 64 bytes",
));
}
Ok(Password(v.to_owned()))
}
}
deserializer.deserialize_string(SecretDeserializer)
}
}
#[post("/login")] #[post("/login")]
async fn login(csrf_cookie: CsrfCookie, form: Form<Login>, session: Session) -> impl Responder { async fn login(csrf_cookie: CsrfCookie, form: Form<Login>, session: Session) -> impl Responder {
if !csrf_cookie.validate(form.csrf.as_ref()) { if !csrf_cookie.validate(form.csrf.as_ref()) {
@ -188,23 +155,18 @@ async fn login(csrf_cookie: CsrfCookie, form: Form<Login>, session: Session) ->
.await; .await;
if let Ok(record) = verified { if let Ok(record) = verified {
let verified = argon2id13::pwhash_verify( if form
&HashedPassword::from_slice(&record.password).unwrap(), .password
form.password.0.as_bytes(), .verify(&HashedPassword::from_slice(&record.password).unwrap())
); {
if verified {
let redirect_to = session.get_redirect_url(); let redirect_to = session.get_redirect_url();
session.init(&form.email); session.init(&form.email);
return redirect(
let mut resp = HttpResponse::SeeOther(); &redirect_to
.as_ref()
if let Some(path) = redirect_to { .map(Uri::to_string)
resp.insert_header((LOCATION, path.to_string())); .unwrap_or("/account".into()),
} else { );
resp.insert_header((LOCATION, "/account"));
}
return resp.finish();
} }
} else { } else {
// To guard against timing attacks, we'll construct a fake password to // To guard against timing attacks, we'll construct a fake password to
@ -220,15 +182,11 @@ async fn login(csrf_cookie: CsrfCookie, form: Form<Login>, session: Session) ->
// Rust shouldn't optimize this out since it ultimately calls out to // Rust shouldn't optimize this out since it ultimately calls out to
// a C function, so it shouldn't find out that the function is pure. // a C function, so it shouldn't find out that the function is pure.
argon2id13::pwhash_verify( form.password
&HashedPassword::from_slice(&data).unwrap(), .verify(&HashedPassword::from_slice(&data).expect("Should be valid password data"));
form.password.0.as_bytes(),
);
} }
HttpResponse::SeeOther() redirect("/login?error=true")
.insert_header((LOCATION, "/login?error=true"))
.finish()
} }
#[get("/register")] #[get("/register")]
@ -238,13 +196,7 @@ async fn register_ui(csrf: CsrfToken) -> impl Responder {
csrf: CsrfToken, csrf: CsrfToken,
} }
match TEMPLATE_ENGINE.render("register", &TemplateArgs { csrf }) { render!("register", &TemplateArgs { csrf })
Ok(resp) => Ok(HttpResponse::Ok().body(resp)),
Err(e) => {
error!("{}", e);
Err(InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))
}
}
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -255,26 +207,15 @@ struct RegistrationInfo {
} }
#[post("/register")] #[post("/register")]
async fn register( async fn register(csrf_cookie: CsrfCookie, form: Form<RegistrationInfo>) -> impl Responder {
csrf_cookie: CsrfCookie,
form: Form<RegistrationInfo>,
session: Session,
) -> impl Responder {
if !csrf_cookie.validate(form.csrf.as_ref()) { if !csrf_cookie.validate(form.csrf.as_ref()) {
return HttpResponse::BadRequest().finish(); return HttpResponse::BadRequest().finish();
} }
let hashed = { let hashed = if let Ok(res) = form.password.hashed() {
let res = argon2id13::pwhash(
form.password.0.as_bytes(),
argon2id13::OPSLIMIT_INTERACTIVE,
argon2id13::MEMLIMIT_INTERACTIVE,
);
if let Ok(res) = res {
res res
} else { } else {
return HttpResponse::InternalServerError().finish(); return HttpResponse::InternalServerError().finish();
}
}; };
let hashed = hashed.as_ref(); let hashed = hashed.as_ref();
@ -288,31 +229,36 @@ async fn register(
.await; .await;
if insert_res.is_ok() { if insert_res.is_ok() {
session.init(&form.email); send_registration_email(form.email.clone()).await.unwrap();
HttpResponse::SeeOther()
.insert_header((LOCATION, "/account"))
.finish()
} else {
todo!()
} }
redirect("/registration_confirmation")
}
#[get("/registration_confirmation")]
async fn registration_confirmation() -> impl Responder {
render!("registration_confirmation")
} }
#[get("/account")] #[get("/account")]
async fn account_ui(req: HttpRequest, session: Session) -> impl Responder { async fn account_ui(req: HttpRequest, session: Session) -> impl Responder {
if let Err(error) = session.validate_or_redirect(req.uri()) { if let Err(error) = session.validate_or_redirect(req.uri()) {
return error; return Ok(error);
} }
HttpResponse::Ok().body(format!("{:?}", session.email())) render!("account")
} }
#[get("/logout")] #[get("/logout")]
async fn logout(session: Session) -> impl Responder { async fn logout(session: Session) -> impl Responder {
// It should be ok to logout without a CSRF token; the worst case is that // While this is a state-mutating endpoint, it is fine to not have a CSRF
// the user is logged out, which is fail-safe. // token; the worst case is that the user is logged out, which is fail-safe.
session.purge(); session.purge();
redirect("/")
}
fn redirect(location: &str) -> HttpResponse {
HttpResponse::SeeOther() HttpResponse::SeeOther()
.append_header((LOCATION, "/")) .append_header((LOCATION, location))
.finish() .finish()
} }

63
src/password.rs Normal file
View file

@ -0,0 +1,63 @@
use serde::de::Visitor;
use serde::{Deserialize, Serialize};
use sodiumoxide::crypto::pwhash::argon2id13::{self, HashedPassword};
use sodiumoxide::utils::memzero;
#[derive(Serialize, PartialEq, Eq)]
pub struct Password(String);
impl Password {
pub fn hashed(&self) -> Result<HashedPassword, ()> {
argon2id13::pwhash(
self.0.as_bytes(),
argon2id13::OPSLIMIT_INTERACTIVE,
argon2id13::MEMLIMIT_INTERACTIVE,
)
}
pub fn verify(&self, password: &HashedPassword) -> bool {
argon2id13::pwhash_verify(password, self.0.as_bytes())
}
}
/// A custom drop implementation is necessary for this type as we need to ensure
/// that the password is not stored in memory for an extended period of time.
impl Drop for Password {
fn drop(&mut self) {
// SAFETY: self.0 is never accessed after calling drop.
memzero(unsafe { self.0.as_bytes_mut() })
}
}
impl<'de> Deserialize<'de> for Password {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{Error, Unexpected};
struct SecretDeserializer;
impl<'de> Visitor<'de> for SecretDeserializer {
type Value = Password;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a password between 8 and 64 bytes")
}
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
if v.len() < 8 || v.len() > 64 {
println!("password failed");
return Err(Error::invalid_value(
Unexpected::Str("password with invalid size"),
&"a password between 8 and 64 bytes",
));
}
Ok(Password(v.to_owned()))
}
}
deserializer.deserialize_string(SecretDeserializer)
}
}

View file

@ -6,7 +6,7 @@ use actix_web::dev::Payload;
use actix_web::http::header::LOCATION; use actix_web::http::header::LOCATION;
use actix_web::http::Uri; use actix_web::http::Uri;
use actix_web::{FromRequest, HttpRequest, HttpResponse}; use actix_web::{FromRequest, HttpRequest, HttpResponse};
use lettre::EmailAddress; use lettre::Address;
use rand::{thread_rng, Fill}; use rand::{thread_rng, Fill};
pub struct Session(actix_session::Session); pub struct Session(actix_session::Session);
@ -26,7 +26,7 @@ impl FromRequest for Session {
} }
impl Session { impl Session {
pub fn init(&self, email: &EmailAddress) { pub fn init(&self, email: &Address) {
self.0.clear(); self.0.clear();
self.0 self.0
.insert("email", email) .insert("email", email)
@ -77,7 +77,7 @@ impl Session {
.expect("setting a str to work"); .expect("setting a str to work");
} }
pub fn email(&self) -> Option<EmailAddress> { pub fn email(&self) -> Option<Address> {
self.0.get("email").ok().flatten() self.0.get("email").ok().flatten()
} }
} }

View file

@ -3,7 +3,7 @@ h1, h2, h3, h4, h5, h6 {
font-family: 'M PLUS 1p', sans-serif; font-family: 'M PLUS 1p', sans-serif;
} }
p, label, input { p, label, input, a {
font-family: 'Noto Sans', sans-serif; font-family: 'Noto Sans', sans-serif;
margin-top: 0; margin-top: 0;
} }

10
src/templates/account.hbs Normal file
View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
{{> head }}
<body>
<h1>A TLD for everyone</h1>
<h2>Logged in!</h2>
<a href="/logout">Logout</a>
</body>
</html>

View file

@ -5,5 +5,6 @@
<h1>A TLD for everyone</h1> <h1>A TLD for everyone</h1>
<h2>About us</h2> <h2>About us</h2>
<a href="/login">Login</a>
</body> </body>
</html> </html>

View file

@ -6,9 +6,6 @@
<h1>A TLD for everyone</h1> <h1>A TLD for everyone</h1>
<h2>Sign up</h2> <h2>Sign up</h2>
{{#if error}}
<p>The email is already used. Please try again.</p>
{{/if}}
<form action="/register" method="post"> <form action="/register" method="post">
<input name="csrf" type="hidden" value="{{csrf}}"> <input name="csrf" type="hidden" value="{{csrf}}">
<label>Email <label>Email

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
{{> head }}
<body>
<h1>A TLD for everyone</h1>
<h2>Registration Requested</h2>
<p>
If successful, a link to activate your account has been emailed to the address provided.
</p>
<p>
You must register your account before you can log in.
</p>
<h4>I didn't receive an email!</h4>
<p>
An email will not have been sent if you already have an existing account
with us. If this is your first time registering, please also check your spam
folder.
</p>
</body>
</html>