Files
imks/auth/jwt_decoder.rs
zhenyi 821537186e 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
2026-06-11 12:11:05 +08:00

120 lines
4.0 KiB
Rust

//! 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());
}
}