Skip to main content

modo/auth/session/jwt/
config.rs

1use serde::Deserialize;
2
3use super::source::TokenSourceConfig;
4
5/// YAML configuration for JWT session services.
6///
7/// # Example
8///
9/// ```yaml
10/// jwt:
11///   signing_secret: "${JWT_SECRET}"
12///   issuer: "my-app"
13///   access_ttl_secs: 900
14///   refresh_ttl_secs: 2592000
15///   max_per_user: 20
16///   touch_interval_secs: 300
17///   stateful_validation: true
18///   access_source:
19///     kind: bearer
20///   refresh_source:
21///     kind: body
22///     field: refresh_token
23/// ```
24#[non_exhaustive]
25#[derive(Debug, Clone, Deserialize)]
26#[serde(default)]
27pub struct JwtSessionsConfig {
28    /// HMAC secret used for signing and verifying tokens.
29    pub signing_secret: String,
30    /// Required issuer (`iss`). When set, the decoder rejects tokens whose `iss` does not match.
31    pub issuer: Option<String>,
32    /// Access token lifetime in seconds (default: 900 = 15 minutes).
33    pub access_ttl_secs: u64,
34    /// Refresh token lifetime in seconds (default: 2592000 = 30 days).
35    pub refresh_ttl_secs: u64,
36    /// Maximum concurrent sessions per user (default: 20).
37    pub max_per_user: usize,
38    /// Minimum interval between session touch updates in seconds (default: 300).
39    pub touch_interval_secs: u64,
40    /// When `true`, tokens are validated against the session store on every request.
41    pub stateful_validation: bool,
42    /// Where to extract access tokens from incoming requests.
43    pub access_source: TokenSourceConfig,
44    /// Where to extract refresh tokens from incoming requests.
45    pub refresh_source: TokenSourceConfig,
46}
47
48impl Default for JwtSessionsConfig {
49    fn default() -> Self {
50        Self {
51            signing_secret: String::new(),
52            issuer: None,
53            access_ttl_secs: 900,
54            refresh_ttl_secs: 2_592_000,
55            max_per_user: 20,
56            touch_interval_secs: 300,
57            stateful_validation: true,
58            access_source: TokenSourceConfig::Bearer,
59            refresh_source: TokenSourceConfig::Body {
60                field: "refresh_token".into(),
61            },
62        }
63    }
64}
65
66impl JwtSessionsConfig {
67    /// Create a JWT sessions configuration with the given HMAC signing secret.
68    pub fn new(signing_secret: impl Into<String>) -> Self {
69        Self {
70            signing_secret: signing_secret.into(),
71            ..Self::default()
72        }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn deserialize_full_config() {
82        let yaml = r#"
83            signing_secret: "my-secret"
84            issuer: "my-app"
85            access_ttl_secs: 900
86            refresh_ttl_secs: 2592000
87            max_per_user: 20
88            touch_interval_secs: 300
89            stateful_validation: true
90        "#;
91        let config: JwtSessionsConfig = serde_yaml_ng::from_str(yaml).unwrap();
92        assert_eq!(config.signing_secret, "my-secret");
93        assert_eq!(config.issuer.as_deref(), Some("my-app"));
94        assert_eq!(config.access_ttl_secs, 900);
95        assert_eq!(config.refresh_ttl_secs, 2_592_000);
96        assert_eq!(config.max_per_user, 20);
97        assert_eq!(config.touch_interval_secs, 300);
98        assert!(config.stateful_validation);
99    }
100
101    #[test]
102    fn deserialize_minimal_config() {
103        let yaml = r#"signing_secret: "my-secret""#;
104        let config: JwtSessionsConfig = serde_yaml_ng::from_str(yaml).unwrap();
105        assert_eq!(config.signing_secret, "my-secret");
106        assert!(config.issuer.is_none());
107        assert_eq!(config.access_ttl_secs, 900);
108    }
109
110    #[test]
111    fn missing_secret_defaults_to_empty() {
112        let yaml = r#"access_ttl_secs: 1800"#;
113        let config: JwtSessionsConfig = serde_yaml_ng::from_str(yaml).unwrap();
114        assert!(config.signing_secret.is_empty());
115        assert_eq!(config.access_ttl_secs, 1800);
116    }
117
118    #[test]
119    fn default_values() {
120        let config = JwtSessionsConfig::default();
121        assert!(config.signing_secret.is_empty());
122        assert!(config.issuer.is_none());
123        assert_eq!(config.access_ttl_secs, 900);
124        assert_eq!(config.refresh_ttl_secs, 2_592_000);
125        assert_eq!(config.max_per_user, 20);
126        assert_eq!(config.touch_interval_secs, 300);
127        assert!(config.stateful_validation);
128    }
129
130    #[test]
131    fn new_sets_signing_secret() {
132        let config = JwtSessionsConfig::new("my-super-secret");
133        assert_eq!(config.signing_secret, "my-super-secret");
134        assert_eq!(config.access_ttl_secs, 900);
135    }
136}