diff options
author | Toby Vincent <tobyv@tobyvin.dev> | 2024-04-08 16:31:44 -0500 |
---|---|---|
committer | Toby Vincent <tobyv@tobyvin.dev> | 2024-04-11 23:49:41 -0500 |
commit | d9ed52fc239e3547eb99fe03bd296ab2808d2ebc (patch) | |
tree | 2fdc8a0e33bdf0902f608daa8e41d61df80ea9b2 /src/routes | |
parent | 9a6c04d52edb10431f9f5ca2dbc83c410cb5daee (diff) |
wip: impl jwt handling
Diffstat (limited to 'src/routes')
-rw-r--r-- | src/routes/jwt.rs | 114 | ||||
-rw-r--r-- | src/routes/login.rs | 5 | ||||
-rw-r--r-- | src/routes/register.rs | 15 | ||||
-rw-r--r-- | src/routes/user.rs | 10 |
4 files changed, 132 insertions, 12 deletions
diff --git a/src/routes/jwt.rs b/src/routes/jwt.rs new file mode 100644 index 0000000..6a229a3 --- /dev/null +++ b/src/routes/jwt.rs @@ -0,0 +1,114 @@ +use std::sync::Arc; + +use axum::{ + extract::{Request, State}, + response::IntoResponse, +}; +use axum_extra::{ + extract::{cookie::Cookie, CookieJar}, + headers::{authorization::Bearer, Authorization}, + routing::TypedPath, + TypedHeader, +}; +use jsonwebtoken::{DecodingKey, Validation}; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::{error::AuthError, state::AppState, Error}; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct Claims { + pub sub: Uuid, + pub iat: i64, + pub exp: i64, + pub jti: Uuid, +} + +impl Claims { + const MAX_AGE: i64 = 3600; + + pub fn new(sub: Uuid) -> Self { + let iat = OffsetDateTime::now_utc().unix_timestamp(); + let exp = iat + Self::MAX_AGE; + let jti = uuid::Uuid::new_v4(); + Self { sub, iat, exp, jti } + } + + pub fn encode(&self, secret: &[u8]) -> Result<String, jsonwebtoken::errors::Error> { + jsonwebtoken::encode( + &jsonwebtoken::Header::default(), + self, + &jsonwebtoken::EncodingKey::from_secret(secret), + ) + } +} + +impl From<Uuid> for Claims { + fn from(value: Uuid) -> Self { + Self::new(value) + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +struct Session { + jti: Uuid, + uuid: Uuid, +} + +#[derive(Debug, Deserialize, TypedPath)] +#[typed_path("/api/auth/refresh")] +pub struct Refresh; + +impl Refresh { + #[tracing::instrument] + pub async fn post( + self, + State(state): State<Arc<AppState>>, + TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>, + cookie_jar: CookieJar, + ) -> Result<impl IntoResponse, Error> { + let Claims { sub, .. } = jsonwebtoken::decode::<Claims>( + bearer.token(), + &DecodingKey::from_secret(state.jwt_secret.as_ref()), + &Validation::default(), + )? + .claims; + + let claims = Claims::from(sub); + + let token = jsonwebtoken::encode( + &jsonwebtoken::Header::default(), + &claims, + &jsonwebtoken::EncodingKey::from_secret(state.jwt_secret.as_ref()), + )?; + + let cookie = Cookie::build(("token", token)) + .expires(OffsetDateTime::from_unix_timestamp(claims.exp)?) + .secure(true) + .http_only(true); + + Ok(cookie_jar.add(cookie)) + } +} + +pub async fn authenticate( + State(state): State<Arc<AppState>>, + cookie_jar: CookieJar, + mut req: Request, +) -> Result<Request, AuthError> { + let token = cookie_jar + .get("token") + .ok_or(AuthError::JwtNotFound)? + .to_string(); + + let claims = jsonwebtoken::decode::<Claims>( + &token, + &DecodingKey::from_secret(state.jwt_secret.as_ref()), + &Validation::default(), + )? + .claims; + + req.extensions_mut().insert(claims); + Ok(req) +} diff --git a/src/routes/login.rs b/src/routes/login.rs index 67f8422..8843bd5 100644 --- a/src/routes/login.rs +++ b/src/routes/login.rs @@ -7,12 +7,13 @@ use serde::Deserialize; use crate::{ error::AuthError, - jwt::Claims, model::{LoginSchema, UserSchema}, state::AppState, Error, }; +use super::jwt::Claims; + #[derive(Debug, Deserialize, TypedPath)] #[typed_path("/api/login")] pub struct Login; @@ -40,7 +41,7 @@ impl Login { Argon2::default() .verify_password(password.as_bytes(), &PasswordHash::new(&password_hash)?)?; - let token = Claims::new(uuid, state.jwt_max_age).encode(state.jwt_secret.as_ref())?; + let token = Claims::from(uuid).encode(state.jwt_secret.as_ref())?; Authorization::bearer(&token) .map(TypedHeader) diff --git a/src/routes/register.rs b/src/routes/register.rs index d2a570c..2181808 100644 --- a/src/routes/register.rs +++ b/src/routes/register.rs @@ -6,13 +6,16 @@ use argon2::{ }; use axum::{extract::State, http::StatusCode, response::IntoResponse, Json}; use axum_extra::routing::TypedPath; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -use crate::{ - model::{RegisterSchema, UserSchema}, - state::AppState, - Error, -}; +use crate::{model::UserSchema, state::AppState, Error}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RegisterSchema { + pub name: String, + pub email: String, + pub password: String, +} #[derive(Debug, Deserialize, TypedPath)] #[typed_path("/api/register")] diff --git a/src/routes/user.rs b/src/routes/user.rs index e6e5c3d..3663ec6 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -4,7 +4,9 @@ use axum::{extract::State, response::IntoResponse, Extension, Json}; use axum_extra::routing::TypedPath; use serde::Deserialize; -use crate::{jwt::Claims, model::UserSchema, state::AppState, Error}; +use crate::{model::UserSchema, state::AppState, Error}; + +use super::jwt::Claims; #[derive(Debug, Deserialize, TypedPath)] #[typed_path("/api/user/:uuid")] @@ -33,7 +35,7 @@ impl User { pub async fn get( self, State(state): State<Arc<AppState>>, - Extension(Claims { sub, iat, exp }): Extension<Claims>, + Extension(Claims { sub, .. }): Extension<Claims>, ) -> Result<impl IntoResponse, Error> { sqlx::query_as!(UserSchema, "SELECT * FROM users WHERE uuid = $1", sub) .fetch_optional(&state.pool) @@ -136,7 +138,7 @@ mod tests { }); let router = init_router(state.clone()); - let token = Claims::new(UUID, JWT_MAX_AGE).encode(JWT_SECRET.as_ref())?; + let token = Claims::from(UUID).encode(JWT_SECRET.as_ref())?; let request = Request::builder() .uri("/api/user") @@ -168,7 +170,7 @@ mod tests { }); let router = init_router(state.clone()); - let token = Claims::new(UUID, JWT_MAX_AGE).encode("BAD_SECRET".as_ref())?; + let token = Claims::from(UUID).encode("BAD_SECRET".as_ref())?; let request = Request::builder() .uri("/api/user") |