diff options
| author | Daniel Hader <[email protected]> | 2026-05-29 18:14:31 -0500 |
|---|---|---|
| committer | Daniel Hader <[email protected]> | 2026-05-29 18:14:31 -0500 |
| commit | 772c7844c4ca1de632f64eb9428e8e97eea64ac1 (patch) | |
| tree | 2b038268f0d15a02830f4c4e465a323f43ce3c35 /src | |
| parent | 334867ba0732f85a48ad88ef8f3201c10bc1da4e (diff) | |
login page interaction with server
Diffstat (limited to 'src')
| -rw-r--r-- | src/auth.rs | 25 | ||||
| -rw-r--r-- | src/main.rs | 9 | ||||
| -rw-r--r-- | src/routes/auth.rs | 104 |
3 files changed, 110 insertions, 28 deletions
diff --git a/src/auth.rs b/src/auth.rs index 90ce4a5..a621756 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,3 +1,5 @@ +use axum::{extract::FromRequestParts, http::{StatusCode, request::Parts}}; +use serde::Serialize; use argon2::{Argon2, PasswordHash, PasswordVerifier, password_hash::{ Error, PasswordHasher, SaltString, rand_core::OsRng }}; @@ -14,6 +16,29 @@ pub fn check_password(password: &str, password_hash: &str) -> Result<bool, Error Ok(argon2.verify_password(password.as_bytes(), &hash).is_ok()) } +#[derive(Serialize)] +pub struct Claims { + pub sub: String, + pub exp: usize, + pub iat: usize, + pub is_admin: bool, +} + +//pub fn create_jwt(email: &str, is_admin: bool) -> Result + +pub struct AuthUser(pub Claims); + +impl<S: Send + Sync> FromRequestParts<S> for AuthUser { + type Rejection = StatusCode; + + async fn from_request_parts( + parts: &mut Parts, + state: &S, + ) -> Result<Self, Self::Rejection> { + todo!(); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/main.rs b/src/main.rs index 511a8f2..5629578 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::{env, fs}; +use std::env; mod database; mod routes; @@ -19,7 +19,7 @@ use crate::database::Database; #[derive(Clone)] struct AppState { - api_key: EncodingKey, + secret: String, database: Database, } @@ -31,14 +31,11 @@ async fn main() { return; }; - let api_key = EncodingKey::from_secret(secret.as_ref()); - - let database = Database::new_in_memory().unwrap(); database.initialize().unwrap(); let state = AppState { - api_key: api_key.to_owned(), + secret: secret, database: database, }; 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()); + } + } +} |
