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#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
14#[derive(Debug, Clone, Deserialize, Default)]
15pub struct OAuthProviderRemoteConfig {
16 #[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 #[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 #[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 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#[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#[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}