hmac jwts

This commit is contained in:
Brandon Thomas committed 2021-05-17 20:31:55 -04:00
1 parent d2a75a29ec
commit 9893737a57
10 files changed
+292 -19

No files matched your search

Generated
+38
View File
@@ -879,6 +879,16 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "crypto-mac"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6"
dependencies = [
"generic-array",
"subtle",
]
[[package]]
name = "ctor"
version = "0.1.20"
@@ -1286,6 +1296,16 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hmac"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
dependencies = [
"crypto-mac",
"digest",
]
[[package]]
name = "hostname"
version = "0.3.1"
@@ -1400,6 +1420,21 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "jwt"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86e46349d67dc03bdbdb28da0337a355a53ca1d5156452722c36fe21d0e6389b"
dependencies = [
"base64 0.13.0",
"crypto-mac",
"digest",
"hmac",
"serde",
"serde_json",
"sha2",
]
[[package]]
name = "kernel32-sys"
version = "0.2.2"
@@ -2476,7 +2511,9 @@ dependencies = [
"diesel",
"easyenv",
"futures",
"hmac",
"hostname",
"jwt",
"lazy_static",
"log",
"rand 0.8.3",
@@ -2484,6 +2521,7 @@ dependencies = [
"serde",
"serde_derive",
"serde_json",
"sha2",
"sqlx",
"toml",
]
+6 -1
View File
@@ -15,7 +15,6 @@ path = "src/main.rs"
[dependencies]
anyhow = "1.0.40"
base64 = "0.13.0"
bcrypt = "0.8"
chrono = { version = "0.4.19", features = ["serde"] }
derive_more = "0.99.14"
easyenv = "0.0.5"
@@ -46,6 +45,12 @@ actix-http = "2.2.0"
actix-rt = "2.2.0"
actix-utils = "3.0.0"
actix-web = "3.3.2"
jwt = "0.13.0"
# Crypto
bcrypt = "0.8"
hmac = "0.10.0"
sha2 = "0.9.5"
# Serialization
serde = "1.0.125"
+5 -7
View File
@@ -7,7 +7,7 @@ use actix_web::{Responder, web, HttpResponse, error, HttpRequest};
use crate::endpoints::users::create_account::CreateAccountError::{BadInput, ServerError, UsernameTaken, EmailTaken};
use crate::server_state::ServerState;
use crate::util::ip_address::get_request_ip;
use crate::util::random::random_token;
use crate::util::tokens::random_token;
use derive_more::{Display, Error};
use log::{info, warn, log};
use regex::Regex;
@@ -210,12 +210,10 @@ VALUES ( ?, ?, ?, NOW() + interval 1 year )
info!("new user session created");
let session_cookie = Cookie::build("session", session_token)
.domain(".vo.codes")
.path("/")
.secure(true) // HTTPS-only
.http_only(true) // Not exposed to Javascript
.finish();
let session_cookie = match server_state.cookie_manager.create_cookie(&session_token) {
Ok(cookie) => cookie,
Err(_) => return Err(ServerError),
};
let response = CreateAccountSuccessResponse {
success: true,
+176
View File
@@ -0,0 +1,176 @@
use actix_http::Error;
use actix_http::http::header;
use actix_web::dev::HttpResponseBuilder;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::{Responder, web, HttpResponse, error, HttpRequest};
use crate::endpoints::users::create_account::CreateAccountError::{BadInput, ServerError, UsernameTaken, EmailTaken};
use crate::server_state::ServerState;
use crate::util::ip_address::get_request_ip;
use crate::util::tokens::random_token;
use derive_more::{Display, Error};
use log::{info, warn, log};
use regex::Regex;
use sqlx::error::DatabaseError;
use sqlx::error::Error::Database;
use sqlx::mysql::MySqlDatabaseError;
use std::sync::Arc;
use crate::validations::username::validate_username;
use crate::validations::passwords::validate_passwords;
use actix_web::cookie::Cookie;
#[derive(Deserialize)]
pub struct LoginRequest {
pub username_or_email: String,
pub password: String,
}
#[derive(Serialize)]
pub struct LoginSuccessResponse {
pub success: bool,
}
#[derive(Serialize)]
pub struct ErrorResponse {
pub success: bool,
pub error_reason: String,
}
#[derive(Debug, Display)]
pub enum CreateAccountError {
BadInput(String),
UsernameTaken,
EmailTaken,
ServerError,
}
impl ResponseError for CreateAccountError {
fn status_code(&self) -> StatusCode {
match *self {
CreateAccountError::BadInput(_) => StatusCode::BAD_REQUEST,
CreateAccountError::UsernameTaken => StatusCode::BAD_REQUEST,
CreateAccountError::EmailTaken => StatusCode::BAD_REQUEST,
CreateAccountError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
BadInput(reason) => reason.to_string(),
CreateAccountError::UsernameTaken => "username already taken".to_string(),
CreateAccountError::EmailTaken => "email already taken".to_string(),
CreateAccountError::ServerError => "server error".to_string(),
};
let response = ErrorResponse {
success: false,
error_reason,
};
let body = match serde_json::to_string(&response) {
Ok(json) => json,
Err(_) => "{}".to_string(),
};
HttpResponseBuilder::new(self.status_code())
.set_header(header::CONTENT_TYPE, "application/json")
.body(body)
}
}
pub async fn login_handler(
http_request: HttpRequest,
request: web::Json<LoginRequest>,
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, CreateAccountError>
{
let try_password_hash = match bcrypt::hash(&request.password, bcrypt::DEFAULT_COST) {
Ok(hash) => hash,
Err(err) => {
warn!("Bcrypt error: {:?}", err);
return Err(ServerError);
}
};
let query_result = sqlx::query!(
r#"
INSERT INTO users (
token,
username,
display_name,
email_address,
profile_markdown,
profile_rendered_html,
user_role_slug,
password_hash,
ip_address_creation,
ip_address_last_login,
ip_address_last_update
)
VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
"#,
user_token.to_string(),
username,
display_name,
request.email_address.to_string(),
profile_markdown,
profile_rendered_html,
NEW_USER_ROLE,
password_hash,
ip_address.to_string(),
ip_address.to_string(),
ip_address.to_string(),
)
.execute(&server_state.mysql_pool)
.await;
let record_id = match query_result {
Ok(res) => {
res.last_insert_id()
},
Err(err) => {
warn!("New user creation DB error: {:?}", err);
// NB: SQLSTATE[23000]: Integrity constraint violation
// NB: MySQL Error Code 1062: Duplicate key insertion (this is harder to access)
match err {
Database(err) => {
let maybe_code = err.code().map(|c| c.into_owned());
match maybe_code.as_deref() {
Some("23000") => {
if err.message().contains("username") {
return Err(UsernameTaken);
} else if err.message().contains("email_address") {
return Err(EmailTaken);
}
}
_ => {},
}
},
_ => {},
}
return Err(ServerError);
}
};
info!("login session created");
let session_cookie = Cookie::build("session", session_token)
.domain(".vo.codes")
.path("/")
.secure(true) // HTTPS-only
.http_only(true) // Not exposed to Javascript
.finish();
let response = CreateAccountSuccessResponse {
success: true,
};
let body = serde_json::to_string(&response)
.map_err(|e| BadInput("".to_string()))?;
Ok(HttpResponse::Ok()
.cookie(session_cookie)
.content_type("application/json")
.body(body))
}
+2 -2
View File
@@ -1,3 +1,3 @@
pub mod login;
pub mod logout;
//pub mod login;
//pub mod logout;
pub mod create_account;
+11 -6
View File
@@ -18,17 +18,14 @@ use sqlx::MySqlPool;
use sqlx::mysql::MySqlPoolOptions;
use crate::queries::badges::NewBadge;
use crate::endpoints::users::create_account::create_account_handler;
//use crate::endpoints::users::login::login_handler;
use crate::util::cookies::CookieManager;
const DEFAULT_BIND_ADDRESS : &'static str = "0.0.0.0:12345";
const DEFAULT_RUST_LOG: &'static str = "debug,actix_web=info";
pub type AnyhowResult<T> = anyhow::Result<T>;
//pub async fn main() -> AnyhowResult<()> {
//#[tokio::main]
//#[async_std::main]
//#[actix_web::main]
//pub async fn main() -> std::io::Result<()> {
#[actix_web::main]
async fn main() -> AnyhowResult<()> {
easyenv::init_all_with_default_logging(Some(DEFAULT_RUST_LOG));
@@ -45,6 +42,9 @@ async fn main() -> AnyhowResult<()> {
.connect(db_connection_string)
.await?;
let hmac_secret = easyenv::get_env_string_or_default("COOKIE_SECRET", "notsecret");
let cookie_manager = CookieManager::new(".vo.codes", &hmac_secret);
let server_state = ServerState {
env_config: EnvConfig {
num_workers: 4,
@@ -52,6 +52,7 @@ async fn main() -> AnyhowResult<()> {
},
hostname: server_hostname,
mysql_pool: pool,
cookie_manager,
};
serve(server_state)
@@ -59,7 +60,6 @@ async fn main() -> AnyhowResult<()> {
Ok(())
}
//#[actix_web::main]
pub async fn serve(server_state: ServerState) -> AnyhowResult<()>
{
let bind_address = server_state.env_config.bind_address.clone();
@@ -110,6 +110,11 @@ pub async fn serve(server_state: ServerState) -> AnyhowResult<()>
.route(web::post().to(create_account_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
/*.service(
web::resource("/login")
.route(web::post().to(login_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)*/
.service(
web::scope("/foo")
.service(
+4
View File
@@ -1,13 +1,17 @@
use sqlx::MySqlPool;
use crate::util::cookies::CookieManager;
/// State that is injected into every endpoint.
#[derive(Clone)]
pub struct ServerState {
/// Configuration from ENV vars.
pub env_config: EnvConfig,
pub hostname: String,
pub mysql_pool: MySqlPool,
pub cookie_manager: CookieManager,
}
#[derive(Clone)]
+46
View File
@@ -0,0 +1,46 @@
use actix_web::cookie::Cookie;
use anyhow::anyhow;
use crate::AnyhowResult;
use hmac::Hmac;
use hmac::NewMac;
use std::collections::BTreeMap;
use sha2::Sha256;
use jwt::SignWithKey;
const COOKIE_VERSION : u32 = 1;
#[derive(Clone)]
pub struct CookieManager {
cookie_domain: String,
hmac_secret: String,
}
impl CookieManager {
pub fn new(cookie_domain: &str, hmac_secret: &str) -> Self {
Self {
cookie_domain: cookie_domain.to_string(),
hmac_secret: hmac_secret.to_string(),
}
}
pub fn create_cookie(&self, session_token: &str) -> AnyhowResult<Cookie> {
let key: Hmac<Sha256> = Hmac::new_varkey(self.hmac_secret.as_bytes())
.map_err(|e| anyhow!("invalid hmac: {:?}", e))?;
let cookie_version = COOKIE_VERSION.to_string();
let mut claims = BTreeMap::new();
claims.insert("session_token", session_token);
claims.insert("cookie_version", &cookie_version);
let jwt_string = claims.sign_with_key(&key)?;
Ok(Cookie::build("session", jwt_string)
.domain(&self.cookie_domain)
.path("/")
.secure(true) // HTTPS-only
.http_only(true) // Not exposed to Javascript
.finish())
}
}
+3 -2
View File
@@ -1,2 +1,3 @@
pub mod random;
pub mod ip_address;
pub mod cookies;
pub mod ip_address;
pub mod tokens;
+1 -1
View File
@@ -18,7 +18,7 @@ pub fn random_token(length: usize) -> String {
#[cfg(test)]
mod tests {
use crate::util::random::random_token;
use crate::util::tokens::random_token;
#[test]
fn random_token_length() {