#[derive(serde::Deserialize)]
struct MetaClaims {
exp: Option<i64>,
sub: Option<String>,
}
pub(crate) struct JwtMeta {
pub exp: Option<chrono::DateTime<chrono::Utc>>,
pub sub: Option<String>,
}
pub(crate) fn extract_meta(token: &str) -> Option<JwtMeta> {
jsonwebtoken::dangerous::insecure_decode::<MetaClaims>(token)
.ok()
.map(|td| JwtMeta {
exp: td
.claims
.exp
.and_then(|e| chrono::DateTime::from_timestamp(e, 0)),
sub: td.claims.sub,
})
}
pub(crate) fn extract_exp(token: &str) -> Option<chrono::DateTime<chrono::Utc>> {
extract_meta(token).and_then(|m| m.exp)
}
#[cfg(test)]
mod tests {
use super::*;
use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
fn make_jwt(payload: &serde_json::Value) -> String {
encode(
&Header::new(Algorithm::HS256),
payload,
&EncodingKey::from_secret(b"unused"),
)
.expect("test JWT encoding should not fail")
}
#[test]
fn extract_exp_returns_some_for_token_with_exp_claim() {
let token = make_jwt(&serde_json::json!({ "exp": 1700000000_i64 }));
let result = extract_exp(&token);
assert!(
result.is_some(),
"expected Some(DateTime) for token with exp claim"
);
let dt = result.unwrap();
assert_eq!(dt.timestamp(), 1700000000);
}
#[test]
fn extract_exp_returns_none_for_token_without_exp_claim() {
let token = make_jwt(&serde_json::json!({ "sub": "user@example.com" }));
let result = extract_exp(&token);
assert!(
result.is_none(),
"expected None for token without exp claim"
);
}
#[test]
fn extract_exp_returns_none_for_garbage_input() {
assert!(extract_exp("not.a.jwt").is_none());
assert!(extract_exp("").is_none());
}
#[test]
fn extract_exp_handles_real_world_claim_set_with_aud() {
let token = make_jwt(&serde_json::json!({
"iss": "https://api.dev.dasch.swiss",
"sub": "http://rdfh.ch/users/root",
"aud": ["Knora", "Sipi"],
"exp": 1782457152_i64,
"iat": 1779865152_i64,
"nbf": 1779865152_i64,
"jti": "abc-123",
"scope": "admin",
}));
let result = extract_exp(&token);
assert!(
result.is_some(),
"expected Some(DateTime) for a real-world token bearing an aud claim"
);
assert_eq!(result.unwrap().timestamp(), 1782457152);
}
#[test]
fn extract_exp_accepts_rs256_algorithm_header() {
let token = encode(
&Header::new(Algorithm::HS384),
&serde_json::json!({ "exp": 1800000000_i64 }),
&EncodingKey::from_secret(b"secret"),
)
.expect("test JWT encoding should not fail");
let result = extract_exp(&token);
assert!(result.is_some());
assert_eq!(result.unwrap().timestamp(), 1800000000);
}
#[test]
fn extract_meta_returns_sub_when_present() {
let token = make_jwt(
&serde_json::json!({ "sub": "http://rdfh.ch/users/root", "exp": 1700000000_i64 }),
);
let meta = extract_meta(&token).expect("expected Some for valid JWT");
assert_eq!(
meta.sub.as_deref(),
Some("http://rdfh.ch/users/root"),
"sub should be returned when present"
);
}
#[test]
fn extract_meta_returns_none_sub_when_absent() {
let token = make_jwt(&serde_json::json!({ "exp": 1700000000_i64 }));
let meta = extract_meta(&token).expect("expected Some for valid JWT");
assert!(meta.sub.is_none(), "sub should be None when not in payload");
}
#[test]
fn extract_meta_returns_both_exp_and_sub_when_both_present() {
let token = make_jwt(&serde_json::json!({
"sub": "http://rdfh.ch/users/root",
"exp": 1782457152_i64,
}));
let meta = extract_meta(&token).expect("expected Some for valid JWT");
assert_eq!(meta.sub.as_deref(), Some("http://rdfh.ch/users/root"));
assert!(meta.exp.is_some());
assert_eq!(meta.exp.unwrap().timestamp(), 1782457152);
}
#[test]
fn extract_meta_returns_some_with_all_none_for_empty_payload() {
let token = make_jwt(&serde_json::json!({}));
let meta = extract_meta(&token).expect("expected Some for valid JWT with empty payload");
assert!(
meta.exp.is_none(),
"exp should be None when payload is empty"
);
assert!(
meta.sub.is_none(),
"sub should be None when payload is empty"
);
}
#[test]
fn extract_meta_returns_none_for_non_jwt_input() {
assert!(
extract_meta("not.a.jwt").is_none(),
"non-JWT string should return None"
);
assert!(
extract_meta("").is_none(),
"empty string should return None"
);
assert!(
extract_meta("garbage").is_none(),
"garbage string should return None"
);
}
#[test]
fn extract_exp_still_returns_none_for_valid_jwt_without_exp() {
let token = make_jwt(&serde_json::json!({ "sub": "user@example.com" }));
assert!(
extract_exp(&token).is_none(),
"extract_exp must return None for a valid JWT without exp"
);
}
}