auth: extract community auth crate
This commit is contained in:
+14
-99
@@ -1,26 +1,23 @@
|
||||
#[cfg(test)]
|
||||
use argon2::{Algorithm, Params, Version};
|
||||
use argon2::{
|
||||
Argon2,
|
||||
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
|
||||
};
|
||||
use axum::{
|
||||
extract::{OriginalUri, Request, State},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use axum_extra::extract::cookie::{Cookie, CookieJar};
|
||||
use crank_community_auth::{
|
||||
cleared_session_cookie as build_cleared_session_cookie,
|
||||
create_session_cookie as build_session_cookie, hash_password as community_hash_password,
|
||||
session_cookie as build_session_cookie_header,
|
||||
};
|
||||
use crank_core::{User, UserSessionId, WorkspaceId};
|
||||
use crank_registry::WorkspaceMembershipRecord;
|
||||
use rand::RngCore;
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
use crate::{error::ApiError, state::AppState};
|
||||
|
||||
pub const SESSION_COOKIE_NAME: &str = "crank_session";
|
||||
pub use crank_community_auth::{
|
||||
SESSION_COOKIE_NAME, SessionCookie, extract_session_token, hash_session_secret, verify_password,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BootstrapAdminConfig {
|
||||
@@ -46,104 +43,22 @@ pub struct AuthenticatedSession {
|
||||
pub current_workspace_id: Option<WorkspaceId>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SessionCookie {
|
||||
pub session_id: UserSessionId,
|
||||
pub value: String,
|
||||
pub expires_at: OffsetDateTime,
|
||||
}
|
||||
|
||||
pub fn hash_password(password: &str, pepper: &str) -> Result<String, ApiError> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let password = format!("{password}{pepper}");
|
||||
password_hasher()?
|
||||
.hash_password(password.as_bytes(), &salt)
|
||||
.map(|hash| hash.to_string())
|
||||
community_hash_password(password, pepper)
|
||||
.map_err(|error| ApiError::internal(format!("failed to hash password: {error}")))
|
||||
}
|
||||
|
||||
pub fn verify_password(password: &str, pepper: &str, password_hash: &str) -> bool {
|
||||
let Ok(parsed) = PasswordHash::new(password_hash) else {
|
||||
return false;
|
||||
};
|
||||
let password = format!("{password}{pepper}");
|
||||
match password_hasher() {
|
||||
Ok(argon2) => argon2.verify_password(password.as_bytes(), &parsed).is_ok(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn password_hasher() -> Result<Argon2<'static>, ApiError> {
|
||||
#[cfg(test)]
|
||||
{
|
||||
let params = Params::new(8 * 1024, 1, 1, None).map_err(|error| {
|
||||
ApiError::internal(format!("failed to initialize test argon2 params: {error}"))
|
||||
})?;
|
||||
Ok(Argon2::new(Algorithm::Argon2id, Version::V0x13, params))
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
Ok(Argon2::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_session_cookie(settings: &AuthSettings) -> Result<SessionCookie, ApiError> {
|
||||
let session_id = UserSessionId::new(format!("sess_{}", uuid::Uuid::now_v7().simple()));
|
||||
let mut secret_bytes = [0_u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut secret_bytes);
|
||||
let secret = URL_SAFE_NO_PAD.encode(secret_bytes);
|
||||
let expires_at = OffsetDateTime::now_utc()
|
||||
.checked_add(Duration::hours(settings.session_ttl_hours))
|
||||
.ok_or_else(|| ApiError::internal("failed to compute session expiration"))?;
|
||||
|
||||
Ok(SessionCookie {
|
||||
session_id,
|
||||
value: format!("{secret}.{}", expires_at.unix_timestamp_nanos()),
|
||||
expires_at,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hash_session_secret(
|
||||
session_id: &UserSessionId,
|
||||
session_value: &str,
|
||||
session_secret: &str,
|
||||
) -> String {
|
||||
let mut digest = Sha256::new();
|
||||
digest.update(session_id.as_str().as_bytes());
|
||||
digest.update(b":");
|
||||
digest.update(session_value.as_bytes());
|
||||
digest.update(b":");
|
||||
digest.update(session_secret.as_bytes());
|
||||
URL_SAFE_NO_PAD.encode(digest.finalize())
|
||||
build_session_cookie(settings.session_ttl_hours)
|
||||
.map_err(|error| ApiError::internal(format!("failed to create session cookie: {error}")))
|
||||
}
|
||||
|
||||
pub fn session_cookie(settings: &AuthSettings, token: &str) -> Cookie<'static> {
|
||||
Cookie::build((SESSION_COOKIE_NAME, token.to_owned()))
|
||||
.http_only(true)
|
||||
.same_site(SameSite::Lax)
|
||||
.secure(settings.cookie_secure)
|
||||
.path("/")
|
||||
.max_age(Duration::hours(settings.session_ttl_hours))
|
||||
.build()
|
||||
build_session_cookie_header(token, settings.cookie_secure, settings.session_ttl_hours)
|
||||
}
|
||||
|
||||
pub fn cleared_session_cookie(settings: &AuthSettings) -> Cookie<'static> {
|
||||
Cookie::build((SESSION_COOKIE_NAME, String::new()))
|
||||
.http_only(true)
|
||||
.same_site(SameSite::Lax)
|
||||
.secure(settings.cookie_secure)
|
||||
.path("/")
|
||||
.max_age(Duration::seconds(0))
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn extract_session_token(jar: &CookieJar) -> Option<(UserSessionId, String)> {
|
||||
let cookie = jar.get(SESSION_COOKIE_NAME)?;
|
||||
let value = cookie.value();
|
||||
let (session_id, session_value) = value.split_once('.')?;
|
||||
|
||||
Some((UserSessionId::new(session_id), session_value.to_owned()))
|
||||
build_cleared_session_cookie(settings.cookie_secure)
|
||||
}
|
||||
|
||||
pub async fn require_session(
|
||||
|
||||
Reference in New Issue
Block a user