securitydept_oauth_provider/
shared.rs1use serde::Deserialize;
2use serde_with::{NoneAsEmptyString, serde_as};
3
4use crate::{OAuthProviderRemoteConfig, default_jwks_refresh_interval};
5
6#[serde_as]
39#[derive(Debug, Clone, Deserialize, Default)]
40pub struct OidcSharedConfig {
41 #[serde(flatten)]
43 pub remote: OAuthProviderRemoteConfig,
44
45 #[serde(default)]
49 #[serde_as(as = "NoneAsEmptyString")]
50 pub client_id: Option<String>,
51
52 #[serde(default)]
54 #[serde_as(as = "NoneAsEmptyString")]
55 pub client_secret: Option<String>,
56
57 #[serde_as(as = "securitydept_utils::ser::CommaOrSpaceSeparated<String>")]
60 #[serde(default)]
61 pub required_scopes: Vec<String>,
62}
63
64impl OidcSharedConfig {
65 pub fn resolve_remote(&self, local: &OAuthProviderRemoteConfig) -> OAuthProviderRemoteConfig {
70 OAuthProviderRemoteConfig {
71 well_known_url: local
72 .well_known_url
73 .clone()
74 .or_else(|| self.remote.well_known_url.clone()),
75 issuer_url: local
76 .issuer_url
77 .clone()
78 .or_else(|| self.remote.issuer_url.clone()),
79 jwks_uri: local
80 .jwks_uri
81 .clone()
82 .or_else(|| self.remote.jwks_uri.clone()),
83 metadata_refresh_interval: if local.metadata_refresh_interval.is_zero() {
84 self.remote.metadata_refresh_interval
85 } else {
86 local.metadata_refresh_interval
87 },
88 jwks_refresh_interval: if local.jwks_refresh_interval == default_jwks_refresh_interval()
89 {
90 self.remote.jwks_refresh_interval
91 } else {
92 local.jwks_refresh_interval
93 },
94 }
95 }
96
97 pub fn resolve_client_id(&self, local: Option<&str>) -> Option<String> {
103 local
104 .map(ToOwned::to_owned)
105 .or_else(|| self.client_id.clone())
106 }
107
108 pub fn resolve_client_secret(&self, local: Option<&str>) -> Option<String> {
110 local
111 .map(ToOwned::to_owned)
112 .or_else(|| self.client_secret.clone())
113 }
114
115 pub fn resolve_required_scopes(&self, local: &[String]) -> Vec<String> {
121 if !local.is_empty() {
122 local.to_vec()
123 } else {
124 self.required_scopes.clone()
125 }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use std::time::Duration;
132
133 use super::OidcSharedConfig;
134 use crate::OAuthProviderRemoteConfig;
135
136 #[test]
141 fn local_url_values_take_priority_over_shared() {
142 let shared = OidcSharedConfig {
143 remote: OAuthProviderRemoteConfig {
144 well_known_url: Some("https://shared.example.com/.well-known".to_string()),
145 issuer_url: Some("https://shared.example.com".to_string()),
146 jwks_uri: Some("https://shared.example.com/jwks".to_string()),
147 ..Default::default()
148 },
149 ..Default::default()
150 };
151 let local = OAuthProviderRemoteConfig {
152 well_known_url: Some("https://local.example.com/.well-known".to_string()),
153 ..Default::default()
154 };
155 let resolved = shared.resolve_remote(&local);
156
157 assert_eq!(
158 resolved.well_known_url.as_deref(),
159 Some("https://local.example.com/.well-known"),
160 "local well_known_url should take priority"
161 );
162 assert_eq!(
163 resolved.issuer_url.as_deref(),
164 Some("https://shared.example.com"),
165 "shared issuer_url should fill the gap"
166 );
167 assert_eq!(
168 resolved.jwks_uri.as_deref(),
169 Some("https://shared.example.com/jwks"),
170 "shared jwks_uri should fill the gap"
171 );
172 }
173
174 #[test]
175 fn empty_shared_returns_local_remote_unchanged() {
176 let shared = OidcSharedConfig::default();
177 let local = OAuthProviderRemoteConfig {
178 well_known_url: Some("https://local.example.com/.well-known".to_string()),
179 ..Default::default()
180 };
181 let resolved = shared.resolve_remote(&local);
182
183 assert_eq!(resolved.well_known_url, local.well_known_url);
184 assert!(resolved.issuer_url.is_none());
185 }
186
187 #[test]
188 fn local_interval_overrides_shared_interval() {
189 let shared = OidcSharedConfig {
190 remote: OAuthProviderRemoteConfig {
191 metadata_refresh_interval: Duration::from_secs(600),
192 ..Default::default()
193 },
194 ..Default::default()
195 };
196 let local = OAuthProviderRemoteConfig {
197 metadata_refresh_interval: Duration::from_secs(120),
198 ..Default::default()
199 };
200 let resolved = shared.resolve_remote(&local);
201
202 assert_eq!(
203 resolved.metadata_refresh_interval,
204 Duration::from_secs(120),
205 "non-zero local interval should take priority"
206 );
207 }
208
209 #[test]
210 fn zero_local_interval_falls_back_to_shared_interval() {
211 let shared = OidcSharedConfig {
212 remote: OAuthProviderRemoteConfig {
213 metadata_refresh_interval: Duration::from_secs(600),
214 ..Default::default()
215 },
216 ..Default::default()
217 };
218 let local = OAuthProviderRemoteConfig {
219 metadata_refresh_interval: Duration::ZERO,
220 ..Default::default()
221 };
222 let resolved = shared.resolve_remote(&local);
223
224 assert_eq!(
225 resolved.metadata_refresh_interval,
226 Duration::from_secs(600),
227 "zero local interval should fall back to shared"
228 );
229 }
230
231 #[test]
236 fn local_client_id_takes_priority_over_shared() {
237 let shared = OidcSharedConfig {
238 client_id: Some("shared-client".to_string()),
239 ..Default::default()
240 };
241
242 let resolved = shared.resolve_client_id(Some("local-client"));
243 assert_eq!(resolved.as_deref(), Some("local-client"));
244 }
245
246 #[test]
247 fn shared_client_id_fills_gap_when_local_is_absent() {
248 let shared = OidcSharedConfig {
249 client_id: Some("shared-client".to_string()),
250 ..Default::default()
251 };
252
253 let resolved = shared.resolve_client_id(None);
254 assert_eq!(resolved.as_deref(), Some("shared-client"));
255 }
256
257 #[test]
258 fn no_client_id_anywhere_returns_none() {
259 let shared = OidcSharedConfig::default();
260 let resolved = shared.resolve_client_id(None);
261 assert!(resolved.is_none());
262 }
263
264 #[test]
265 fn local_client_secret_takes_priority_over_shared() {
266 let shared = OidcSharedConfig {
267 client_secret: Some("shared-secret".to_string()),
268 ..Default::default()
269 };
270
271 let resolved = shared.resolve_client_secret(Some("local-secret"));
272 assert_eq!(resolved.as_deref(), Some("local-secret"));
273 }
274
275 #[test]
276 fn shared_client_secret_fills_gap_when_local_is_absent() {
277 let shared = OidcSharedConfig {
278 client_secret: Some("shared-secret".to_string()),
279 ..Default::default()
280 };
281
282 let resolved = shared.resolve_client_secret(None);
283 assert_eq!(resolved.as_deref(), Some("shared-secret"));
284 }
285
286 #[test]
287 fn deserialization_of_shared_config_from_flat_json() {
288 let json = serde_json::json!({
290 "well_known_url": "https://auth.example.com/.well-known/openid-configuration",
291 "client_id": "shared-app",
292 "client_secret": "s3cr3t"
293 });
294 let config: OidcSharedConfig =
295 serde_json::from_value(json).expect("shared config should deserialize");
296
297 assert_eq!(
298 config.remote.well_known_url.as_deref(),
299 Some("https://auth.example.com/.well-known/openid-configuration")
300 );
301 assert_eq!(config.client_id.as_deref(), Some("shared-app"));
302 assert_eq!(config.client_secret.as_deref(), Some("s3cr3t"));
303 }
304}