1use crate::binding::{base64_encode, build_redirect_url};
4use crate::constants::{namespace, Binding, CertUse, ParserType};
5use crate::entity::{
6 generate_id, now_iso8601, BindingContext, CustomTagReplacement, EntitySetting,
7};
8use crate::error::SamlError;
9use crate::flow::{
10 flow_with_expected_recipient_and_signature_evidence, AssertionSignatureRequirement,
11 FlowOptions, FlowResult, FlowResultWithSignatureEvidence, HttpRequest,
12 ResponseSignatureRequirement,
13};
14use crate::idp::IdentityProvider;
15use crate::metadata::{generate_sp_metadata, SpMetadata, SpMetadataConfig};
16use crate::template::{replace_tags_by_optional_value, LOGIN_REQUEST_TEMPLATE};
17use crate::xml::write::XmlWriter;
18use std::time::SystemTime;
19
20#[derive(Debug, Clone)]
22pub struct ServiceProvider {
23 pub setting: EntitySetting,
25 pub metadata: SpMetadata,
27}
28
29#[derive(Default)]
31pub struct LoginRequestOptions<'a> {
32 pub relay_state: Option<&'a str>,
34 pub custom: Option<CustomTagReplacement<'a>>,
36 pub force_authn: Option<bool>,
38 pub assertion_consumer_service_index: Option<u16>,
41 pub response_binding: Option<Binding>,
43}
44
45struct AuthnRequestXml<'a> {
46 id: &'a str,
47 issue_instant: &'a str,
48 destination: &'a str,
49 force_authn: Option<bool>,
50 protocol_binding: Option<&'a str>,
51 assertion_consumer_service_url: Option<&'a str>,
52 assertion_consumer_service_index: Option<u16>,
53 issuer: &'a str,
54 name_id_format: Option<&'a str>,
55 allow_create: AllowCreateAttribute,
56}
57
58enum AllowCreateAttribute {
59 Include(bool),
60 Omit,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
69pub(crate) enum WebBrowserSsoProducer {
70 #[default]
72 Compatibility,
73 Follow,
75}
76
77impl WebBrowserSsoProducer {
78 pub(crate) fn follows(self) -> bool {
79 matches!(self, Self::Follow)
80 }
81}
82
83#[derive(Debug, Clone, Copy)]
84enum LoginResponseCorrelation<'a> {
85 Unsolicited,
86 MessageId(&'a str),
87}
88
89pub(crate) struct LoginResponseParseOptions<'a> {
90 expected_recipient: Option<&'a str>,
91 now: Option<SystemTime>,
92 clock_drifts: (i64, i64),
93}
94
95impl<'a> LoginResponseParseOptions<'a> {
96 fn compatibility(clock_drifts: (i64, i64)) -> Self {
97 Self {
98 expected_recipient: None,
99 now: None,
100 clock_drifts,
101 }
102 }
103
104 pub(crate) fn at(now: SystemTime, clock_drifts: (i64, i64)) -> Self {
105 Self {
106 expected_recipient: None,
107 now: Some(now),
108 clock_drifts,
109 }
110 }
111
112 pub(crate) fn with_expected_recipient(mut self, expected_recipient: &'a str) -> Self {
113 self.expected_recipient = Some(expected_recipient);
114 self
115 }
116}
117
118fn render_default_authn_request_xml(input: &AuthnRequestXml<'_>) -> String {
119 let force_authn = input.force_authn.map(|value| value.to_string());
120 let assertion_consumer_service_index = input
121 .assertion_consumer_service_index
122 .map(|value| value.to_string());
123 let allow_create = match input.allow_create {
124 AllowCreateAttribute::Include(true) => Some("true"),
125 AllowCreateAttribute::Include(false) => Some("false"),
126 AllowCreateAttribute::Omit => None,
127 };
128 let mut name_id_policy_attrs = Vec::new();
129 if let Some(format) = input.name_id_format {
130 name_id_policy_attrs.push(("Format", format));
131 }
132 if let Some(allow_create) = allow_create {
133 name_id_policy_attrs.push(("AllowCreate", allow_create));
134 }
135
136 let mut attrs = Vec::with_capacity(8);
137 attrs.push(("xmlns:samlp", namespace::PROTOCOL));
138 attrs.push(("xmlns:saml", namespace::ASSERTION));
139 attrs.push(("ID", input.id));
140 attrs.push(("Version", "2.0"));
141 attrs.push(("IssueInstant", input.issue_instant));
142 attrs.push(("Destination", input.destination));
143 if let Some(force_authn) = force_authn.as_deref() {
144 attrs.push(("ForceAuthn", force_authn));
145 }
146 if let Some(protocol_binding) = input.protocol_binding {
147 attrs.push(("ProtocolBinding", protocol_binding));
148 }
149 if let Some(assertion_consumer_service_url) = input.assertion_consumer_service_url {
150 attrs.push((
151 "AssertionConsumerServiceURL",
152 assertion_consumer_service_url,
153 ));
154 }
155 if let Some(assertion_consumer_service_index) = assertion_consumer_service_index.as_deref() {
156 attrs.push((
157 "AssertionConsumerServiceIndex",
158 assertion_consumer_service_index,
159 ));
160 }
161
162 let mut writer = XmlWriter::new();
163 writer.start("samlp:AuthnRequest", &attrs);
164 writer.text_element("saml:Issuer", &[], input.issuer);
165 writer.empty("samlp:NameIDPolicy", &name_id_policy_attrs);
166 writer.end("samlp:AuthnRequest");
167 writer.finish()
168}
169
170impl ServiceProvider {
171 pub fn from_metadata(xml: &str, mut setting: EntitySetting) -> Result<Self, SamlError> {
179 let metadata = SpMetadata::from_xml(xml)?;
180 setting.authn_requests_signed = metadata.is_authn_request_signed();
181 setting.want_assertions_signed = metadata.is_want_assertions_signed();
182 let formats = metadata.get_name_id_format();
183 if !formats.is_empty() {
184 setting.name_id_format = formats;
185 }
186 if setting.entity_id.is_none() {
187 setting.entity_id = metadata.get_entity_id().map(str::to_string);
188 }
189 Ok(Self { setting, metadata })
190 }
191
192 pub fn from_config(
199 config: &SpMetadataConfig,
200 setting: EntitySetting,
201 ) -> Result<Self, SamlError> {
202 Self::from_metadata(&generate_sp_metadata(config), setting)
203 }
204
205 pub fn metadata_xml(&self) -> &str {
207 self.metadata.get_metadata()
208 }
209
210 fn entity_id(&self) -> String {
211 self.setting
212 .entity_id
213 .clone()
214 .or_else(|| self.metadata.get_entity_id().map(str::to_string))
215 .unwrap_or_default()
216 }
217
218 pub fn create_login_request(
235 &self,
236 idp: &IdentityProvider,
237 binding: Binding,
238 custom: Option<CustomTagReplacement<'_>>,
239 ) -> Result<BindingContext, SamlError> {
240 let options = LoginRequestOptions {
241 custom,
242 ..Default::default()
243 };
244 self.create_login_request_with_options(idp, binding, &options)
245 }
246
247 pub fn create_login_request_with_options(
259 &self,
260 idp: &IdentityProvider,
261 binding: Binding,
262 options: &LoginRequestOptions<'_>,
263 ) -> Result<BindingContext, SamlError> {
264 self.create_login_request_inner(idp, binding, options, WebBrowserSsoProducer::Compatibility)
265 }
266
267 pub(crate) fn create_typed_login_request(
268 &self,
269 idp: &IdentityProvider,
270 binding: Binding,
271 options: &LoginRequestOptions<'_>,
272 producer: WebBrowserSsoProducer,
273 ) -> Result<BindingContext, SamlError> {
274 self.create_login_request_inner(idp, binding, options, producer)
275 }
276
277 fn create_login_request_inner(
278 &self,
279 idp: &IdentityProvider,
280 binding: Binding,
281 options: &LoginRequestOptions<'_>,
282 producer: WebBrowserSsoProducer,
283 ) -> Result<BindingContext, SamlError> {
284 if self.metadata.is_authn_request_signed() != idp.metadata.is_want_authn_requests_signed() {
285 return Err(SamlError::Invalid(format!(
286 "ERR_METADATA_CONFLICT_REQUEST_SIGNED_FLAG: SP AuthnRequestsSigned={} but IdP WantAuthnRequestsSigned={}",
287 self.metadata.is_authn_request_signed(),
288 idp.metadata.is_want_authn_requests_signed()
289 )));
290 }
291 let destination = idp
292 .metadata
293 .get_single_sign_on_service(binding)
294 .ok_or_else(|| SamlError::MissingMetadata("SingleSignOnService".into()))?;
295 let custom_template = self.setting.login_request_template.as_deref();
296 if producer.follows() && (options.custom.is_some() || custom_template.is_some()) {
297 return Err(SamlError::Invalid(
298 "Web Browser SSO generation rules require the built-in AuthnRequest renderer"
299 .into(),
300 ));
301 }
302 let template = custom_template.unwrap_or(LOGIN_REQUEST_TEMPLATE);
303 let (id, xml) = match (options.custom, custom_template) {
304 (Some(f), _) => f(template),
305 (None, _) => {
306 let uses_acs_index = options.assertion_consumer_service_index.is_some();
307 let response_binding = options.response_binding.unwrap_or(Binding::Post);
308 let acs_url = if uses_acs_index {
309 None
310 } else {
311 Some(
312 self.metadata
313 .get_assertion_consumer_service(response_binding)
314 .ok_or_else(|| {
315 SamlError::MissingMetadata("AssertionConsumerService".into())
316 })?,
317 )
318 };
319 let protocol_binding =
320 (!uses_acs_index).then(|| response_binding.urn().to_string());
321 let acs_index = options
322 .assertion_consumer_service_index
323 .map(|index| index.to_string());
324 let name_id_format = self
325 .setting
326 .name_id_format
327 .first()
328 .cloned()
329 .unwrap_or_default();
330 let name_id_format_attr = if producer.follows() && name_id_format.is_empty() {
331 None
332 } else {
333 Some(name_id_format.as_str())
334 };
335 let allow_create = if producer.follows()
336 && name_id_format == crate::constants::name_id_format::TRANSIENT
337 {
338 AllowCreateAttribute::Omit
339 } else {
340 AllowCreateAttribute::Include(self.setting.allow_create)
341 };
342 let id = generate_id();
343 let xml = if custom_template.is_none() {
344 let issue_instant = now_iso8601();
345 let issuer = self.entity_id();
346 render_default_authn_request_xml(&AuthnRequestXml {
347 id: &id,
348 issue_instant: &issue_instant,
349 destination: &destination,
350 force_authn: options.force_authn,
351 protocol_binding: protocol_binding.as_deref(),
352 assertion_consumer_service_url: acs_url.as_deref(),
353 assertion_consumer_service_index: options.assertion_consumer_service_index,
354 issuer: &issuer,
355 name_id_format: name_id_format_attr,
356 allow_create,
357 })
358 } else {
359 replace_tags_by_optional_value(
360 template,
361 &[
362 ("ID", Some(id.clone())),
363 ("IssueInstant", Some(now_iso8601())),
364 ("Destination", Some(destination.clone())),
365 (
366 "ForceAuthn",
367 options
368 .force_authn
369 .map(|force_authn| force_authn.to_string()),
370 ),
371 ("ProtocolBinding", protocol_binding),
372 ("AssertionConsumerServiceURL", acs_url),
373 ("AssertionConsumerServiceIndex", acs_index),
374 ("Issuer", Some(self.entity_id())),
375 ("NameIDFormat", Some(name_id_format)),
376 ("AllowCreate", Some(self.setting.allow_create.to_string())),
377 ],
378 )
379 };
380 (id, xml)
381 }
382 };
383 let relay_state = match options.relay_state {
384 Some(value) => Some(value.to_string()),
385 None => {
386 (!self.setting.relay_state.is_empty()).then(|| self.setting.relay_state.clone())
387 }
388 };
389
390 if self.metadata.is_authn_request_signed() {
391 return self.signed_request_context(binding, &xml, destination, relay_state, id);
392 }
393
394 let context = match binding {
395 Binding::Redirect => build_redirect_url(
396 &destination,
397 ParserType::SamlRequest,
398 &xml,
399 relay_state.as_deref(),
400 )?,
401 Binding::Post | Binding::SimpleSign => base64_encode(xml.as_bytes()),
402 Binding::Artifact => {
403 return Err(SamlError::UnsupportedBinding {
404 binding: Binding::Artifact,
405 });
406 }
407 };
408 Ok(BindingContext {
409 id,
410 context,
411 relay_state,
412 entity_endpoint: destination,
413 binding,
414 request_type: "SAMLRequest",
415 signature: None,
416 sig_alg: None,
417 })
418 }
419
420 #[cfg(any(
421 feature = "crypto-rustcrypto",
422 feature = "crypto-aws-lc",
423 feature = "crypto-fips"
424 ))]
425 fn signed_request_context(
426 &self,
427 binding: Binding,
428 xml: &str,
429 destination: String,
430 relay_state: Option<String>,
431 id: String,
432 ) -> Result<BindingContext, SamlError> {
433 use crate::binding::{append_signature, build_redirect_octet};
434 use crate::crypto::{
435 construct_message_signature, construct_saml_signature, keys::load_private_key,
436 };
437
438 let key_pem = self
439 .setting
440 .private_key
441 .as_deref()
442 .ok_or_else(|| SamlError::MissingKey("private_key".into()))?;
443 let cert = self
444 .setting
445 .signing_cert
446 .as_deref()
447 .ok_or_else(|| SamlError::MissingKey("signing_cert".into()))?;
448 let sig_alg = &self.setting.request_signature_algorithm;
449 let key = load_private_key(key_pem, self.setting.private_key_pass.as_deref())?;
450
451 let (context, signature, sig_alg_out) = match binding {
452 Binding::Redirect => {
453 let octet = build_redirect_octet(
454 ParserType::SamlRequest,
455 xml,
456 relay_state.as_deref(),
457 sig_alg,
458 )?;
459 let sig = construct_message_signature(&octet, &key, sig_alg)?;
460 (append_signature(&destination, &octet, &sig), None, None)
461 }
462 Binding::Post => {
463 let signed = construct_saml_signature(
464 xml,
465 true,
466 &key,
467 cert,
468 sig_alg,
469 &self.setting.transformation_algorithms,
470 self.setting.signature_config.as_ref(),
471 )?;
472 (base64_encode(signed.as_bytes()), None, None)
473 }
474 Binding::SimpleSign => {
475 let octet = crate::binding::build_simplesign_octet(
476 ParserType::SamlRequest.query_param(),
477 xml,
478 relay_state.as_deref(),
479 sig_alg,
480 );
481 let sig = construct_message_signature(&octet, &key, sig_alg)?;
482 (
483 base64_encode(xml.as_bytes()),
484 Some(sig),
485 Some(sig_alg.clone()),
486 )
487 }
488 Binding::Artifact => {
489 return Err(SamlError::UnsupportedBinding {
490 binding: Binding::Artifact,
491 });
492 }
493 };
494 Ok(BindingContext {
495 id,
496 context,
497 relay_state,
498 entity_endpoint: destination,
499 binding,
500 request_type: "SAMLRequest",
501 signature,
502 sig_alg: sig_alg_out,
503 })
504 }
505
506 #[cfg(not(any(
507 feature = "crypto-rustcrypto",
508 feature = "crypto-aws-lc",
509 feature = "crypto-fips"
510 )))]
511 fn signed_request_context(
512 &self,
513 _binding: Binding,
514 _xml: &str,
515 _destination: String,
516 _relay_state: Option<String>,
517 _id: String,
518 ) -> Result<BindingContext, SamlError> {
519 Err(SamlError::Unsupported(
520 "signing AuthnRequest requires a crypto provider feature".into(),
521 ))
522 }
523
524 pub fn parse_login_response(
543 &self,
544 idp: &IdentityProvider,
545 binding: Binding,
546 request: &HttpRequest,
547 ) -> Result<FlowResult, SamlError> {
548 self.parse_unsolicited_login_response(idp, binding, request)
549 }
550
551 pub fn parse_unsolicited_login_response(
565 &self,
566 idp: &IdentityProvider,
567 binding: Binding,
568 request: &HttpRequest,
569 ) -> Result<FlowResult, SamlError> {
570 self.parse_login_response_inner(
571 idp,
572 binding,
573 request,
574 LoginResponseCorrelation::Unsolicited,
575 LoginResponseParseOptions::compatibility(self.setting.clock_drifts),
576 )
577 }
578
579 pub(crate) fn parse_unsolicited_login_response_at(
580 &self,
581 idp: &IdentityProvider,
582 binding: Binding,
583 request: &HttpRequest,
584 now: SystemTime,
585 clock_drifts: (i64, i64),
586 ) -> Result<FlowResultWithSignatureEvidence, SamlError> {
587 self.parse_login_response_inner_with_signature_evidence(
588 idp,
589 binding,
590 request,
591 LoginResponseCorrelation::Unsolicited,
592 LoginResponseParseOptions::at(now, clock_drifts),
593 )
594 }
595
596 pub fn parse_login_response_with_request_id(
613 &self,
614 idp: &IdentityProvider,
615 binding: Binding,
616 request: &HttpRequest,
617 request_id: &str,
618 ) -> Result<FlowResult, SamlError> {
619 if request_id.is_empty() {
620 return Err(SamlError::InvalidInResponseTo);
621 }
622 self.parse_login_response_inner(
623 idp,
624 binding,
625 request,
626 LoginResponseCorrelation::MessageId(request_id),
627 LoginResponseParseOptions::compatibility(self.setting.clock_drifts),
628 )
629 }
630
631 pub(crate) fn parse_login_response_with_request_id_at(
632 &self,
633 idp: &IdentityProvider,
634 binding: Binding,
635 request: &HttpRequest,
636 request_id: &str,
637 options: LoginResponseParseOptions<'_>,
638 ) -> Result<FlowResultWithSignatureEvidence, SamlError> {
639 if request_id.is_empty() {
640 return Err(SamlError::InvalidInResponseTo);
641 }
642 self.parse_login_response_inner_with_signature_evidence(
643 idp,
644 binding,
645 request,
646 LoginResponseCorrelation::MessageId(request_id),
647 options,
648 )
649 }
650
651 fn parse_login_response_inner(
652 &self,
653 idp: &IdentityProvider,
654 binding: Binding,
655 request: &HttpRequest,
656 correlation: LoginResponseCorrelation<'_>,
657 options: LoginResponseParseOptions<'_>,
658 ) -> Result<FlowResult, SamlError> {
659 let result = self.parse_login_response_inner_with_signature_evidence(
660 idp,
661 binding,
662 request,
663 correlation,
664 options,
665 )?;
666 Ok(result.into_parts().0)
667 }
668
669 fn parse_login_response_inner_with_signature_evidence(
670 &self,
671 idp: &IdentityProvider,
672 binding: Binding,
673 request: &HttpRequest,
674 correlation: LoginResponseCorrelation<'_>,
675 options: LoginResponseParseOptions<'_>,
676 ) -> Result<FlowResultWithSignatureEvidence, SamlError> {
677 let signing_certs = idp.metadata.x509_certificates(CertUse::Signing);
678 let decrypt_key = if self.setting.is_assertion_encrypted {
679 self.setting.enc_private_key.as_deref()
680 } else {
681 None
682 };
683 let audience = self.entity_id();
684 let expected_in_response_to = match correlation {
685 LoginResponseCorrelation::MessageId(request_id) => Some(request_id),
686 LoginResponseCorrelation::Unsolicited => None,
687 };
688 let recipient = match options.expected_recipient {
689 Some(recipient) => recipient.to_string(),
690 None => self
691 .metadata
692 .get_assertion_consumer_service(binding)
693 .ok_or_else(|| SamlError::MissingMetadata("AssertionConsumerService".into()))?,
694 };
695 let result = flow_with_expected_recipient_and_signature_evidence(
696 &FlowOptions {
697 binding: Some(binding),
698 parser_type: Some(ParserType::SamlResponse),
699 check_signature: true,
700 verify_signature_if_present: false,
701 require_audience_restriction: self.setting.require_audience_restriction,
702 strict_xml_signature_profile: self.setting.strict_xml_signature_profile,
703 from_issuer: idp.metadata.get_entity_id(),
704 signing_certs: &signing_certs,
705 decrypt_key,
706 decrypt_key_pass: self.setting.enc_private_key_pass.as_deref(),
707 allow_insecure_software_rsa_key_transport_decryption: self
708 .setting
709 .allow_insecure_software_rsa_key_transport_decryption,
710 clock_drifts: options.clock_drifts,
711 now: options.now,
712 redirect_inflate_max_bytes: self.setting.redirect_inflate_max_bytes,
713 xml_limits: self.setting.xml_limits,
714 expected_audience: self.setting.validate_audience.then_some(audience.as_str()),
715 expected_in_response_to,
716 },
717 request,
718 recipient.as_str(),
719 if self.setting.want_assertions_signed {
720 AssertionSignatureRequirement::Direct
721 } else {
722 AssertionSignatureRequirement::Compatible
723 },
724 if self.setting.want_message_signed {
725 ResponseSignatureRequirement::Required
726 } else if self.setting.want_encrypted_cbc_response_signed {
727 ResponseSignatureRequirement::RequiredForEncryptedCbc
728 } else {
729 ResponseSignatureRequirement::Optional
730 },
731 )?;
732 if matches!(correlation, LoginResponseCorrelation::Unsolicited)
733 && result
734 .extract()
735 .get_str("response.inResponseTo")
736 .is_some_and(|actual| !actual.is_empty())
737 {
738 return Err(SamlError::in_response_to_mismatch(
739 None,
740 result.extract().get_str("response.inResponseTo"),
741 ));
742 }
743 if matches!(correlation, LoginResponseCorrelation::Unsolicited) {
744 crate::assertion_acceptance::reject_unsolicited_bearer_in_response_to(
745 result.saml_content(),
746 self.setting.xml_limits,
747 )?;
748 }
749 Ok(result)
750 }
751}
752
753#[cfg(test)]
754mod tests {
755 use super::*;
756 use crate::binding::{base64_decode, deflate_raw_decode};
757 use crate::metadata::{Endpoint, IdpMetadataConfig};
758 use url::Url;
759
760 fn unsigned_idp() -> Result<IdentityProvider, SamlError> {
761 IdentityProvider::from_config(
762 &IdpMetadataConfig {
763 entity_id: "https://idp.example.com/metadata".into(),
764 single_sign_on_service: vec![
765 Endpoint::new(Binding::Redirect, "https://idp.example.com/sso"),
766 Endpoint::new(Binding::Post, "https://idp.example.com/sso"),
767 ],
768 ..Default::default()
769 },
770 EntitySetting::default(),
771 )
772 }
773
774 fn unsigned_sp() -> Result<ServiceProvider, SamlError> {
775 ServiceProvider::from_config(
776 &SpMetadataConfig {
777 entity_id: "https://sp.example.com/metadata".into(),
778 assertion_consumer_service: vec![Endpoint::new(
779 Binding::Post,
780 "https://sp.example.com/acs",
781 )],
782 ..Default::default()
783 },
784 EntitySetting::default(),
785 )
786 }
787
788 #[test]
789 fn create_unsigned_login_request_redirect_round_trips() -> Result<(), Box<dyn std::error::Error>>
790 {
791 let ctx = unsigned_sp()?.create_login_request(&unsigned_idp()?, Binding::Redirect, None)?;
792 let url = Url::parse(&ctx.context)?;
793 let (_, value) = url
794 .query_pairs()
795 .find(|(k, _)| k == "SAMLRequest")
796 .ok_or("missing SAMLRequest")?;
797 let xml = String::from_utf8(deflate_raw_decode(&base64_decode(&value)?)?)?;
798 assert!(xml.contains("AssertionConsumerServiceURL=\"https://sp.example.com/acs\""));
799 assert!(url.query_pairs().all(|(k, _)| k != "Signature"));
800 Ok(())
801 }
802
803 #[test]
804 fn create_unsigned_login_request_post_is_base64() -> Result<(), Box<dyn std::error::Error>> {
805 let ctx = unsigned_sp()?.create_login_request(&unsigned_idp()?, Binding::Post, None)?;
806 let xml = String::from_utf8(base64_decode(&ctx.context)?)?;
807 assert!(xml.starts_with("<samlp:AuthnRequest"));
808 Ok(())
809 }
810
811 #[test]
812 fn custom_tag_replacement_overrides_request() -> Result<(), Box<dyn std::error::Error>> {
813 let replace = |_t: &str| {
814 (
815 "_custom".to_string(),
816 "<samlp:AuthnRequest ID=\"_custom\"/>".to_string(),
817 )
818 };
819 let ctx = unsigned_sp()?.create_login_request(
820 &unsigned_idp()?,
821 Binding::Post,
822 Some(&replace as &dyn Fn(&str) -> (String, String)),
823 )?;
824 assert_eq!(ctx.id, "_custom");
825 let xml = String::from_utf8(base64_decode(&ctx.context)?)?;
826 assert!(xml.contains("ID=\"_custom\""));
827 Ok(())
828 }
829}
830
831#[cfg(all(
832 test,
833 any(
834 feature = "crypto-rustcrypto",
835 feature = "crypto-aws-lc",
836 feature = "crypto-fips"
837 )
838))]
839mod crypto_tests {
840 use super::*;
841 use crate::binding::base64_decode;
842 use crate::constants::signature_algorithm::RSA_SHA256;
843 use crate::crypto::verify_signature;
844 use crate::entity::User;
845 use crate::idp::LoginResponseOptions;
846 use crate::metadata::{Endpoint, IdpMetadataConfig};
847
848 const IDP_CERT: &str = include_str!("../tests/fixtures/key/idp_cert.cer");
849 const SP_PRIVKEY: &str = include_str!("../tests/fixtures/key/sp_privkey.pem");
850 const SP_SIGNING_CERT: &str = include_str!("../tests/fixtures/key/sp_signing_cert.cer");
851
852 fn signing_idp() -> Result<IdentityProvider, SamlError> {
853 IdentityProvider::from_config(
854 &IdpMetadataConfig {
855 entity_id: "https://idp.example.com/metadata".into(),
856 signing_certs: vec![IDP_CERT.into()],
857 want_authn_requests_signed: true,
858 single_sign_on_service: vec![
859 Endpoint::new(Binding::Redirect, "https://idp/sso"),
860 Endpoint::new(Binding::Post, "https://idp/sso"),
861 ],
862 ..Default::default()
863 },
864 EntitySetting::default(),
865 )
866 }
867
868 #[test]
869 fn parse_signed_response_extracts_name_id() -> Result<(), Box<dyn std::error::Error>> {
870 let idp = IdentityProvider::from_config(
871 &IdpMetadataConfig {
872 entity_id: "https://idp.example.com/metadata".into(),
873 signing_certs: vec![SP_SIGNING_CERT.into()],
874 single_sign_on_service: vec![Endpoint::new(Binding::Post, "https://idp/sso")],
875 ..Default::default()
876 },
877 EntitySetting {
878 private_key: Some(SP_PRIVKEY.into()),
879 signing_cert: Some(SP_SIGNING_CERT.into()),
880 request_signature_algorithm: RSA_SHA256.into(),
881 ..Default::default()
882 },
883 )?;
884 let sp = ServiceProvider::from_config(
885 &SpMetadataConfig {
886 entity_id: "https://sp.example.com/metadata".into(),
887 assertion_consumer_service: vec![Endpoint::new(
888 Binding::Post,
889 "http://sp.example.com/demo1/index.php?acs",
890 )],
891 ..Default::default()
892 },
893 EntitySetting::default(),
894 )?;
895 let request_id = "_41e758fee373d51639552c4b040b1090e97f6685";
896 let name_id = "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7";
897 let ctx = idp.create_login_response(
898 &sp,
899 Binding::Post,
900 &User::new(name_id),
901 &LoginResponseOptions {
902 in_response_to: Some(request_id),
903 ..Default::default()
904 },
905 )?;
906 let request = HttpRequest::post(vec![("SAMLResponse".into(), ctx.context)]);
907 let result =
908 sp.parse_login_response_with_request_id(&idp, Binding::Post, &request, request_id)?;
909 assert_eq!(result.extract.get_str("nameID"), Some(name_id));
910 Ok(())
911 }
912
913 #[test]
914 fn create_signed_post_request_verifies() -> Result<(), Box<dyn std::error::Error>> {
915 let sp = ServiceProvider::from_config(
916 &SpMetadataConfig {
917 entity_id: "https://sp.example.com/metadata".into(),
918 authn_requests_signed: true,
919 assertion_consumer_service: vec![Endpoint::new(Binding::Post, "https://sp/acs")],
920 ..Default::default()
921 },
922 EntitySetting {
923 private_key: Some(SP_PRIVKEY.into()),
924 signing_cert: Some(SP_SIGNING_CERT.into()),
925 request_signature_algorithm: RSA_SHA256.into(),
926 ..Default::default()
927 },
928 )?;
929 let ctx = sp.create_login_request(&signing_idp()?, Binding::Post, None)?;
930 let signed_xml = String::from_utf8(base64_decode(&ctx.context)?)?;
931 let (verified, _) = verify_signature(&signed_xml, &[SP_SIGNING_CERT.to_string()])?;
932 assert!(
933 verified,
934 "signed AuthnRequest should verify with the SP cert"
935 );
936 Ok(())
937 }
938
939 #[test]
940 fn create_signed_redirect_request_has_signature() -> Result<(), Box<dyn std::error::Error>> {
941 let sp = ServiceProvider::from_config(
942 &SpMetadataConfig {
943 entity_id: "https://sp.example.com/metadata".into(),
944 authn_requests_signed: true,
945 assertion_consumer_service: vec![Endpoint::new(Binding::Post, "https://sp/acs")],
946 ..Default::default()
947 },
948 EntitySetting {
949 private_key: Some(SP_PRIVKEY.into()),
950 signing_cert: Some(SP_SIGNING_CERT.into()),
951 request_signature_algorithm: RSA_SHA256.into(),
952 ..Default::default()
953 },
954 )?;
955 let ctx = sp.create_login_request(&signing_idp()?, Binding::Redirect, None)?;
956 assert!(ctx.context.contains("&SigAlg="));
957 assert!(ctx.context.contains("&Signature="));
958 Ok(())
959 }
960}