Skip to main content

securitydept_oauth_provider/
config.rs

1use std::time::Duration;
2
3use openidconnect::core::{CoreClientAuthMethod, CoreJwsSigningAlgorithm};
4use securitydept_utils::ser::CommaOrSpaceSeparated;
5use serde::Deserialize;
6use serde_with::{NoneAsEmptyString, PickFirst, serde_as};
7
8use crate::{OAuthProviderError, OAuthProviderResult};
9
10/// Shared provider connectivity settings used by both OIDC clients and
11/// resource-server verifiers.
12#[serde_as]
13#[derive(Debug, Clone, Deserialize, Default)]
14pub struct OAuthProviderRemoteConfig {
15    /// OpenID Connect discovery document URL.
16    ///
17    /// When set, the runtime fetches remote metadata and periodically refreshes
18    /// it when `metadata_refresh_interval > 0`.
19    #[serde(default)]
20    #[serde_as(as = "NoneAsEmptyString")]
21    pub well_known_url: Option<String>,
22    #[serde(default)]
23    #[serde_as(as = "NoneAsEmptyString")]
24    pub issuer_url: Option<String>,
25    #[serde(default)]
26    #[serde_as(as = "NoneAsEmptyString")]
27    pub jwks_uri: Option<String>,
28    /// Refresh interval for the discovery metadata cache.
29    ///
30    /// Set to `0` to disable periodic discovery refresh.
31    #[serde(
32        default = "default_metadata_refresh_interval",
33        with = "humantime_serde"
34    )]
35    pub metadata_refresh_interval: Duration,
36    /// Refresh interval for the remote JWKS cache.
37    ///
38    /// Set to `0` to disable time-based JWKS refresh.
39    #[serde(default = "default_jwks_refresh_interval", with = "humantime_serde")]
40    pub jwks_refresh_interval: Duration,
41}
42
43impl OAuthProviderRemoteConfig {
44    pub fn validate(&self) -> OAuthProviderResult<()> {
45        if !self.is_discovery_configured() {
46            return Err(OAuthProviderError::InvalidConfig {
47                message: "At least one of well_known_url or issuer_url or jwks_uri should be set"
48                    .to_string(),
49            });
50        }
51
52        Ok(())
53    }
54
55    /// Returns `true` when at least one discovery source is configured
56    /// (`well_known_url`, `issuer_url`, or `jwks_uri`).
57    ///
58    /// When `false`, no OIDC discovery or JWK resolution can take place,
59    /// meaning a resource-server verifier should **not** be constructed.
60    pub fn is_discovery_configured(&self) -> bool {
61        self.well_known_url.is_some() || self.issuer_url.is_some() || self.jwks_uri.is_some()
62    }
63}
64
65/// OIDC-specific provider metadata overrides.
66#[serde_as]
67#[derive(Debug, Clone, Deserialize, Default)]
68pub struct OAuthProviderOidcConfig {
69    #[serde(default)]
70    #[serde_as(as = "NoneAsEmptyString")]
71    pub authorization_endpoint: Option<String>,
72    #[serde(default)]
73    #[serde_as(as = "NoneAsEmptyString")]
74    pub token_endpoint: Option<String>,
75    #[serde(default)]
76    #[serde_as(as = "NoneAsEmptyString")]
77    pub userinfo_endpoint: Option<String>,
78    #[serde(default)]
79    #[serde_as(as = "NoneAsEmptyString")]
80    pub introspection_endpoint: Option<String>,
81    #[serde(default)]
82    #[serde_as(as = "NoneAsEmptyString")]
83    pub revocation_endpoint: Option<String>,
84    #[serde(default)]
85    #[serde_as(as = "NoneAsEmptyString")]
86    pub device_authorization_endpoint: Option<String>,
87    #[serde_as(as = "Option<PickFirst<(CommaOrSpaceSeparated<CoreClientAuthMethod>, _)>>")]
88    #[serde(default)]
89    pub token_endpoint_auth_methods_supported: Option<Vec<CoreClientAuthMethod>>,
90    #[serde_as(as = "Option<PickFirst<(CommaOrSpaceSeparated<CoreJwsSigningAlgorithm>, _)>>")]
91    #[serde(default)]
92    pub id_token_signing_alg_values_supported: Option<Vec<CoreJwsSigningAlgorithm>>,
93    #[serde_as(as = "Option<PickFirst<(CommaOrSpaceSeparated<CoreJwsSigningAlgorithm>, _)>>")]
94    #[serde(default)]
95    pub userinfo_signing_alg_values_supported: Option<Vec<CoreJwsSigningAlgorithm>>,
96}
97
98/// Normalized provider runtime config.
99#[serde_as]
100#[derive(Debug, Clone, Deserialize, Default)]
101pub struct OAuthProviderConfig {
102    #[serde(flatten)]
103    pub remote: OAuthProviderRemoteConfig,
104    #[serde(flatten)]
105    pub oidc: OAuthProviderOidcConfig,
106}
107
108impl OAuthProviderConfig {
109    pub fn validate(&self) -> OAuthProviderResult<()> {
110        self.remote.validate()
111    }
112}
113
114pub fn default_metadata_refresh_interval() -> Duration {
115    Duration::ZERO
116}
117
118pub fn default_jwks_refresh_interval() -> Duration {
119    Duration::from_secs(300)
120}
121
122pub fn default_id_token_signing_alg_values_supported() -> Vec<CoreJwsSigningAlgorithm> {
123    vec![CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256]
124}
125
126#[cfg(test)]
127mod tests {
128    use openidconnect::core::{CoreClientAuthMethod, CoreJwsSigningAlgorithm};
129
130    use super::{OAuthProviderConfig, OAuthProviderOidcConfig, OAuthProviderRemoteConfig};
131
132    #[test]
133    fn deserialize_empty_strings_as_none() {
134        let config: OAuthProviderConfig = serde_json::from_value(serde_json::json!({
135            "well_known_url": "",
136            "issuer_url": "https://issuer.example.com",
137            "jwks_uri": "https://issuer.example.com/jwks"
138        }))
139        .expect("config should deserialize");
140
141        assert!(config.remote.well_known_url.is_none());
142        assert_eq!(
143            config.remote.issuer_url.as_deref(),
144            Some("https://issuer.example.com")
145        );
146    }
147
148    #[test]
149    fn deserialize_space_or_comma_separated_algorithms() {
150        let config: OAuthProviderConfig = serde_json::from_value(serde_json::json!({
151            "issuer_url": "https://issuer.example.com",
152            "jwks_uri": "https://issuer.example.com/jwks",
153            "token_endpoint_auth_methods_supported": "client_secret_basic,private_key_jwt",
154            "id_token_signing_alg_values_supported": "RS256 ES256",
155            "userinfo_signing_alg_values_supported": ["RS256"]
156        }))
157        .expect("config should deserialize");
158
159        assert_eq!(
160            config.oidc.token_endpoint_auth_methods_supported,
161            Some(vec![
162                CoreClientAuthMethod::ClientSecretBasic,
163                CoreClientAuthMethod::PrivateKeyJwt,
164            ])
165        );
166        assert_eq!(
167            config.oidc.id_token_signing_alg_values_supported,
168            Some(vec![
169                CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256,
170                CoreJwsSigningAlgorithm::EcdsaP256Sha256,
171            ])
172        );
173        assert_eq!(
174            config.oidc.userinfo_signing_alg_values_supported,
175            Some(vec![CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256])
176        );
177    }
178
179    #[test]
180    fn validate_rejects_missing_manual_fields() {
181        let config = OAuthProviderConfig::default();
182
183        assert!(config.validate().is_err());
184    }
185
186    #[test]
187    fn validate_accepts_well_known_only() {
188        let config = OAuthProviderConfig {
189            remote: OAuthProviderRemoteConfig {
190                well_known_url: Some(
191                    "https://issuer.example.com/.well-known/openid-configuration".to_string(),
192                ),
193                ..Default::default()
194            },
195            oidc: OAuthProviderOidcConfig::default(),
196        };
197
198        assert!(config.validate().is_ok());
199    }
200}