//! 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 { 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 { let validation = build_validation(); let token_data = decode::(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()); } }