diff --git a/Cargo.lock b/Cargo.lock index 724e777..bf228d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,6 +123,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -780,6 +786,7 @@ name = "icarus" version = "0.1.100" dependencies = [ "axum", + "base64 0.21.7", "common-multipart-rfc7578", "futures", "icarus_envy", @@ -961,7 +968,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a808e078330e6af222eb0044b71d4b1ff981bfef43e7bc8133a88234e0c86a0c" dependencies = [ "anyhow", - "base64", + "base64 0.22.1", "flate2", "openssl", "regex", @@ -1792,7 +1799,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "crc", "crossbeam-queue", @@ -1868,7 +1875,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "bytes", @@ -1912,7 +1919,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "crc", diff --git a/Cargo.toml b/Cargo.toml index f1e4cc2..9791cff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ uuid = { version = "1.17.0", features = ["v4", "serde"] } sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio-native-tls", "time", "uuid"] } time = { version = "0.3.41", features = ["formatting", "macros", "parsing", "serde"] } thiserror = "1.0" +base64 = "0.21" josekit = { version = "0.10.1" } icarus_meta = { git = "ssh://git@git.kundeng.us/phoenix/icarus_meta.git", tag = "v0.3.0-devel-f4b71de969-680" } icarus_models = { git = "ssh://git@git.kundeng.us/phoenix/icarus_models.git", tag = "v0.5.1-devel-1c5de9dc26-111" } diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..18ae3ef --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,113 @@ +use std::collections::BTreeMap; + + +use axum::{ + http::{Request, StatusCode}, + middleware::Next, + response::{IntoResponse, Response}, + Json, +}; +use josekit::{ + jwt::{self, JwtPayload}, + jws::{self, JwsHeader, JwsSigner, JwsVerifier}, + jwk::Jwk, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use thiserror::Error; +use time::OffsetDateTime; + +#[derive(Debug, Serialize, Deserialize)] +pub struct UserClaims { + pub sub: String, // Subject (user ID) + pub exp: i64, // Expiration time (UTC timestamp) + pub iat: i64, // Issued at (UTC timestamp) + #[serde(skip_serializing_if = "Option::is_none")] + pub roles: Option>, // Optional roles +} + + +// use time::OffsetDateTime; + +#[derive(Error, Debug)] +pub enum JwtError { + #[error("Token creation failed")] + TokenCreation, + #[error("Token verification failed")] + TokenVerification, + #[error("Invalid token")] + InvalidToken, + #[error("Token expired")] + ExpiredToken, + #[error("Invalid key")] + InvalidKey, +} + +pub struct JwtAuth { + key: Jwk, // Now properly parameterized +} + +impl JwtAuth { + pub fn new(secret: &str) -> Result { + let mut key = Jwk::new("oct"); + key.set_parameter("k", Some(json!(base64::encode(secret)))) + .map_err(|_| JwtError::InvalidKey)?; + Ok(Self { key }) + } + + pub fn create_token(&self, claims: &UserClaims) -> Result { + let mut header = JwsHeader::new(); + header.set_token_type("JWT"); + + let claims_value = serde_json::to_value(claims) + .map_err(|_| JwtError::SerializationError)?; + let claims_map = claims_value.as_object() + .ok_or(JwtError::SerializationError)? + .to_owned(); + + let mut payload = JwtPayload::new(); + for (k, v) in claims_map { + payload.set_claim(&k, Some(v)) + .map_err(|_| JwtError::TokenCreation)?; + } + + let signer = jws::HS256.signer_from_jwk(&self.key) + .map_err(|_| JwtError::TokenCreation)?; + + jwt::encode_with_signer(&payload, &header, &signer) + .map_err(|_| JwtError::TokenCreation) + } + + pub fn verify_token(&self, token: &str) -> Result { + let verifier = jws::HS256.verifier_from_jwk(&self.key) + .map_err(|_| JwtError::TokenVerification)?; + + let (payload, _) = jwt::decode_with_verifier(token, &verifier) + .map_err(|_| JwtError::InvalidToken)?; + + // 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) + } +} diff --git a/src/main.rs b/src/main.rs index daef760..80e41f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +pub mod auth; pub mod callers; pub mod db { @@ -156,30 +157,6 @@ pub mod init { } } -mod auth { - use std::collections::BTreeMap; - - use serde::{Deserialize, Serialize}; - - use axum::{ - http::{Request, StatusCode}, - middleware::Next, - response::{IntoResponse, Response}, - Json, - }; - - use time::OffsetDateTime; - - #[derive(Debug, Serialize, Deserialize)] - pub struct UserClaims { - pub sub: String, // Subject (user ID) - pub exp: i64, // Expiration time (UTC timestamp) - pub iat: i64, // Issued at (UTC timestamp) - #[serde(skip_serializing_if = "Option::is_none")] - pub roles: Option>, // Optional roles - } - -} // TODO: Move elsewhere fn get_full() -> String {