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