feat: complete Epic 1 production foundation

This commit is contained in:
2026-08-25 01:24:11 +03:00
parent 767428436d
commit 182bde8ac0
298 changed files with 35719 additions and 5299 deletions
+6 -2
View File
@@ -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)
+2 -1
View File
@@ -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")
);
}
}