1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
use serde::Deserialize;
/// YAML configuration for JWT services.
///
/// # Example
///
/// ```yaml
/// jwt:
/// secret: "${JWT_SECRET}"
/// default_expiry: 3600
/// leeway: 5
/// issuer: "my-app"
/// audience: "api"
/// ```
#[non_exhaustive]
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default)]
pub struct JwtConfig {
/// HMAC secret used for signing and verifying tokens.
pub secret: String,
/// Default token lifetime in seconds. Applied automatically by `JwtEncoder::encode()`
/// when `claims.exp` is `None`. If `None`, tokens without an `exp` are rejected by the decoder.
pub default_expiry: Option<u64>,
/// Clock skew tolerance in seconds. Applied to both `exp` and `nbf` checks.
/// Defaults to `0` when omitted from YAML.
#[serde(default)]
pub leeway: u64,
/// Required issuer (`iss`). When set, `JwtDecoder::decode()` rejects tokens
/// whose `iss` does not match.
pub issuer: Option<String>,
/// Required audience (`aud`). When set, `JwtDecoder::decode()` rejects tokens
/// whose `aud` does not match.
pub audience: Option<String>,
}
impl JwtConfig {
/// Create a JWT configuration with the given HMAC signing secret.
pub fn new(secret: impl Into<String>) -> Self {
Self {
secret: secret.into(),
default_expiry: None,
leeway: 0,
issuer: None,
audience: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_full_config() {
let yaml = r#"
secret: "my-secret"
default_expiry: 3600
leeway: 5
issuer: "my-app"
audience: "api"
"#;
let config: JwtConfig = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(config.secret, "my-secret");
assert_eq!(config.default_expiry, Some(3600));
assert_eq!(config.leeway, 5);
assert_eq!(config.issuer.as_deref(), Some("my-app"));
assert_eq!(config.audience.as_deref(), Some("api"));
}
#[test]
fn deserialize_minimal_config() {
let yaml = r#"secret: "my-secret""#;
let config: JwtConfig = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(config.secret, "my-secret");
assert!(config.default_expiry.is_none());
assert_eq!(config.leeway, 0);
assert!(config.issuer.is_none());
assert!(config.audience.is_none());
}
#[test]
fn missing_secret_defaults_to_empty() {
let yaml = r#"leeway: 5"#;
let config: JwtConfig = serde_yaml_ng::from_str(yaml).unwrap();
assert!(config.secret.is_empty());
assert_eq!(config.leeway, 5);
}
}