mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-08 11:01:35 +00:00
hmac jwts
This commit is contained in:
1 parent
d2a75a29ec
commit
9893737a57
10 files changed
+292
-19
No files matched your search
Generated
+38
@@ -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
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod login;
|
||||
pub mod logout;
|
||||
//pub mod login;
|
||||
//pub mod logout;
|
||||
pub mod create_account;
|
||||
+11
-6
@@ -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(
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
@@ -1,2 +1,3 @@
|
||||
pub mod random;
|
||||
pub mod ip_address;
|
||||
pub mod cookies;
|
||||
pub mod ip_address;
|
||||
pub mod tokens;
|
||||
@@ -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() {
|
||||
Reference in new issue
Block a user