feat: set up basic sessions

This commit is contained in:
Andrew Rioux
2025-01-28 03:10:43 -05:00
parent bee66a8d6c
commit bf879bb081
23 changed files with 862 additions and 106 deletions
+129 -16
View File
@@ -1,29 +1,77 @@
use pbkdf2::{pbkdf2_hmac_array, password_hash::{rand_core::OsRng, SaltString}};
use sha2::Sha256;
#[derive(Clone)]
pub struct User {
pub user_id: i64,
pub user_name: String,
password_hash: String,
pub last_active: Option<i64>
}
use async_trait::async_trait;
use pbkdf2::{Pbkdf2, password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, rand_core::OsRng, SaltString}};
use axum_login::{AuthUser, AuthnBackend, UserId};
use sqlx::SqlitePool;
use crate::error::Error;
const PASSWORD_ITERATIONS: u32 = 100_000;
impl std::fmt::Debug for User {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("User")
.field("user_id", &self.user_id)
.field("user_name", &self.user_name)
.field("password_hash", &"[redacted]")
.finish()
}
}
impl AuthUser for User {
type Id = i64;
fn id(&self) -> Self::Id {
self.user_id
}
fn session_auth_hash(&self) -> &[u8] {
self.password_hash.as_bytes()
}
}
async fn hash_password(pass: &[u8]) -> Result<String, Error> {
Ok(tokio::task::spawn_blocking({
let pass = pass.to_owned();
let salt = SaltString::generate(&mut OsRng);
move || Pbkdf2.hash_password(
&*pass,
&salt,
).map(|hash| hash.to_string())
}).await??)
}
async fn verify_password(pass: &str, hash: &str) -> Result<bool, Error> {
Ok(tokio::task::spawn_blocking({
let pass = pass.to_owned();
let hash = hash.to_owned();
move ||
PasswordHash::new(&*hash)
.map(|parsed| Pbkdf2.verify_password(
&pass.as_bytes(),
&parsed
).is_ok())
}).await??)
}
pub async fn reset_password<'a, E>(pool: E, id: i16, password: String) -> Result<(), crate::error::Error>
where
E: sqlx::SqliteExecutor<'a>
{
let salt = SaltString::generate(&mut OsRng);
let key = pbkdf2_hmac_array::<Sha256, 20>(
password.as_bytes(),
salt.as_str().as_bytes(),
PASSWORD_ITERATIONS
);
let salt_string = hex::encode(salt.as_str().as_bytes());
let password_string = hex::encode(&key[..]);
let password_string = hash_password(
password.as_bytes()
).await?;
sqlx::query!(
"UPDATE users SET password_hash = ?, password_salt = ? WHERE user_id = ?",
"UPDATE users SET password_hash = ? WHERE user_id = ?",
password_string,
salt_string,
id
)
.execute(pool)
@@ -52,7 +100,7 @@ where
tracing::info!("Creating new user {}", name);
let new_id = sqlx::query!(
r#"INSERT INTO users (user_name, password_salt, password_hash) VALUES (?, "", "")"#,
r#"INSERT INTO users (user_name, password_hash) VALUES (?, "")"#,
name
)
.execute(&mut *tx)
@@ -65,3 +113,68 @@ where
Ok(())
}
#[derive(Clone)]
pub struct Backend(SqlitePool);
impl Backend {
pub fn new(db: SqlitePool) -> Self {
Self(db)
}
}
#[async_trait]
impl AuthnBackend for Backend {
type User = User;
type Credentials = (String, String);
type Error = Error;
async fn authenticate(
&self,
creds: Self::Credentials
) -> Result<Option<Self::User>, Self::Error> {
let user: Option<Self::User> = sqlx::query_as!(
User,
"SELECT * FROM users WHERE user_name = ?",
creds.0
)
.fetch_optional(&self.0)
.await?;
let Some(user) = user else { return Ok(None); };
let good_hash = verify_password(
&user.password_hash,
&creds.1
).await?;
if good_hash {
let now = chrono::Utc::now().timestamp();
sqlx::query!(
"UPDATE users SET last_active = ?",
now
)
.execute(&self.0)
.await?;
Ok(Some(user))
} else {
Ok(None)
}
}
async fn get_user(&self, user_id: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> {
let user: Option<Self::User> = sqlx::query_as!(
User,
"SELECT * FROM users WHERE user_id = ?",
user_id
)
.fetch_optional(&self.0)
.await?;
Ok(user)
}
}
pub type AuthSession = axum_login::AuthSession<Backend>;