securitydept_oauth_provider/
config.rs1use 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#[serde_as]
13#[derive(Debug, Clone, Deserialize, Default)]
14pub struct OAuthProviderRemoteConfig {
15 #[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 #[serde(
32 default = "default_metadata_refresh_interval",
33 with = "humantime_serde"
34 )]
35 pub metadata_refresh_interval: Duration,
36 #[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 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#[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#[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}