feat: set up basic sessions
This commit is contained in:
+129
-16
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user