diff options
Diffstat (limited to 'src/routes')
| -rw-r--r-- | src/routes/auth.rs | 104 |
1 files changed, 82 insertions, 22 deletions
diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 64ada2b..4fb9124 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,34 +1,72 @@ use std::time::{SystemTime, UNIX_EPOCH}; -use axum::extract::{Json, State}; +use axum::extract::{Json, State, FromRequestParts}; use axum::response::IntoResponse; -use jsonwebtoken::{encode, Header}; +use axum::http::{StatusCode, request::Parts}; +use axum_extra::extract::CookieJar; +use axum_extra::extract::cookie::{Cookie, SameSite}; +use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode}; use serde::{Deserialize, Serialize}; +use time::Duration; + +use argon2::{Argon2, PasswordHash, PasswordVerifier, password_hash::{ + Error, PasswordHasher, SaltString, rand_core::OsRng +}}; use crate::AppState; -use crate::auth::{check_password}; -use super::errors::RouteError; +use crate::routes::errors::RouteError; -#[derive(Deserialize)] -pub struct LoginRequest { - email: String, - password: String, +#[derive(Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub exp: usize, + pub iat: usize, + pub is_admin: bool, } -#[derive(Serialize)] -struct LoginResponse { - token: String, +pub struct AuthUser(pub Claims); + +impl FromRequestParts<AppState> for AuthUser { + type Rejection = RouteError; + + async fn from_request_parts( + parts: &mut Parts, + state: &AppState, + ) -> Result<Self, Self::Rejection> { + let jar = CookieJar::from_request_parts(parts, state).await.unwrap(); + + let token = jar.get("token") + .map(|c| c.value().to_string()) + .ok_or(RouteError::AuthorizationFailure())?; + + let key = DecodingKey::from_secret(state.secret.as_ref()); + let data = decode::<Claims>(token, &key, &Validation::default()) + .map_err(|_| RouteError::AuthorizationFailure())?; + + Ok(AuthUser(data.claims)) + } } -#[derive(Serialize)] -struct JWTClaims { - sub: String, - exp: usize, - iat: usize, - is_admin: bool, +pub fn hash_password(password: &str) -> Result<String, Error> { + let argon2 = Argon2::default(); + let salt = SaltString::generate(&mut OsRng); + Ok(argon2.hash_password(password.as_bytes(), &salt)?.to_string()) +} + +pub fn check_password(password: &str, password_hash: &str) -> Result<bool, Error> { + let argon2 = Argon2::default(); + let hash = PasswordHash::new(password_hash)?; + Ok(argon2.verify_password(password.as_bytes(), &hash).is_ok()) +} + +#[derive(Deserialize)] +pub struct LoginRequest { + email: String, + password: String, } pub async fn login( + jar: CookieJar, State(state): State<AppState>, Json(request): Json<LoginRequest> ) -> Result<impl IntoResponse, RouteError> { @@ -50,17 +88,39 @@ pub async fn login( .map_err(|_| RouteError::Internal("failed to access system clock".into()))? .as_secs() as usize; - let claims = JWTClaims { - sub: user.email().to_owned(), + let claims = Claims { + sub: user.email().to_string(), iat: now, exp: now + 60 * 60 * 24, is_admin: false }; - - let token= encode(&Header::default(), &claims, &state.api_key) + + let key = EncodingKey::from_secret(state.secret.as_ref()); + let token = encode(&Header::default(), &claims, &key) .map_err(|e| RouteError::Internal(format!("failed to encode jwt: {e}")))?; - Ok(Json(LoginResponse { token })) + let cookie = Cookie::build(("token", token)) + .path("/") + .max_age(Duration::hours(24)) + .same_site(SameSite::Strict) + .http_only(true) + .build(); + + Ok(jar.add(cookie)) } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_password_hashing() { + let passwords = vec!["password", "test1", "random"]; + + for password in &passwords { + let hash = hash_password(password).unwrap(); + assert!(check_password(password, &hash).unwrap()); + } + } +} |
