locality/src/authentication/mod.rs

142 lines
3.6 KiB
Rust

use {
crate::{db, db::Database},
argon2::{
password_hash::{
rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString,
},
Argon2,
},
std::ops::Deref,
tracing::{error, warn},
};
mod jwt;
pub use jwt::{authenticate_user_with_jwt, create_jwt_for_user, Error as JwtError, ParsedJwt};
#[derive(Debug)]
pub enum AuthenticationError {
DatabaseError(db::Error),
HashError(argon2::password_hash::Error),
}
impl std::fmt::Display for AuthenticationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AuthenticationError::HashError(e) => write!(f, "{}", e),
AuthenticationError::DatabaseError(e) => {
write!(f, "Could not get password hash from database: {}", e)
}
}
}
}
impl std::error::Error for AuthenticationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
AuthenticationError::HashError(_) => None,
AuthenticationError::DatabaseError(e) => Some(e),
}
}
}
impl From<db::Error> for AuthenticationError {
fn from(value: db::Error) -> Self {
warn!(details = value.to_string(), "Database error");
AuthenticationError::DatabaseError(value)
}
}
impl From<argon2::password_hash::Error> for AuthenticationError {
fn from(value: argon2::password_hash::Error) -> Self {
error!(details = value.to_string(), "Error hashing password.");
AuthenticationError::HashError(value)
}
}
#[derive(Debug, Clone)]
pub struct AuthenticatedUser(db::User);
impl Deref for AuthenticatedUser {
type Target = db::User;
fn deref(&self) -> &db::User {
&self.0
}
}
#[derive(Debug)]
pub struct AuthenticatedAdminUser(db::User);
impl Deref for AuthenticatedAdminUser {
type Target = db::User;
fn deref(&self) -> &db::User {
&self.0
}
}
pub struct Password {
hash: String,
}
impl Password {
#[tracing::instrument]
pub fn new(password: &str) -> Result<Password, AuthenticationError> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
Ok(Password {
hash: argon2
.hash_password(password.as_bytes(), &salt)?
.to_string(),
})
}
pub fn check(&self, password: &str) -> Result<bool, AuthenticationError> {
let hash = PasswordHash::new(&self.hash)?;
Ok(Argon2::default()
.verify_password(password.as_bytes(), &hash)
.is_ok())
}
}
impl From<db::PasswordHash> for Password {
fn from(password: db::PasswordHash) -> Self {
Password {
hash: password.to_string(),
}
}
}
impl From<Password> for db::PasswordHash {
fn from(password: Password) -> Self {
db::PasswordHash(password.hash)
}
}
#[tracing::instrument]
pub async fn authenticate_user_with_password<D: Database>(
db: &D,
user: db::User,
supplied_password: &str,
) -> Result<Option<AuthenticatedUser>, AuthenticationError> {
let password: Password = db.get_password_for_user(&user).await?.into();
Ok(if password.check(supplied_password)? {
Some(AuthenticatedUser(user))
} else {
None
})
}
#[tracing::instrument]
pub async fn check_if_user_is_admin<D: Database>(
db: &D,
user: &AuthenticatedUser,
) -> Result<Option<AuthenticatedAdminUser>, db::Error> {
if db.is_user_admin(user).await? {
Ok(Some(AuthenticatedAdminUser(user.0.clone())))
} else {
Ok(None)
}
}