use argon2::{Argon2, PasswordHash, PasswordVerifier}; use axum::{ async_trait, extract::{FromRequestParts, State}, http::request::Parts, RequestPartsExt, }; use axum_extra::{ headers::{authorization::Basic, Authorization}, TypedHeader, }; use sqlx::PgPool; use uuid::Uuid; use crate::state::AppState; 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() -> axum::Router { use axum::routing::get; axum::Router::new() .route("/issue", get(issue)) .route("/refresh", get(refresh)) .merge(credentials::router()) } pub async fn issue( State(pool): State, Account { id, password }: Account, ) -> Result<(AccessClaims, RefreshClaims), Error> { let p: String = sqlx::query_scalar!("SELECT password_hash FROM credential WHERE id = $1", id) .fetch_optional(&pool) .await? .ok_or(Error::LoginInvalid)?; Argon2::default().verify_password(password.as_bytes(), &PasswordHash::new(&p)?)?; let refresh = RefreshClaims::issue(id); let access = refresh.refresh(); Ok((access, refresh)) } pub async fn refresh(claims: RefreshClaims) -> AccessClaims { claims.refresh() } #[derive(Debug, Clone, PartialEq, Eq)] pub struct Account { pub id: Uuid, pub password: String, } #[async_trait] impl FromRequestParts for Account where S: Send + Sync, { type Rejection = Error; async fn from_request_parts(parts: &mut Parts, _: &S) -> Result { let TypedHeader(Authorization(basic)) = parts.extract::>>().await?; Ok(Self { id: Uuid::try_parse(basic.username())?, password: basic.password().to_string(), }) } } #[cfg(test)] mod tests { use super::*; use axum::{ body::Body, http::{header::AUTHORIZATION, Request, StatusCode}, Router, }; use axum_extra::headers::{authorization::Credentials, Authorization}; use tower::ServiceExt; use crate::tests::{setup_test_env, TestResult}; #[sqlx::test(fixtures(path = "../fixtures", scripts("users")))] async fn test_issue_ok(pool: PgPool) -> TestResult { setup_test_env(); let router = Router::new().merge(router()).with_state(AppState { pool }); let auth = Authorization::basic( "4c14f795-86f0-4361-a02f-0edb966fb145", "solongandthanksforallthefish", ); let request = Request::builder() .uri("/issue") .method("GET") .header(AUTHORIZATION, auth.0.encode()) .body(Body::empty())?; let response = router.oneshot(request).await?; assert_eq!(StatusCode::OK, response.status()); Ok(()) } #[sqlx::test(fixtures(path = "../fixtures", scripts("users")))] async fn test_issue_unauthorized(pool: PgPool) -> TestResult { setup_test_env(); let router = Router::new().merge(router()).with_state(AppState { pool }); let auth = Authorization::basic("4c14f795-86f0-4361-a02f-0edb966fb145", "hunter2"); let request = Request::builder() .uri("/issue") .method("GET") .header(AUTHORIZATION, auth.0.encode()) .body(Body::empty())?; let response = router.oneshot(request).await?; assert_eq!(StatusCode::UNAUTHORIZED, response.status()); Ok(()) } }