Auth #169

Merged
kdeng00 merged 19 commits from auth into v0.2 2025-08-01 15:16:00 -04:00
3 changed files with 110 additions and 74 deletions
Showing only changes of commit c8ffd8411d - Show all commits
Generated
+35
View File
@@ -108,6 +108,29 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "axum-extra"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d"
dependencies = [
"axum",
"axum-core",
"bytes",
"cookie",
"futures-util",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"serde",
"tower",
"tower-layer",
"tower-service",
]
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.75" version = "0.3.75"
@@ -243,6 +266,17 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@@ -788,6 +822,7 @@ name = "icarus"
version = "0.1.100" version = "0.1.100"
dependencies = [ dependencies = [
"axum", "axum",
"axum-extra",
"base64 0.21.7", "base64 0.21.7",
"common-multipart-rfc7578", "common-multipart-rfc7578",
"futures", "futures",
+1
View File
@@ -6,6 +6,7 @@ rust-version = "1.88"
[dependencies] [dependencies]
axum = { version = "0.8.4", features = ["multipart"] } axum = { version = "0.8.4", features = ["multipart"] }
axum-extra = { version = "0.10.1", features = ["cookie"] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = { version = "1.0.140" } serde_json = { version = "1.0.140" }
tower = { version = "0.5.2", features = ["full"] } tower = { version = "0.5.2", features = ["full"] }
+68 -68
View File
@@ -1,24 +1,25 @@
use std::collections::BTreeMap; // use std::collections::BTreeMap;
// use std::sync::Arc;
use axum::{ use axum::{
// extract::State,
http::{Request, StatusCode}, http::{Request, StatusCode},
middleware::Next, middleware::Next,
response::{IntoResponse, Response}, response::{IntoResponse},
Json, Json,
}; };
use josekit::{ use axum_extra::extract::cookie::CookieJar;
jwt::{self, JwtPayload}, use jsonwebtoken::{decode, DecodingKey, Validation};
jws::{self, JwsHeader, JwsSigner, JwsVerifier},
jwk::Jwk,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{json, Value}; // use serde_json::{json, Value};
use thiserror::Error; use thiserror::Error;
use time::OffsetDateTime; // use time::OffsetDateTime;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct UserClaims { pub struct UserClaims {
pub aud: String, // Audience
pub sub: String, // Subject (user ID) pub sub: String, // Subject (user ID)
pub exp: i64, // Expiration time (UTC timestamp) pub exp: i64, // Expiration time (UTC timestamp)
pub iat: i64, // Issued at (UTC timestamp) pub iat: i64, // Issued at (UTC timestamp)
@@ -27,8 +28,6 @@ pub struct UserClaims {
} }
// use time::OffsetDateTime;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum JwtError { pub enum JwtError {
#[error("Token creation failed")] #[error("Token creation failed")]
@@ -43,71 +42,72 @@ pub enum JwtError {
InvalidKey, InvalidKey,
} }
/*
pub struct JwtAuth { pub struct JwtAuth {
key: Jwk, // Now properly parameterized key: Jwk, // Now properly parameterized
} }
*/
impl JwtAuth {
pub fn new(secret: &str) -> Result<Self, JwtError> { #[derive(Debug, Serialize)]
let mut key = Jwk::new("oct"); pub struct ErrorResponse {
key.set_parameter("k", Some(json!(base64::encode(secret)))) pub status: &'static str,
.map_err(|_| JwtError::InvalidKey)?; pub message: String,
Ok(Self { key })
} }
pub fn create_token(&self, claims: &UserClaims) -> Result<String, JwtError> { pub async fn auth<B>(
let mut header = JwsHeader::new(); cookie_jar: CookieJar,
header.set_token_type("JWT"); // State(data): State<Arc<AppState>>,
mut req: Request<axum::body::Body>,
let claims_value = serde_json::to_value(claims) next: Next,
.map_err(|_| JwtError::SerializationError)?; ) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
let claims_map = claims_value.as_object() let token = cookie_jar
.ok_or(JwtError::SerializationError)? .get("token")
.to_owned(); .map(|cookie| cookie.value().to_string())
.or_else(|| {
let mut payload = JwtPayload::new(); req.headers()
for (k, v) in claims_map { .get(axum::http::header::AUTHORIZATION)
payload.set_claim(&k, Some(v)) .and_then(|auth_header| auth_header.to_str().ok())
.map_err(|_| JwtError::TokenCreation)?; .and_then(|auth_value| {
if auth_value.starts_with("Bearer ") {
Some(auth_value[7..].to_owned())
} else {
None
} }
})
});
let signer = jws::HS256.signer_from_jwk(&self.key) let token = token.ok_or_else(|| {
.map_err(|_| JwtError::TokenCreation)?; let json_error = ErrorResponse {
status: "fail",
message: "You are not logged in, please provide token".to_string(),
};
(StatusCode::UNAUTHORIZED, Json(json_error))
})?;
jwt::encode_with_signer(&payload, &header, &signer) let claims = decode::<UserClaims>(
.map_err(|_| JwtError::TokenCreation) &token,
} // TODO: Replace with code to get secret from env
&DecodingKey::from_secret("my_super_secret".as_ref()),
&Validation::default(),
)
.map_err(|_| {
let json_error = ErrorResponse {
status: "fail",
message: "Invalid token".to_string(),
};
(StatusCode::UNAUTHORIZED, Json(json_error))
})?
.claims;
pub fn verify_token(&self, token: &str) -> Result<UserClaims, JwtError> { let user_id = uuid::Uuid::parse_str(&claims.sub).map_err(|_| {
let verifier = jws::HS256.verifier_from_jwk(&self.key) let json_error = ErrorResponse {
.map_err(|_| JwtError::TokenVerification)?; status: "fail",
message: "Invalid token".to_string(),
};
(StatusCode::UNAUTHORIZED, Json(json_error))
})?;
let (payload, _) = jwt::decode_with_verifier(token, &verifier) req.extensions_mut().insert(user_id);
.map_err(|_| JwtError::InvalidToken)?; Ok(next.run(req).await)
// Convert payload to JSON value
let mut claims_value = json!({});
if let Some(Value::Object(map)) = claims_value.as_object_mut() {
for key in payload.claim_names() {
if let Some(value) = payload.claim(key) {
map.insert(key.to_string(), value.clone());
}
}
}
let claims = serde_json::from_value(claims_value)
.map_err(|_| JwtError::InvalidToken)?;
// Check expiration
if let Some(Value::Number(exp)) = payload.claim("exp") {
if let Some(exp) = exp.as_i64() {
let now = OffsetDateTime::now_utc().unix_timestamp();
if exp < now {
return Err(JwtError::ExpiredToken);
}
}
}
Ok(claims)
}
} }