refactor(tests): reformat code and update dependency management
- Reorganized import statements in adapter tests for better readability - Replaced or_insert_with(Vec::new) with or_default() in test closures - Updated Cargo.lock with new dependency versions and checksums - Added TLS features to tonic dependency configuration - Included sqlx, chrono, and uuid dependencies with specific features - Added jsonwebtoken and arc-swap as project dependencies - Reformatted assertion statements to comply with line length limits - Adjusted base64 import order in engine codec module - Updated protobuf include statement formatting
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
//! Low-level HS256 JWT decoding and verification.
|
||||
//!
|
||||
//! Stateless functions — no caching or key management.
|
||||
//! Used by the `Authenticator` in combination with `SigningKeyStore`.
|
||||
|
||||
use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode, decode_header};
|
||||
|
||||
use crate::{ImksError, ImksResult};
|
||||
|
||||
use super::claims::TokenClaims;
|
||||
|
||||
/// Expected JWT issuer claim.
|
||||
const EXPECTED_ISSUER: &str = "appks";
|
||||
/// Signing algorithm used by appks.
|
||||
const ALGORITHM: Algorithm = Algorithm::HS256;
|
||||
|
||||
/// Extract the `kid` from a JWT header without verifying the signature.
|
||||
///
|
||||
/// This is the first step in local verification: find which signing key
|
||||
/// was used, then look it up in the `SigningKeyStore`.
|
||||
pub fn extract_kid(token: &str) -> ImksResult<String> {
|
||||
let header = decode_header(token).map_err(map_jwt_error)?;
|
||||
header
|
||||
.kid
|
||||
.ok_or_else(|| ImksError::Auth("JWT header missing 'kid' field".into()))
|
||||
}
|
||||
|
||||
/// Verify an HS256 JWT signature and decode its claims.
|
||||
///
|
||||
/// Validates: algorithm, issuer, expiration. Does NOT validate audience.
|
||||
pub fn verify_and_decode(token: &str, key: &DecodingKey) -> ImksResult<TokenClaims> {
|
||||
let validation = build_validation();
|
||||
let token_data = decode::<TokenClaims>(token, key, &validation).map_err(map_jwt_error)?;
|
||||
Ok(token_data.claims)
|
||||
}
|
||||
|
||||
/// Build the standard `Validation` config for imks JWT verification.
|
||||
fn build_validation() -> Validation {
|
||||
let mut validation = Validation::new(ALGORITHM);
|
||||
validation.set_issuer(&[EXPECTED_ISSUER]);
|
||||
validation.validate_exp = true;
|
||||
// Audience validation not required for imks tokens.
|
||||
validation.validate_aud = false;
|
||||
validation
|
||||
}
|
||||
|
||||
/// Map `jsonwebtoken` errors to `ImksError`, distinguishing expired tokens.
|
||||
fn map_jwt_error(e: jsonwebtoken::errors::Error) -> ImksError {
|
||||
use jsonwebtoken::errors::ErrorKind;
|
||||
match e.kind() {
|
||||
ErrorKind::ExpiredSignature => ImksError::TokenExpired,
|
||||
ErrorKind::InvalidSignature => ImksError::Auth("invalid JWT signature".into()),
|
||||
ErrorKind::InvalidIssuer => ImksError::Auth("invalid JWT issuer".into()),
|
||||
_ => ImksError::Auth(format!("JWT error: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use jsonwebtoken::{EncodingKey, Header, encode};
|
||||
|
||||
fn make_test_token(claims: &TokenClaims, secret: &[u8]) -> String {
|
||||
let mut header = Header::new(ALGORITHM);
|
||||
header.kid = Some("test-kid".into());
|
||||
encode(&header, claims, &EncodingKey::from_secret(secret)).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_kid() {
|
||||
let claims = TokenClaims {
|
||||
sub: "u1".into(),
|
||||
iss: "appks".into(),
|
||||
iat: 1000,
|
||||
exp: 9999999999,
|
||||
jti: "j1".into(),
|
||||
scope: "im:read".into(),
|
||||
extra: Default::default(),
|
||||
};
|
||||
let token = make_test_token(&claims, b"secret");
|
||||
let kid = extract_kid(&token).unwrap();
|
||||
assert_eq!(kid, "test-kid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_and_decode_valid() {
|
||||
let secret = b"test-secret-key-material-32bytes!";
|
||||
let claims = TokenClaims {
|
||||
sub: "user-1".into(),
|
||||
iss: "appks".into(),
|
||||
iat: 1000,
|
||||
exp: 9999999999,
|
||||
jti: "tok-1".into(),
|
||||
scope: "im:read".into(),
|
||||
extra: Default::default(),
|
||||
};
|
||||
let token = make_test_token(&claims, secret);
|
||||
let key = DecodingKey::from_secret(secret);
|
||||
let decoded = verify_and_decode(&token, &key).unwrap();
|
||||
assert_eq!(decoded.sub, "user-1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_rejects_wrong_key() {
|
||||
let claims = TokenClaims {
|
||||
sub: "u1".into(),
|
||||
iss: "appks".into(),
|
||||
iat: 1000,
|
||||
exp: 9999999999,
|
||||
jti: "j1".into(),
|
||||
scope: "".into(),
|
||||
extra: Default::default(),
|
||||
};
|
||||
let token = make_test_token(&claims, b"correct-secret");
|
||||
let wrong_key = DecodingKey::from_secret(b"wrong-secret");
|
||||
let result = verify_and_decode(&token, &wrong_key);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user