diff options
author | Toby Vincent <tobyv@tobyvin.dev> | 2024-04-16 13:56:26 -0500 |
---|---|---|
committer | Toby Vincent <tobyv@tobyvin.dev> | 2024-04-16 13:56:26 -0500 |
commit | e607eb77d4253adfb15c8a4ce08684e16ae96674 (patch) | |
tree | 921e6d002d9e3dc761f5d1bb7fea82abd2045919 | |
parent | 469cbc20853bcae0e74922f16f7a969d1b7a9a67 (diff) |
refactor(auth): move credential resource to module
-rw-r--r-- | src/api/users.rs | 4 | ||||
-rw-r--r-- | src/auth.rs | 51 | ||||
-rw-r--r-- | src/auth/credentials.rs | 66 | ||||
-rw-r--r-- | src/auth/jwt.rs | 23 |
4 files changed, 100 insertions, 44 deletions
diff --git a/src/api/users.rs b/src/api/users.rs index 45d69fe..7b378ef 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -57,8 +57,8 @@ pub async fn create( return Err(Error::EmailExists); } - // TODO: Move this into a tower service - let (status, (access, refresh)) = crate::auth::create( + // TODO: Move this into a micro service + let (status, (access, refresh)) = crate::auth::credentials::create( State(state.clone()), TypedHeader(Authorization::basic(&email, &password)), ) diff --git a/src/auth.rs b/src/auth.rs index d2cfb3e..a27deb2 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,11 +1,7 @@ -use argon2::{ - password_hash::{rand_core::OsRng, SaltString}, - Argon2, PasswordHash, PasswordHasher, PasswordVerifier, -}; -use axum::{extract::State, http::StatusCode, Router}; +use argon2::{Argon2, PasswordHash, PasswordVerifier}; +use axum::{extract::State, routing::get, Router}; use axum_extra::{ headers::{authorization::Basic, Authorization}, - routing::Resource, TypedHeader, }; use uuid::Uuid; @@ -17,11 +13,15 @@ use self::{error::Error, jwt::JWT}; pub use self::claims::{AccessClaims, RefreshClaims}; pub mod claims; +pub mod credentials; pub mod error; pub mod jwt; pub fn router() -> Router<AppState> { - axum::Router::new().merge(Resource::named("users").index(issue).create(create)) + Router::new() + .route("/issue", get(issue)) + .route("/refresh", get(refresh)) + .merge(credentials::router()) } pub async fn issue( @@ -42,28 +42,6 @@ pub async fn issue( Ok((access, refresh)) } -pub async fn create( - State(state): State<AppState>, - TypedHeader(Authorization(basic)): TypedHeader<Authorization<Basic>>, -) -> Result<(StatusCode, (AccessClaims, RefreshClaims)), Error> { - let salt = SaltString::generate(&mut OsRng); - let password_hash = Argon2::default().hash_password(basic.password().as_bytes(), &salt)?; - - let uuid = sqlx::query!( - "INSERT INTO credential (password_hash) VALUES ($1) RETURNING id", - password_hash.to_string() - ) - .fetch_optional(&state.pool) - .await? - .ok_or(Error::Registration)? - .id; - - let refresh = RefreshClaims::issue(uuid); - let access = refresh.refresh(); - - Ok((StatusCode::CREATED, (access, refresh))) -} - pub async fn refresh(claims: RefreshClaims) -> AccessClaims { claims.refresh() } @@ -83,17 +61,6 @@ mod tests { use crate::tests::{setup_test_env, TestResult}; - #[test] - fn test_jwt_encode_decode() -> TestResult { - setup_test_env(); - - let claims = AccessClaims::issue(uuid::Uuid::new_v4()); - let token = JWT.encode(&claims)?; - let decoded = JWT.decode(&token)?.claims; - assert_eq!(claims, decoded); - Ok(()) - } - #[sqlx::test(fixtures(path = "../fixtures", scripts("users")))] async fn test_issue_ok(pool: PgPool) -> TestResult { setup_test_env(); @@ -106,7 +73,7 @@ mod tests { ); let request = Request::builder() - .uri("/users") + .uri("/issue") .method("GET") .header(AUTHORIZATION, auth.0.encode()) .body(Body::empty())?; @@ -127,7 +94,7 @@ mod tests { let auth = Authorization::basic("4c14f795-86f0-4361-a02f-0edb966fb145", "hunter2"); let request = Request::builder() - .uri("/users") + .uri("/issue") .method("GET") .header(AUTHORIZATION, auth.0.encode()) .body(Body::empty())?; diff --git a/src/auth/credentials.rs b/src/auth/credentials.rs new file mode 100644 index 0000000..7f92048 --- /dev/null +++ b/src/auth/credentials.rs @@ -0,0 +1,66 @@ +use argon2::{ + password_hash::{rand_core::OsRng, SaltString}, + Argon2, PasswordHasher, +}; +use axum::{ + extract::{Path, State}, + http::StatusCode, +}; +use axum_extra::{ + headers::{authorization::Basic, Authorization}, + routing::Resource, + TypedHeader, +}; +use uuid::Uuid; + +use crate::state::AppState; + +use super::{error::Error, AccessClaims, RefreshClaims}; + +pub fn router() -> Resource<AppState> { + Resource::named("credentials") + .create(create) + .destroy(destroy) +} + +pub async fn create( + State(state): State<AppState>, + TypedHeader(Authorization(basic)): TypedHeader<Authorization<Basic>>, +) -> Result<(StatusCode, (AccessClaims, RefreshClaims)), Error> { + let salt = SaltString::generate(&mut OsRng); + let password_hash = Argon2::default().hash_password(basic.password().as_bytes(), &salt)?; + + let uuid = sqlx::query!( + "INSERT INTO credential (password_hash) VALUES ($1) RETURNING id", + password_hash.to_string() + ) + .fetch_optional(&state.pool) + .await? + .ok_or(Error::Registration)? + .id; + + let refresh = RefreshClaims::issue(uuid); + let access = refresh.refresh(); + + Ok((StatusCode::CREATED, (access, refresh))) +} + +pub async fn destroy(State(state): State<AppState>, Path(uuid): Path<Uuid>) -> Result<(), Error> { + let mut tx = state.pool.begin().await?; + let rows = sqlx::query!("DELETE FROM credential WHERE id = $1", uuid) + .execute(&mut *tx) + .await? + .rows_affected(); + + match rows { + 0 => Err(Error::UserNotFound), + 1 => { + tx.commit().await?; + Ok(()) + } + _ => { + tracing::error!("Delete query affected {rows} rows. This should not happen."); + Ok(()) + } + } +} diff --git a/src/auth/jwt.rs b/src/auth/jwt.rs index 9d70b94..0d7b593 100644 --- a/src/auth/jwt.rs +++ b/src/auth/jwt.rs @@ -40,3 +40,26 @@ impl Jwt { jsonwebtoken::decode(token, &self.decoding, &self.validation).map_err(Into::into) } } + +#[cfg(test)] +mod tests { + use super::*; + + use crate::{ + auth::AccessClaims, + tests::{setup_test_env, TestResult}, + }; + + #[test] + fn test_jwt_encode_decode() -> TestResult { + setup_test_env(); + + let claims = AccessClaims::issue(uuid::Uuid::new_v4()); + let token = JWT.encode(&claims)?; + let decoded = JWT.decode(&token)?.claims; + + assert_eq!(claims, decoded); + + Ok(()) + } +} |