summaryrefslogtreecommitdiffstats
path: root/src/routes
diff options
context:
space:
mode:
authorToby Vincent <tobyv@tobyvin.dev>2024-04-08 16:31:44 -0500
committerToby Vincent <tobyv@tobyvin.dev>2024-04-11 23:49:41 -0500
commitd9ed52fc239e3547eb99fe03bd296ab2808d2ebc (patch)
tree2fdc8a0e33bdf0902f608daa8e41d61df80ea9b2 /src/routes
parent9a6c04d52edb10431f9f5ca2dbc83c410cb5daee (diff)
wip: impl jwt handling
Diffstat (limited to 'src/routes')
-rw-r--r--src/routes/jwt.rs114
-rw-r--r--src/routes/login.rs5
-rw-r--r--src/routes/register.rs15
-rw-r--r--src/routes/user.rs10
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")