Skip to main content

saml_rs/config/
builders.rs

1use crate::browser::{AcsEndpoint, SloEndpoint, SsoEndpoint};
2use crate::entity::EntitySetting;
3use crate::error::SamlError;
4
5use super::algorithms::{name_id_format_uris, transform_algorithm_uris, NameIdFormat};
6use super::credentials::Credentials;
7use super::descriptors::{validate_entity_id, EntityId, IdpMetadataConfig, SpMetadataConfig};
8use super::policies::{
9    assertion_signature_required, audience_validation_enabled, authn_request_signature_required,
10    authn_request_signing_enabled, logout_signature_required, message_signature_required,
11    name_id_creation_allowed, AlgorithmPolicy, AssertionEncryptionPolicy,
12    AuthnRequestSigningPolicy, IdpValidationPolicy, SpValidationPolicy, TemplatePolicy, XmlPolicy,
13};
14#[cfg(not(feature = "crypto-bergshamra"))]
15use super::policies::{
16    AssertionSignaturePolicy, AuthnRequestValidationPolicy, LogoutPolicy, LogoutSignaturePolicy,
17    MessageSignaturePolicy,
18};
19
20/// Typed Service Provider configuration.
21///
22/// # Examples
23///
24/// ```
25/// use saml_rs::{AcsEndpoint, EntityId, SpConfig, SpMetadataConfig};
26///
27/// let acs = AcsEndpoint::post("https://sp.example.com/acs")?;
28/// let config = SpConfig::try_new(
29///     EntityId::try_new("https://sp.example.com/metadata")?,
30///     SpMetadataConfig::new(vec![acs]),
31/// )?;
32///
33/// assert_eq!(config.entity_id.as_str(), "https://sp.example.com/metadata");
34/// # Ok::<(), saml_rs::SamlError>(())
35/// ```
36#[derive(Debug, Clone)]
37pub struct SpConfig {
38    /// Local SP entity ID.
39    pub entity_id: EntityId,
40    /// Local SP metadata inputs.
41    pub metadata: SpMetadataConfig,
42    /// Local credentials.
43    pub credentials: Credentials,
44    /// Validation and outbound signing policy.
45    pub validation: SpValidationPolicy,
46    /// Algorithm policy.
47    pub algorithms: AlgorithmPolicy,
48    /// XML parser, redirect, clock, and encryption policy.
49    pub xml: XmlPolicy,
50    /// Template and prefix policy.
51    pub templates: TemplatePolicy,
52}
53
54impl SpConfig {
55    /// Create SP configuration with required identity and metadata inputs.
56    ///
57    /// This convenience constructor accepts already-validated typed inputs but
58    /// does not validate the final config. Use [`Self::try_new`] or
59    /// [`Self::builder`] for caller-provided setup.
60    pub fn new(entity_id: EntityId, metadata: SpMetadataConfig) -> Self {
61        Self {
62            entity_id,
63            metadata,
64            credentials: Credentials::default(),
65            validation: SpValidationPolicy::default(),
66            algorithms: AlgorithmPolicy::default(),
67            xml: XmlPolicy::default(),
68            templates: TemplatePolicy::default(),
69        }
70    }
71
72    /// Validate and create SP configuration with compatibility defaults.
73    ///
74    /// # Errors
75    ///
76    /// Returns [`SamlError`] when the entity ID is empty or required SP
77    /// metadata endpoints are missing.
78    pub fn try_new(entity_id: EntityId, metadata: SpMetadataConfig) -> Result<Self, SamlError> {
79        let config = Self::new(entity_id, metadata);
80        config.validate()?;
81        Ok(config)
82    }
83
84    /// Start a dependency-free SP config builder with strict typed defaults.
85    pub fn builder(entity_id: EntityId) -> SpConfigBuilder {
86        SpConfigBuilder::new(entity_id)
87    }
88
89    /// Validate required SP config fields.
90    ///
91    /// # Errors
92    ///
93    /// Returns [`SamlError`] when the entity ID is empty or required SP
94    /// metadata endpoints are missing.
95    pub fn validate(&self) -> Result<(), SamlError> {
96        validate_entity_id(&self.entity_id)?;
97        self.metadata.validate()?;
98        validate_sp_policy(self)
99    }
100}
101
102/// Dependency-free builder for [`SpConfig`].
103#[derive(Debug, Clone)]
104pub struct SpConfigBuilder {
105    entity_id: EntityId,
106    metadata: SpMetadataConfig,
107    credentials: Credentials,
108    validation: SpValidationPolicy,
109    algorithms: AlgorithmPolicy,
110    xml: XmlPolicy,
111    templates: TemplatePolicy,
112}
113
114impl SpConfigBuilder {
115    fn new(entity_id: EntityId) -> Self {
116        Self {
117            entity_id,
118            metadata: SpMetadataConfig::new(Vec::new()),
119            credentials: Credentials::default(),
120            validation: SpValidationPolicy::strict(),
121            algorithms: AlgorithmPolicy::default(),
122            xml: XmlPolicy::default(),
123            templates: TemplatePolicy::default(),
124        }
125    }
126
127    /// Add an ACS endpoint.
128    pub fn acs_endpoint(mut self, endpoint: AcsEndpoint) -> Self {
129        self.metadata.assertion_consumer_service.push(endpoint);
130        self
131    }
132
133    /// Add an SLO endpoint.
134    pub fn slo_endpoint(mut self, endpoint: SloEndpoint) -> Self {
135        self.metadata.single_logout_service.push(endpoint);
136        self
137    }
138
139    /// Add an advertised NameID format.
140    pub fn name_id_format(mut self, format: NameIdFormat) -> Self {
141        self.metadata.name_id_format.push(format);
142        self
143    }
144
145    /// Set generated metadata element ordering.
146    pub fn elements_order(mut self, elements_order: Vec<String>) -> Self {
147        self.metadata.elements_order = Some(elements_order);
148        self
149    }
150
151    /// Set local credentials.
152    pub fn credentials(mut self, credentials: Credentials) -> Self {
153        self.credentials = credentials;
154        self
155    }
156
157    /// Set SP validation and outbound signing policy.
158    pub fn validation(mut self, validation: SpValidationPolicy) -> Self {
159        self.validation = validation;
160        self
161    }
162
163    /// Set algorithm policy.
164    pub fn algorithms(mut self, algorithms: AlgorithmPolicy) -> Self {
165        self.algorithms = algorithms;
166        self
167    }
168
169    /// Set XML policy.
170    pub fn xml(mut self, xml: XmlPolicy) -> Self {
171        self.xml = xml;
172        self
173    }
174
175    /// Set template policy.
176    pub fn templates(mut self, templates: TemplatePolicy) -> Self {
177        self.templates = templates;
178        self
179    }
180
181    /// Build and validate the SP config.
182    ///
183    /// # Errors
184    ///
185    /// Returns [`SamlError`] when required fields are missing, selected policy
186    /// needs unavailable credentials, or selected policy requires crypto in a
187    /// no-default-features build.
188    pub fn build(self) -> Result<SpConfig, SamlError> {
189        let config = SpConfig {
190            entity_id: self.entity_id,
191            metadata: self.metadata,
192            credentials: self.credentials,
193            validation: self.validation,
194            algorithms: self.algorithms,
195            xml: self.xml,
196            templates: self.templates,
197        };
198        config.validate()?;
199        Ok(config)
200    }
201}
202
203/// Typed Identity Provider configuration.
204///
205/// # Examples
206///
207/// The builder starts with strict validation defaults. Use compatibility
208/// policy explicitly when compiling or testing without the default crypto
209/// feature.
210///
211/// ```
212/// use saml_rs::{EntityId, IdpConfig, IdpValidationPolicy, SsoEndpoint};
213///
214/// let config = IdpConfig::builder(EntityId::try_new("https://idp.example.com/metadata")?)
215///     .sso_endpoint(SsoEndpoint::post("https://idp.example.com/sso")?)
216///     .validation(IdpValidationPolicy::compatibility())
217///     .build()?;
218///
219/// assert_eq!(config.entity_id.as_str(), "https://idp.example.com/metadata");
220/// # Ok::<(), saml_rs::SamlError>(())
221/// ```
222#[derive(Debug, Clone)]
223pub struct IdpConfig {
224    /// Local IdP entity ID.
225    pub entity_id: EntityId,
226    /// Local IdP metadata inputs.
227    pub metadata: IdpMetadataConfig,
228    /// Local credentials.
229    pub credentials: Credentials,
230    /// Validation policy.
231    pub validation: IdpValidationPolicy,
232    /// Algorithm policy.
233    pub algorithms: AlgorithmPolicy,
234    /// XML parser, redirect, clock, and encryption policy.
235    pub xml: XmlPolicy,
236    /// Template and prefix policy.
237    pub templates: TemplatePolicy,
238}
239
240impl IdpConfig {
241    /// Create IdP configuration with required identity and metadata inputs.
242    ///
243    /// This convenience constructor accepts already-validated typed inputs but
244    /// does not validate the final config. Use [`Self::try_new`] or
245    /// [`Self::builder`] for caller-provided setup.
246    pub fn new(entity_id: EntityId, metadata: IdpMetadataConfig) -> Self {
247        Self {
248            entity_id,
249            metadata,
250            credentials: Credentials::default(),
251            validation: IdpValidationPolicy::default(),
252            algorithms: AlgorithmPolicy::default(),
253            xml: XmlPolicy::default(),
254            templates: TemplatePolicy::default(),
255        }
256    }
257
258    /// Validate and create IdP configuration with compatibility defaults.
259    ///
260    /// # Errors
261    ///
262    /// Returns [`SamlError`] when the entity ID is empty or required IdP
263    /// metadata endpoints are missing.
264    pub fn try_new(entity_id: EntityId, metadata: IdpMetadataConfig) -> Result<Self, SamlError> {
265        let config = Self::new(entity_id, metadata);
266        config.validate()?;
267        Ok(config)
268    }
269
270    /// Start a dependency-free IdP config builder with strict typed defaults.
271    pub fn builder(entity_id: EntityId) -> IdpConfigBuilder {
272        IdpConfigBuilder::new(entity_id)
273    }
274
275    /// Validate required IdP config fields.
276    ///
277    /// # Errors
278    ///
279    /// Returns [`SamlError`] when the entity ID is empty or required IdP
280    /// metadata endpoints are missing.
281    pub fn validate(&self) -> Result<(), SamlError> {
282        validate_entity_id(&self.entity_id)?;
283        self.metadata.validate()?;
284        validate_idp_policy(self)
285    }
286}
287
288/// Dependency-free builder for [`IdpConfig`].
289#[derive(Debug, Clone)]
290pub struct IdpConfigBuilder {
291    entity_id: EntityId,
292    metadata: IdpMetadataConfig,
293    credentials: Credentials,
294    validation: IdpValidationPolicy,
295    algorithms: AlgorithmPolicy,
296    xml: XmlPolicy,
297    templates: TemplatePolicy,
298}
299
300impl IdpConfigBuilder {
301    fn new(entity_id: EntityId) -> Self {
302        Self {
303            entity_id,
304            metadata: IdpMetadataConfig::new(Vec::new()),
305            credentials: Credentials::default(),
306            validation: IdpValidationPolicy::strict(),
307            algorithms: AlgorithmPolicy::default(),
308            xml: XmlPolicy::default(),
309            templates: TemplatePolicy::default(),
310        }
311    }
312
313    /// Add an SSO endpoint.
314    pub fn sso_endpoint(mut self, endpoint: SsoEndpoint) -> Self {
315        self.metadata.single_sign_on_service.push(endpoint);
316        self
317    }
318
319    /// Add an SLO endpoint.
320    pub fn slo_endpoint(mut self, endpoint: SloEndpoint) -> Self {
321        self.metadata.single_logout_service.push(endpoint);
322        self
323    }
324
325    /// Add an advertised NameID format.
326    pub fn name_id_format(mut self, format: NameIdFormat) -> Self {
327        self.metadata.name_id_format.push(format);
328        self
329    }
330
331    /// Set generated metadata element ordering.
332    pub fn elements_order(mut self, elements_order: Vec<String>) -> Self {
333        self.metadata.elements_order = Some(elements_order);
334        self
335    }
336
337    /// Set local credentials.
338    pub fn credentials(mut self, credentials: Credentials) -> Self {
339        self.credentials = credentials;
340        self
341    }
342
343    /// Set IdP validation policy.
344    pub fn validation(mut self, validation: IdpValidationPolicy) -> Self {
345        self.validation = validation;
346        self
347    }
348
349    /// Set algorithm policy.
350    pub fn algorithms(mut self, algorithms: AlgorithmPolicy) -> Self {
351        self.algorithms = algorithms;
352        self
353    }
354
355    /// Set XML policy.
356    pub fn xml(mut self, xml: XmlPolicy) -> Self {
357        self.xml = xml;
358        self
359    }
360
361    /// Set template policy.
362    pub fn templates(mut self, templates: TemplatePolicy) -> Self {
363        self.templates = templates;
364        self
365    }
366
367    /// Build and validate the IdP config.
368    ///
369    /// # Errors
370    ///
371    /// Returns [`SamlError`] when required fields are missing or selected
372    /// policy requires crypto in a no-default-features build.
373    pub fn build(self) -> Result<IdpConfig, SamlError> {
374        let config = IdpConfig {
375            entity_id: self.entity_id,
376            metadata: self.metadata,
377            credentials: self.credentials,
378            validation: self.validation,
379            algorithms: self.algorithms,
380            xml: self.xml,
381            templates: self.templates,
382        };
383        config.validate()?;
384        Ok(config)
385    }
386}
387
388fn validate_common_credentials(credentials: &Credentials) -> Result<(), SamlError> {
389    if credentials.signing_key_passphrase.is_some() && credentials.signing_key.is_none() {
390        return Err(SamlError::MissingKey("signing_key".into()));
391    }
392    if credentials.decryption_key_passphrase.is_some() && credentials.decryption_key.is_none() {
393        return Err(SamlError::MissingKey("decryption_key".into()));
394    }
395    Ok(())
396}
397
398fn validate_sp_policy(config: &SpConfig) -> Result<(), SamlError> {
399    validate_sp_crypto_support(config)?;
400    validate_common_credentials(&config.credentials)?;
401    if matches!(
402        config.validation.authn_requests,
403        AuthnRequestSigningPolicy::Sign
404    ) {
405        validate_signing_credentials(&config.credentials)?;
406    }
407    if matches!(
408        config.xml.encryption.assertions,
409        AssertionEncryptionPolicy::EncryptAssertions
410    ) && config.credentials.decryption_key.is_none()
411    {
412        return Err(SamlError::MissingKey("decryption_key".into()));
413    }
414    Ok(())
415}
416
417fn validate_idp_policy(config: &IdpConfig) -> Result<(), SamlError> {
418    validate_idp_crypto_support(config)?;
419    validate_common_credentials(&config.credentials)
420}
421
422fn validate_signing_credentials(credentials: &Credentials) -> Result<(), SamlError> {
423    if credentials.signing_key.is_none() {
424        return Err(SamlError::MissingKey("signing_key".into()));
425    }
426    if credentials.signing_certificate.is_none() {
427        return Err(SamlError::MissingKey("signing_certificate".into()));
428    }
429    Ok(())
430}
431
432#[cfg(feature = "crypto-bergshamra")]
433fn validate_sp_crypto_support(_config: &SpConfig) -> Result<(), SamlError> {
434    Ok(())
435}
436
437#[cfg(not(feature = "crypto-bergshamra"))]
438fn validate_sp_crypto_support(config: &SpConfig) -> Result<(), SamlError> {
439    if sp_config_requires_crypto(config) {
440        return Err(SamlError::Unsupported(
441            "selected SP config policy requires the crypto-bergshamra feature".into(),
442        ));
443    }
444    Ok(())
445}
446
447#[cfg(feature = "crypto-bergshamra")]
448fn validate_idp_crypto_support(_config: &IdpConfig) -> Result<(), SamlError> {
449    Ok(())
450}
451
452#[cfg(not(feature = "crypto-bergshamra"))]
453fn validate_idp_crypto_support(config: &IdpConfig) -> Result<(), SamlError> {
454    if idp_config_requires_crypto(config) {
455        return Err(SamlError::Unsupported(
456            "selected IdP config policy requires the crypto-bergshamra feature".into(),
457        ));
458    }
459    Ok(())
460}
461
462#[cfg(not(feature = "crypto-bergshamra"))]
463fn sp_config_requires_crypto(config: &SpConfig) -> bool {
464    matches!(
465        config.validation.assertions,
466        AssertionSignaturePolicy::RequireSigned
467    ) || matches!(
468        config.validation.messages,
469        MessageSignaturePolicy::RequireSigned
470    ) || matches!(
471        config.validation.authn_requests,
472        AuthnRequestSigningPolicy::Sign
473    ) || logout_policy_requires_crypto(config.validation.logout)
474        || matches!(
475            config.xml.encryption.assertions,
476            AssertionEncryptionPolicy::EncryptAssertions
477        )
478}
479
480#[cfg(not(feature = "crypto-bergshamra"))]
481fn idp_config_requires_crypto(config: &IdpConfig) -> bool {
482    matches!(
483        config.validation.authn_requests,
484        AuthnRequestValidationPolicy::RequireSigned
485    ) || logout_policy_requires_crypto(config.validation.logout)
486        || matches!(
487            config.xml.encryption.assertions,
488            AssertionEncryptionPolicy::EncryptAssertions
489        )
490}
491
492#[cfg(not(feature = "crypto-bergshamra"))]
493fn logout_policy_requires_crypto(policy: LogoutPolicy) -> bool {
494    logout_signature_policy_requires_crypto(policy.requests)
495        || logout_signature_policy_requires_crypto(policy.responses)
496}
497
498#[cfg(not(feature = "crypto-bergshamra"))]
499fn logout_signature_policy_requires_crypto(policy: LogoutSignaturePolicy) -> bool {
500    matches!(policy, LogoutSignaturePolicy::RequireSigned)
501}
502
503fn apply_common_settings(
504    entity_id: &EntityId,
505    name_id_format: &[NameIdFormat],
506    credentials: &Credentials,
507    algorithms: &AlgorithmPolicy,
508    xml: &XmlPolicy,
509    templates: &TemplatePolicy,
510    setting: &mut EntitySetting,
511) {
512    setting.entity_id = Some(entity_id.as_str().to_string());
513    setting.request_signature_algorithm = algorithms.signature.as_uri().to_string();
514    setting.data_encryption_algorithm = algorithms.data_encryption.as_uri().to_string();
515    setting.key_encryption_algorithm = algorithms.key_encryption.as_uri().to_string();
516    setting.message_signing_order = algorithms.message_signing_order;
517    setting.is_assertion_encrypted = matches!(
518        xml.encryption.assertions,
519        AssertionEncryptionPolicy::EncryptAssertions
520    );
521    setting.allow_insecure_software_rsa_key_transport_decryption = xml
522        .encryption
523        .allows_insecure_software_rsa_key_transport_decryption();
524    setting.relay_state = templates.relay_state.clone();
525    setting.name_id_format = name_id_format_uris(name_id_format);
526    setting.private_key = credentials
527        .signing_key
528        .as_ref()
529        .map(|key| key.as_str().to_string());
530    setting.private_key_pass = credentials
531        .signing_key_passphrase
532        .as_ref()
533        .map(|passphrase| passphrase.as_str().to_string());
534    setting.signing_cert = credentials
535        .signing_certificate
536        .as_ref()
537        .map(|certificate| certificate.as_str().to_string());
538    setting.encrypt_cert = credentials
539        .encryption_certificate
540        .as_ref()
541        .map(|certificate| certificate.as_str().to_string());
542    setting.enc_private_key = credentials
543        .decryption_key
544        .as_ref()
545        .map(|key| key.as_str().to_string());
546    setting.enc_private_key_pass = credentials
547        .decryption_key_passphrase
548        .as_ref()
549        .map(|passphrase| passphrase.as_str().to_string());
550    setting.clock_drifts = xml.clock_drifts;
551    setting.redirect_inflate_max_bytes = xml.redirect_inflate_max_bytes;
552    setting.xml_limits = xml.limits;
553    setting.tag_prefix_protocol = templates.tag_prefix_protocol.clone();
554    setting.tag_prefix_assertion = templates.tag_prefix_assertion.clone();
555    setting.tag_prefix_encrypted_assertion = templates.tag_prefix_encrypted_assertion.clone();
556    setting.login_response_template = templates.login_response_template.clone();
557    setting.login_request_template = templates.login_request_template.clone();
558    setting.logout_request_template = templates.logout_request_template.clone();
559    setting.logout_response_template = templates.logout_response_template.clone();
560    setting.signature_config = templates.signature_config.clone();
561    setting.transformation_algorithms =
562        transform_algorithm_uris(&algorithms.signed_reference_transforms);
563}
564
565impl TryFrom<&SpConfig> for EntitySetting {
566    type Error = SamlError;
567
568    fn try_from(config: &SpConfig) -> Result<Self, Self::Error> {
569        config.validate()?;
570        let mut setting = Self::default();
571        apply_common_settings(
572            &config.entity_id,
573            &config.metadata.name_id_format,
574            &config.credentials,
575            &config.algorithms,
576            &config.xml,
577            &config.templates,
578            &mut setting,
579        );
580        setting.allow_create = name_id_creation_allowed(config.validation.name_id_creation);
581        setting.authn_requests_signed =
582            authn_request_signing_enabled(config.validation.authn_requests);
583        setting.want_assertions_signed = assertion_signature_required(config.validation.assertions);
584        setting.validate_audience = audience_validation_enabled(config.validation.audience);
585        setting.want_message_signed = message_signature_required(config.validation.messages);
586        setting.want_logout_request_signed =
587            logout_signature_required(config.validation.logout.requests)?;
588        setting.want_logout_response_signed =
589            logout_signature_required(config.validation.logout.responses)?;
590        Ok(setting)
591    }
592}
593
594impl TryFrom<&IdpConfig> for EntitySetting {
595    type Error = SamlError;
596
597    fn try_from(config: &IdpConfig) -> Result<Self, Self::Error> {
598        config.validate()?;
599        let mut setting = Self::default();
600        apply_common_settings(
601            &config.entity_id,
602            &config.metadata.name_id_format,
603            &config.credentials,
604            &config.algorithms,
605            &config.xml,
606            &config.templates,
607            &mut setting,
608        );
609        setting.want_authn_requests_signed =
610            authn_request_signature_required(config.validation.authn_requests);
611        setting.want_logout_request_signed =
612            logout_signature_required(config.validation.logout.requests)?;
613        setting.want_logout_response_signed =
614            logout_signature_required(config.validation.logout.responses)?;
615        Ok(setting)
616    }
617}