use axum::http::Method; use jsonwebtoken::{DecodingKey, TokenData, Validation, decode}; use log::debug; use serde::Deserialize; /// JWT claims for permission-based access control. /// /// External token generators should include these claims in the JWT payload. /// The server validates the signature and checks permissions for each request. /// /// # Example token payload /// /// ```json /// { /// "sub": "my-client", /// "exp": 1735689600, /// "read": true, /// "write": true, /// "delete": false /// } /// ``` #[derive(Debug, Deserialize)] pub struct Claims { /// Subject (client identifier). pub sub: String, /// Expiration time (Unix timestamp). pub exp: usize, /// Read permission (GET requests). #[serde(default)] pub read: bool, /// Write permission (POST/PUT requests). #[serde(default)] pub write: bool, /// Delete permission (DELETE requests). #[serde(default)] pub delete: bool, } /// Returns the required permission for an HTTP method. /// /// # Mapping /// /// - GET, HEAD → "read" /// - POST, PUT, PATCH → "write" /// - DELETE → "delete" /// /// # Arguments /// /// * `method` - The HTTP method of the incoming request. /// /// # Returns /// /// A string slice representing the required permission. pub fn required_permission(method: &Method) -> &'static str { if method == Method::GET || method == Method::HEAD { "read" } else if method == Method::DELETE { "delete" } else { "write" } } /// Checks if the JWT claims grant the required permission. /// /// # Arguments /// /// * `claims` - The validated JWT claims. /// * `permission` - The required permission string ("read", "write", or "delete"). /// /// # Returns /// /// `true` if the claims grant the permission, `false` otherwise. pub fn check_permission(claims: &Claims, permission: &str) -> bool { match permission { "read" => claims.read, "write" => claims.write, "delete" => claims.delete, _ => false, } } /// Validates a JWT token and returns the claims. /// /// Uses HMAC-SHA256 signature verification with the provided secret. /// /// # Arguments /// /// * `token` - The JWT token string (without "Bearer " prefix). /// * `secret` - The secret key used to verify the signature. /// /// # Returns /// /// * `Ok(Claims)` - The validated claims if the token is valid. /// * `Err(String)` - A human-readable error message if validation fails. pub fn validate_jwt(token: &str, secret: &str) -> Result { let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256); validation.algorithms = vec![jsonwebtoken::Algorithm::HS256]; validation.set_required_spec_claims(&["exp", "sub"]); let token_data: TokenData = decode::( token, &DecodingKey::from_secret(secret.as_bytes()), &validation, ) .map_err(|e| { debug!("JWT validation failed: {e}"); match e.kind() { jsonwebtoken::errors::ErrorKind::ExpiredSignature => "Token expired".to_string(), jsonwebtoken::errors::ErrorKind::InvalidSignature => "Invalid token".to_string(), jsonwebtoken::errors::ErrorKind::InvalidToken => "Malformed token".to_string(), jsonwebtoken::errors::ErrorKind::ImmatureSignature => "Token not yet valid".to_string(), _ => "Invalid token".to_string(), } })?; Ok(token_data.claims) }