Skip to main content

modkit_auth/
config.rs

1use crate::validation::ValidationConfig;
2use serde::{Deserialize, Serialize};
3
4/// Main authentication configuration
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct AuthConfig {
7    /// Leeway in seconds for time-based validations (exp, nbf)
8    #[serde(default = "default_leeway")]
9    pub leeway_seconds: i64,
10
11    /// Allowed issuers (if empty, any issuer is accepted)
12    #[serde(default)]
13    pub issuers: Vec<String>,
14
15    /// Allowed audiences (if empty, any audience is accepted)
16    #[serde(default)]
17    pub audiences: Vec<String>,
18
19    /// Whether the `exp` claim is required (default: `true`).
20    /// Set to `false` to allow tokens without an expiration claim.
21    #[serde(default = "default_require_exp")]
22    pub require_exp: bool,
23
24    /// JWKS configuration
25    #[serde(default)]
26    pub jwks: Option<JwksConfig>,
27}
28
29fn default_leeway() -> i64 {
30    60
31}
32
33fn default_require_exp() -> bool {
34    true
35}
36
37impl Default for AuthConfig {
38    fn default() -> Self {
39        Self {
40            leeway_seconds: default_leeway(),
41            issuers: Vec::new(),
42            audiences: Vec::new(),
43            require_exp: default_require_exp(),
44            jwks: None,
45        }
46    }
47}
48
49impl From<&AuthConfig> for ValidationConfig {
50    fn from(config: &AuthConfig) -> Self {
51        Self {
52            allowed_issuers: config.issuers.clone(),
53            allowed_audiences: config.audiences.clone(),
54            leeway_seconds: config.leeway_seconds,
55            require_exp: config.require_exp,
56        }
57    }
58}
59
60/// JWKS endpoint configuration
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct JwksConfig {
63    /// JWKS endpoint URL
64    pub uri: String,
65
66    /// Refresh interval in seconds (default: 300 = 5 minutes)
67    #[serde(default = "default_refresh_interval")]
68    pub refresh_interval_seconds: u64,
69
70    /// Maximum backoff in seconds (default: 3600 = 1 hour)
71    #[serde(default = "default_max_backoff")]
72    pub max_backoff_seconds: u64,
73}
74
75fn default_refresh_interval() -> u64 {
76    300
77}
78
79fn default_max_backoff() -> u64 {
80    3600
81}
82
83#[cfg(test)]
84#[cfg_attr(coverage_nightly, coverage(off))]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn test_default_config() {
90        let config = AuthConfig::default();
91        assert_eq!(config.leeway_seconds, 60);
92        assert!(config.issuers.is_empty());
93        assert!(config.audiences.is_empty());
94        assert!(config.require_exp);
95        assert!(config.jwks.is_none());
96    }
97
98    #[test]
99    fn test_auth_config_serialization() {
100        let config = AuthConfig {
101            leeway_seconds: 120,
102            issuers: vec!["https://auth.example.com".to_owned()],
103            audiences: vec!["api".to_owned()],
104            require_exp: true,
105            jwks: Some(JwksConfig {
106                uri: "https://auth.example.com/.well-known/jwks.json".to_owned(),
107                refresh_interval_seconds: 300,
108                max_backoff_seconds: 3600,
109            }),
110        };
111
112        let json = serde_json::to_string_pretty(&config).unwrap();
113        let deserialized: AuthConfig = serde_json::from_str(&json).unwrap();
114        assert_eq!(deserialized.leeway_seconds, 120);
115        assert_eq!(deserialized.issuers, vec!["https://auth.example.com"]);
116        assert_eq!(deserialized.audiences, vec!["api"]);
117        assert!(deserialized.require_exp);
118        let jwks = deserialized.jwks.expect("jwks should be present");
119        assert_eq!(jwks.uri, "https://auth.example.com/.well-known/jwks.json");
120        assert_eq!(jwks.refresh_interval_seconds, 300);
121        assert_eq!(jwks.max_backoff_seconds, 3600);
122    }
123
124    #[test]
125    fn test_jwks_config_serialization() {
126        let config = JwksConfig {
127            uri: "https://auth.example.com/.well-known/jwks.json".to_owned(),
128            refresh_interval_seconds: 300,
129            max_backoff_seconds: 3600,
130        };
131
132        let json = serde_json::to_string_pretty(&config).unwrap();
133        let deserialized: JwksConfig = serde_json::from_str(&json).unwrap();
134        assert_eq!(deserialized.uri, config.uri);
135        assert_eq!(
136            deserialized.refresh_interval_seconds,
137            config.refresh_interval_seconds
138        );
139        assert_eq!(deserialized.max_backoff_seconds, config.max_backoff_seconds);
140    }
141
142    #[test]
143    fn test_auth_config_to_validation_config() {
144        let auth_config = AuthConfig {
145            leeway_seconds: 30,
146            issuers: vec!["https://auth.example.com".to_owned()],
147            audiences: vec!["api".to_owned()],
148            require_exp: true,
149            jwks: None,
150        };
151        let validation_config = ValidationConfig::from(&auth_config);
152        assert_eq!(validation_config.allowed_issuers, auth_config.issuers);
153        assert_eq!(validation_config.allowed_audiences, auth_config.audiences);
154        assert_eq!(validation_config.leeway_seconds, auth_config.leeway_seconds);
155        assert!(validation_config.require_exp);
156    }
157
158    #[test]
159    fn test_require_exp_defaults_true_when_omitted() {
160        let json = r#"{"leeway_seconds": 60}"#;
161        let config: AuthConfig = serde_json::from_str(json).unwrap();
162        assert!(config.require_exp);
163    }
164
165    #[test]
166    fn test_require_exp_false_propagates_to_validation_config() {
167        let auth_config = AuthConfig {
168            require_exp: false,
169            ..Default::default()
170        };
171        let validation_config = ValidationConfig::from(&auth_config);
172        assert!(!validation_config.require_exp);
173    }
174
175    #[test]
176    fn test_jwks_config_defaults() {
177        let json = r#"{"uri": "https://example.com/jwks"}"#;
178        let config: JwksConfig = serde_json::from_str(json).unwrap();
179        assert_eq!(config.refresh_interval_seconds, 300);
180        assert_eq!(config.max_backoff_seconds, 3600);
181    }
182}