- count_bounded: break on iterator error instead of counting errors as tokens - collapse nested if-let chains with let-chains in auth middleware - document JWT/Basic Auth as mutually exclusive - TailTokensFilter::clone uses empty buffer (always pre-filter) - fix 9 broken doc examples in server/common.rs - remove 7 duplicate auth tests from auth.rs (covered by auth_tests.rs)
119 lines
3.4 KiB
Rust
119 lines
3.4 KiB
Rust
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<Claims, String> {
|
|
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<Claims> = decode::<Claims>(
|
|
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)
|
|
}
|