821537186e
- 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
120 lines
4.0 KiB
Rust
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());
|
|
}
|
|
}
|