Skip to main content

modo/auth/jwt/
config.rs

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