feat: complete Epic 1 production foundation
This commit is contained in:
@@ -2,8 +2,9 @@
|
||||
use argon2::{Algorithm, Params, Version};
|
||||
use argon2::{
|
||||
Argon2,
|
||||
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
|
||||
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
||||
};
|
||||
use rand::RngExt;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum HashPasswordError {
|
||||
@@ -14,7 +15,10 @@ pub enum HashPasswordError {
|
||||
}
|
||||
|
||||
pub fn hash_password(password: &str, pepper: &str) -> Result<String, HashPasswordError> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let mut salt_bytes = [0u8; 16];
|
||||
rand::rng().fill(&mut salt_bytes);
|
||||
let salt = SaltString::encode_b64(&salt_bytes)
|
||||
.map_err(|error| HashPasswordError::Hash(error.to_string()))?;
|
||||
let password = format!("{password}{pepper}");
|
||||
password_hasher()?
|
||||
.hash_password(password.as_bytes(), &salt)
|
||||
|
||||
@@ -6,5 +6,6 @@ pub use hashing::{HashPasswordError, hash_password, verify_password};
|
||||
pub use password_provider::PasswordIdentityProvider;
|
||||
pub use session_cookie::{
|
||||
SESSION_COOKIE_NAME, SessionCookie, SessionCookieError, cleared_session_cookie,
|
||||
create_session_cookie, extract_session_token, hash_session_secret, session_cookie,
|
||||
create_csrf_token, create_session_cookie, extract_session_token, hash_csrf_token,
|
||||
hash_session_secret, session_cookie,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ use crank_core::{
|
||||
use crank_registry::PostgresRegistry;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::hashing::verify_password;
|
||||
use crate::hashing::{hash_password, verify_password};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PasswordIdentityProvider {
|
||||
@@ -36,15 +36,23 @@ impl IdentityProvider for PasswordIdentityProvider {
|
||||
&self,
|
||||
payload: crank_core::LoginPayload,
|
||||
) -> Result<LoginOutcome, IdentityError> {
|
||||
let user = self
|
||||
let Some(user) = self
|
||||
.registry
|
||||
.get_auth_user_by_email(&payload.email)
|
||||
.await
|
||||
.map_err(|error| IdentityError::Internal(error.to_string()))?
|
||||
.ok_or(IdentityError::BadCredentials)?;
|
||||
else {
|
||||
let _ = hash_password(&payload.password, &self.password_pepper);
|
||||
return Err(IdentityError::BadCredentials);
|
||||
};
|
||||
|
||||
if user.user.status != crank_core::UserStatus::Active {
|
||||
return Err(IdentityError::AccountDisabled);
|
||||
let _ = verify_password(
|
||||
&payload.password,
|
||||
&self.password_pepper,
|
||||
&user.password_hash,
|
||||
);
|
||||
return Err(IdentityError::BadCredentials);
|
||||
}
|
||||
|
||||
if !verify_password(
|
||||
|
||||
@@ -40,11 +40,36 @@ pub fn hash_session_secret(
|
||||
session_id: &UserSessionId,
|
||||
session_value: &str,
|
||||
session_secret: &str,
|
||||
) -> String {
|
||||
hash_scoped_secret("session", session_id, session_value, session_secret)
|
||||
}
|
||||
|
||||
pub fn create_csrf_token() -> String {
|
||||
let mut secret_bytes = [0_u8; 32];
|
||||
rand::rng().fill(&mut secret_bytes);
|
||||
URL_SAFE_NO_PAD.encode(secret_bytes)
|
||||
}
|
||||
|
||||
pub fn hash_csrf_token(
|
||||
session_id: &UserSessionId,
|
||||
csrf_token: &str,
|
||||
session_secret: &str,
|
||||
) -> String {
|
||||
hash_scoped_secret("csrf", session_id, csrf_token, session_secret)
|
||||
}
|
||||
|
||||
fn hash_scoped_secret(
|
||||
scope: &str,
|
||||
session_id: &UserSessionId,
|
||||
secret_value: &str,
|
||||
session_secret: &str,
|
||||
) -> String {
|
||||
let mut digest = Sha256::new();
|
||||
digest.update(scope.as_bytes());
|
||||
digest.update(b":");
|
||||
digest.update(session_id.as_str().as_bytes());
|
||||
digest.update(b":");
|
||||
digest.update(session_value.as_bytes());
|
||||
digest.update(secret_value.as_bytes());
|
||||
digest.update(b":");
|
||||
digest.update(session_secret.as_bytes());
|
||||
URL_SAFE_NO_PAD.encode(digest.finalize())
|
||||
@@ -83,8 +108,8 @@ mod tests {
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
|
||||
use super::{
|
||||
SESSION_COOKIE_NAME, cleared_session_cookie, create_session_cookie, extract_session_token,
|
||||
session_cookie,
|
||||
SESSION_COOKIE_NAME, cleared_session_cookie, create_csrf_token, create_session_cookie,
|
||||
extract_session_token, hash_csrf_token, hash_session_secret, session_cookie,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -113,4 +138,15 @@ mod tests {
|
||||
assert!(session.session_id.as_str().starts_with("sess_"));
|
||||
assert!(session.value.contains('.'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csrf_hash_is_scoped_from_session_hash() {
|
||||
let session = create_session_cookie(24).unwrap();
|
||||
let csrf = create_csrf_token();
|
||||
assert_eq!(csrf.len(), 43);
|
||||
assert_ne!(
|
||||
hash_csrf_token(&session.session_id, &csrf, "secret"),
|
||||
hash_session_secret(&session.session_id, &csrf, "secret")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user