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, AssertionSignatureRequirement, FlowOptions, FlowResult,
11 HttpRequest,
12};
13use crate::idp::IdentityProvider;
14use crate::metadata::{generate_sp_metadata, SpMetadata, SpMetadataConfig};
15use crate::template::{replace_tags_by_optional_value, LOGIN_REQUEST_TEMPLATE};
16use crate::util::Value;
17use crate::xml::write::XmlWriter;
18use crate::xml::{extract_with_limits, ExtractorField, XmlLimits};
19use std::time::SystemTime;
20
21const BEARER_SUBJECT_CONFIRMATION_METHOD: &str = "urn:oasis:names:tc:SAML:2.0:cm:bearer";
22
23#[derive(Debug, Clone)]
25pub struct ServiceProvider {
26 pub setting: EntitySetting,
28 pub metadata: SpMetadata,
30}
31
32#[derive(Default)]
34pub struct LoginRequestOptions<'a> {
35 pub relay_state: Option<&'a str>,
37 pub custom: Option<CustomTagReplacement<'a>>,
39 pub force_authn: Option<bool>,
41 pub assertion_consumer_service_index: Option<u16>,
44 pub response_binding: Option<Binding>,
46}
47
48struct AuthnRequestXml<'a> {
49 id: &'a str,
50 issue_instant: &'a str,
51 destination: &'a str,
52 force_authn: Option<bool>,
53 protocol_binding: Option<&'a str>,
54 assertion_consumer_service_url: Option<&'a str>,
55 assertion_consumer_service_index: Option<u16>,
56 issuer: &'a str,
57 name_id_format: &'a str,
58 allow_create: bool,
59}
60
61#[derive(Debug, Clone, Copy)]
62enum LoginResponseCorrelation<'a> {
63 Unsolicited,
64 MessageId(&'a str),
65}
66
67pub(crate) struct LoginResponseParseOptions<'a> {
68 expected_recipient: Option<&'a str>,
69 now: Option<SystemTime>,
70 clock_drifts: (i64, i64),
71}
72
73impl<'a> LoginResponseParseOptions<'a> {
74 fn compatibility(clock_drifts: (i64, i64)) -> Self {
75 Self {
76 expected_recipient: None,
77 now: None,
78 clock_drifts,
79 }
80 }
81
82 pub(crate) fn at(now: SystemTime, clock_drifts: (i64, i64)) -> Self {
83 Self {
84 expected_recipient: None,
85 now: Some(now),
86 clock_drifts,
87 }
88 }
89
90 pub(crate) fn with_expected_recipient(mut self, expected_recipient: &'a str) -> Self {
91 self.expected_recipient = Some(expected_recipient);
92 self
93 }
94}
95
96fn render_default_authn_request_xml(input: &AuthnRequestXml<'_>) -> String {
97 let force_authn = input.force_authn.map(|value| value.to_string());
98 let assertion_consumer_service_index = input
99 .assertion_consumer_service_index
100 .map(|value| value.to_string());
101 let allow_create = input.allow_create.to_string();
102
103 let mut attrs = Vec::with_capacity(8);
104 attrs.push(("xmlns:samlp", namespace::PROTOCOL));
105 attrs.push(("xmlns:saml", namespace::ASSERTION));
106 attrs.push(("ID", input.id));
107 attrs.push(("Version", "2.0"));
108 attrs.push(("IssueInstant", input.issue_instant));
109 attrs.push(("Destination", input.destination));
110 if let Some(force_authn) = force_authn.as_deref() {
111 attrs.push(("ForceAuthn", force_authn));
112 }
113 if let Some(protocol_binding) = input.protocol_binding {
114 attrs.push(("ProtocolBinding", protocol_binding));
115 }
116 if let Some(assertion_consumer_service_url) = input.assertion_consumer_service_url {
117 attrs.push((
118 "AssertionConsumerServiceURL",
119 assertion_consumer_service_url,
120 ));
121 }
122 if let Some(assertion_consumer_service_index) = assertion_consumer_service_index.as_deref() {
123 attrs.push((
124 "AssertionConsumerServiceIndex",
125 assertion_consumer_service_index,
126 ));
127 }
128
129 let mut writer = XmlWriter::new();
130 writer.start("samlp:AuthnRequest", &attrs);
131 writer.text_element("saml:Issuer", &[], input.issuer);
132 writer.empty(
133 "samlp:NameIDPolicy",
134 &[
135 ("Format", input.name_id_format),
136 ("AllowCreate", allow_create.as_str()),
137 ],
138 );
139 writer.end("samlp:AuthnRequest");
140 writer.finish()
141}
142
143fn subject_confirmation_xmls(extracted: &Value) -> Vec<&str> {
144 match extracted.get("subjectConfirmation") {
145 Some(Value::Str(xml)) => vec![xml.as_str()],
146 Some(Value::Array(items)) => items.iter().filter_map(Value::as_str).collect(),
147 _ => Vec::new(),
148 }
149}
150
151fn reject_unsolicited_request_bound_bearer_confirmations(
152 extracted: &Value,
153 limits: XmlLimits,
154) -> Result<(), SamlError> {
155 let fields = [
156 ExtractorField::new("subjectConfirmation", &["SubjectConfirmation"]).attrs(&["Method"]),
157 ExtractorField::new(
158 "subjectConfirmationData",
159 &["SubjectConfirmation", "SubjectConfirmationData"],
160 )
161 .attrs(&["InResponseTo"]),
162 ];
163 for xml in subject_confirmation_xmls(extracted) {
164 let confirmation = extract_with_limits(xml, &fields, limits)?;
165 let is_bearer =
166 confirmation.get_str("subjectConfirmation") == Some(BEARER_SUBJECT_CONFIRMATION_METHOD);
167 let is_request_bound = confirmation
168 .get_str("subjectConfirmationData")
169 .is_some_and(|actual| !actual.is_empty());
170 if is_bearer && is_request_bound {
171 return Err(SamlError::in_response_to_mismatch(
172 None,
173 confirmation.get_str("subjectConfirmationData"),
174 ));
175 }
176 }
177 Ok(())
178}
179
180impl ServiceProvider {
181 pub fn from_metadata(xml: &str, mut setting: EntitySetting) -> Result<Self, SamlError> {
189 let metadata = SpMetadata::from_xml(xml)?;
190 setting.authn_requests_signed = metadata.is_authn_request_signed();
191 setting.want_assertions_signed = metadata.is_want_assertions_signed();
192 let formats = metadata.get_name_id_format();
193 if !formats.is_empty() {
194 setting.name_id_format = formats;
195 }
196 if setting.entity_id.is_none() {
197 setting.entity_id = metadata.get_entity_id().map(str::to_string);
198 }
199 Ok(Self { setting, metadata })
200 }
201
202 pub fn from_config(
209 config: &SpMetadataConfig,
210 setting: EntitySetting,
211 ) -> Result<Self, SamlError> {
212 Self::from_metadata(&generate_sp_metadata(config), setting)
213 }
214
215 pub fn metadata_xml(&self) -> &str {
217 self.metadata.get_metadata()
218 }
219
220 fn entity_id(&self) -> String {
221 self.setting
222 .entity_id
223 .clone()
224 .or_else(|| self.metadata.get_entity_id().map(str::to_string))
225 .unwrap_or_default()
226 }
227
228 pub fn create_login_request(
245 &self,
246 idp: &IdentityProvider,
247 binding: Binding,
248 custom: Option<CustomTagReplacement<'_>>,
249 ) -> Result<BindingContext, SamlError> {
250 let options = LoginRequestOptions {
251 custom,
252 ..Default::default()
253 };
254 self.create_login_request_with_options(idp, binding, &options)
255 }
256
257 pub fn create_login_request_with_options(
269 &self,
270 idp: &IdentityProvider,
271 binding: Binding,
272 options: &LoginRequestOptions<'_>,
273 ) -> Result<BindingContext, SamlError> {
274 if self.metadata.is_authn_request_signed() != idp.metadata.is_want_authn_requests_signed() {
275 return Err(SamlError::Invalid(format!(
276 "ERR_METADATA_CONFLICT_REQUEST_SIGNED_FLAG: SP AuthnRequestsSigned={} but IdP WantAuthnRequestsSigned={}",
277 self.metadata.is_authn_request_signed(),
278 idp.metadata.is_want_authn_requests_signed()
279 )));
280 }
281 let destination = idp
282 .metadata
283 .get_single_sign_on_service(binding)
284 .ok_or_else(|| SamlError::MissingMetadata("SingleSignOnService".into()))?;
285 let custom_template = self.setting.login_request_template.as_deref();
286 let template = custom_template.unwrap_or(LOGIN_REQUEST_TEMPLATE);
287 let (id, xml) = match (options.custom, custom_template) {
288 (Some(f), _) => f(template),
289 (None, _) => {
290 let uses_acs_index = options.assertion_consumer_service_index.is_some();
291 let response_binding = options.response_binding.unwrap_or(Binding::Post);
292 let acs_url = if uses_acs_index {
293 None
294 } else {
295 Some(
296 self.metadata
297 .get_assertion_consumer_service(response_binding)
298 .ok_or_else(|| {
299 SamlError::MissingMetadata("AssertionConsumerService".into())
300 })?,
301 )
302 };
303 let protocol_binding =
304 (!uses_acs_index).then(|| response_binding.urn().to_string());
305 let acs_index = options
306 .assertion_consumer_service_index
307 .map(|index| index.to_string());
308 let name_id_format = self
309 .setting
310 .name_id_format
311 .first()
312 .cloned()
313 .unwrap_or_default();
314 let id = generate_id();
315 let xml = if custom_template.is_none() {
316 let issue_instant = now_iso8601();
317 let issuer = self.entity_id();
318 render_default_authn_request_xml(&AuthnRequestXml {
319 id: &id,
320 issue_instant: &issue_instant,
321 destination: &destination,
322 force_authn: options.force_authn,
323 protocol_binding: protocol_binding.as_deref(),
324 assertion_consumer_service_url: acs_url.as_deref(),
325 assertion_consumer_service_index: options.assertion_consumer_service_index,
326 issuer: &issuer,
327 name_id_format: &name_id_format,
328 allow_create: self.setting.allow_create,
329 })
330 } else {
331 replace_tags_by_optional_value(
332 template,
333 &[
334 ("ID", Some(id.clone())),
335 ("IssueInstant", Some(now_iso8601())),
336 ("Destination", Some(destination.clone())),
337 (
338 "ForceAuthn",
339 options
340 .force_authn
341 .map(|force_authn| force_authn.to_string()),
342 ),
343 ("ProtocolBinding", protocol_binding),
344 ("AssertionConsumerServiceURL", acs_url),
345 ("AssertionConsumerServiceIndex", acs_index),
346 ("Issuer", Some(self.entity_id())),
347 ("NameIDFormat", Some(name_id_format)),
348 ("AllowCreate", Some(self.setting.allow_create.to_string())),
349 ],
350 )
351 };
352 (id, xml)
353 }
354 };
355 let relay_state = match options.relay_state {
356 Some(value) => Some(value.to_string()),
357 None => {
358 (!self.setting.relay_state.is_empty()).then(|| self.setting.relay_state.clone())
359 }
360 };
361
362 if self.metadata.is_authn_request_signed() {
363 return self.signed_request_context(binding, &xml, destination, relay_state, id);
364 }
365
366 let context = match binding {
367 Binding::Redirect => build_redirect_url(
368 &destination,
369 ParserType::SamlRequest,
370 &xml,
371 relay_state.as_deref(),
372 )?,
373 Binding::Post | Binding::SimpleSign => base64_encode(xml.as_bytes()),
374 Binding::Artifact => {
375 return Err(SamlError::UnsupportedBinding {
376 binding: Binding::Artifact,
377 });
378 }
379 };
380 Ok(BindingContext {
381 id,
382 context,
383 relay_state,
384 entity_endpoint: destination,
385 binding,
386 request_type: "SAMLRequest",
387 signature: None,
388 sig_alg: None,
389 })
390 }
391
392 #[cfg(feature = "crypto-bergshamra")]
393 fn signed_request_context(
394 &self,
395 binding: Binding,
396 xml: &str,
397 destination: String,
398 relay_state: Option<String>,
399 id: String,
400 ) -> Result<BindingContext, SamlError> {
401 use crate::binding::{append_signature, build_redirect_octet};
402 use crate::crypto::{
403 construct_message_signature, construct_saml_signature, keys::load_private_key,
404 };
405
406 let key_pem = self
407 .setting
408 .private_key
409 .as_deref()
410 .ok_or_else(|| SamlError::MissingKey("private_key".into()))?;
411 let cert = self
412 .setting
413 .signing_cert
414 .as_deref()
415 .ok_or_else(|| SamlError::MissingKey("signing_cert".into()))?;
416 let sig_alg = &self.setting.request_signature_algorithm;
417 let key = load_private_key(key_pem, self.setting.private_key_pass.as_deref())?;
418
419 let (context, signature, sig_alg_out) = match binding {
420 Binding::Redirect => {
421 let octet = build_redirect_octet(
422 ParserType::SamlRequest,
423 xml,
424 relay_state.as_deref(),
425 sig_alg,
426 )?;
427 let sig = construct_message_signature(&octet, &key, sig_alg)?;
428 (append_signature(&destination, &octet, &sig), None, None)
429 }
430 Binding::Post => {
431 let signed = construct_saml_signature(
432 xml,
433 true,
434 &key,
435 cert,
436 sig_alg,
437 &self.setting.transformation_algorithms,
438 self.setting.signature_config.as_ref(),
439 )?;
440 (base64_encode(signed.as_bytes()), None, None)
441 }
442 Binding::SimpleSign => {
443 let octet = crate::binding::build_simplesign_octet(
444 ParserType::SamlRequest.query_param(),
445 xml,
446 relay_state.as_deref(),
447 sig_alg,
448 );
449 let sig = construct_message_signature(&octet, &key, sig_alg)?;
450 (
451 base64_encode(xml.as_bytes()),
452 Some(sig),
453 Some(sig_alg.clone()),
454 )
455 }
456 Binding::Artifact => {
457 return Err(SamlError::UnsupportedBinding {
458 binding: Binding::Artifact,
459 });
460 }
461 };
462 Ok(BindingContext {
463 id,
464 context,
465 relay_state,
466 entity_endpoint: destination,
467 binding,
468 request_type: "SAMLRequest",
469 signature,
470 sig_alg: sig_alg_out,
471 })
472 }
473
474 #[cfg(not(feature = "crypto-bergshamra"))]
475 fn signed_request_context(
476 &self,
477 _binding: Binding,
478 _xml: &str,
479 _destination: String,
480 _relay_state: Option<String>,
481 _id: String,
482 ) -> Result<BindingContext, SamlError> {
483 Err(SamlError::Unsupported(
484 "signing AuthnRequest requires feature crypto-bergshamra".into(),
485 ))
486 }
487
488 pub fn parse_login_response(
507 &self,
508 idp: &IdentityProvider,
509 binding: Binding,
510 request: &HttpRequest,
511 ) -> Result<FlowResult, SamlError> {
512 self.parse_unsolicited_login_response(idp, binding, request)
513 }
514
515 pub fn parse_unsolicited_login_response(
529 &self,
530 idp: &IdentityProvider,
531 binding: Binding,
532 request: &HttpRequest,
533 ) -> Result<FlowResult, SamlError> {
534 self.parse_login_response_inner(
535 idp,
536 binding,
537 request,
538 LoginResponseCorrelation::Unsolicited,
539 LoginResponseParseOptions::compatibility(self.setting.clock_drifts),
540 )
541 }
542
543 pub(crate) fn parse_unsolicited_login_response_at(
544 &self,
545 idp: &IdentityProvider,
546 binding: Binding,
547 request: &HttpRequest,
548 now: SystemTime,
549 clock_drifts: (i64, i64),
550 ) -> Result<FlowResult, SamlError> {
551 self.parse_login_response_inner(
552 idp,
553 binding,
554 request,
555 LoginResponseCorrelation::Unsolicited,
556 LoginResponseParseOptions::at(now, clock_drifts),
557 )
558 }
559
560 pub fn parse_login_response_with_request_id(
577 &self,
578 idp: &IdentityProvider,
579 binding: Binding,
580 request: &HttpRequest,
581 request_id: &str,
582 ) -> Result<FlowResult, SamlError> {
583 if request_id.is_empty() {
584 return Err(SamlError::InvalidInResponseTo);
585 }
586 self.parse_login_response_inner(
587 idp,
588 binding,
589 request,
590 LoginResponseCorrelation::MessageId(request_id),
591 LoginResponseParseOptions::compatibility(self.setting.clock_drifts),
592 )
593 }
594
595 pub(crate) fn parse_login_response_with_request_id_at(
596 &self,
597 idp: &IdentityProvider,
598 binding: Binding,
599 request: &HttpRequest,
600 request_id: &str,
601 options: LoginResponseParseOptions<'_>,
602 ) -> Result<FlowResult, SamlError> {
603 if request_id.is_empty() {
604 return Err(SamlError::InvalidInResponseTo);
605 }
606 self.parse_login_response_inner(
607 idp,
608 binding,
609 request,
610 LoginResponseCorrelation::MessageId(request_id),
611 options,
612 )
613 }
614
615 fn parse_login_response_inner(
616 &self,
617 idp: &IdentityProvider,
618 binding: Binding,
619 request: &HttpRequest,
620 correlation: LoginResponseCorrelation<'_>,
621 options: LoginResponseParseOptions<'_>,
622 ) -> Result<FlowResult, SamlError> {
623 let signing_certs = idp.metadata.x509_certificates(CertUse::Signing);
624 let decrypt_key = if self.setting.is_assertion_encrypted {
625 self.setting.enc_private_key.as_deref()
626 } else {
627 None
628 };
629 let audience = self.entity_id();
630 let expected_in_response_to = match correlation {
631 LoginResponseCorrelation::MessageId(request_id) => Some(request_id),
632 LoginResponseCorrelation::Unsolicited => None,
633 };
634 let recipient = match options.expected_recipient {
635 Some(recipient) => recipient.to_string(),
636 None => self
637 .metadata
638 .get_assertion_consumer_service(binding)
639 .ok_or_else(|| SamlError::MissingMetadata("AssertionConsumerService".into()))?,
640 };
641 let result = flow_with_expected_recipient(
642 &FlowOptions {
643 binding: Some(binding),
644 parser_type: Some(ParserType::SamlResponse),
645 check_signature: true,
646 from_issuer: idp.metadata.get_entity_id(),
647 signing_certs: &signing_certs,
648 decrypt_key,
649 decrypt_key_pass: self.setting.enc_private_key_pass.as_deref(),
650 allow_insecure_software_rsa_key_transport_decryption: self
651 .setting
652 .allow_insecure_software_rsa_key_transport_decryption,
653 clock_drifts: options.clock_drifts,
654 now: options.now,
655 redirect_inflate_max_bytes: self.setting.redirect_inflate_max_bytes,
656 xml_limits: self.setting.xml_limits,
657 expected_audience: self.setting.validate_audience.then_some(audience.as_str()),
658 expected_in_response_to,
659 },
660 request,
661 recipient.as_str(),
662 if self.setting.want_assertions_signed {
663 AssertionSignatureRequirement::Direct
664 } else {
665 AssertionSignatureRequirement::Compatible
666 },
667 )?;
668 if matches!(correlation, LoginResponseCorrelation::Unsolicited)
669 && result
670 .extract
671 .get_str("response.inResponseTo")
672 .is_some_and(|actual| !actual.is_empty())
673 {
674 return Err(SamlError::in_response_to_mismatch(
675 None,
676 result.extract.get_str("response.inResponseTo"),
677 ));
678 }
679 if matches!(correlation, LoginResponseCorrelation::Unsolicited) {
680 reject_unsolicited_request_bound_bearer_confirmations(
681 &result.extract,
682 self.setting.xml_limits,
683 )?;
684 }
685 Ok(result)
686 }
687}
688
689#[cfg(test)]
690mod tests {
691 use super::*;
692 use crate::binding::{base64_decode, deflate_raw_decode};
693 use crate::metadata::{Endpoint, IdpMetadataConfig};
694 use url::Url;
695
696 fn unsigned_idp() -> Result<IdentityProvider, SamlError> {
697 IdentityProvider::from_config(
698 &IdpMetadataConfig {
699 entity_id: "https://idp.example.com/metadata".into(),
700 single_sign_on_service: vec![
701 Endpoint::new(Binding::Redirect, "https://idp.example.com/sso"),
702 Endpoint::new(Binding::Post, "https://idp.example.com/sso"),
703 ],
704 ..Default::default()
705 },
706 EntitySetting::default(),
707 )
708 }
709
710 fn unsigned_sp() -> Result<ServiceProvider, SamlError> {
711 ServiceProvider::from_config(
712 &SpMetadataConfig {
713 entity_id: "https://sp.example.com/metadata".into(),
714 assertion_consumer_service: vec![Endpoint::new(
715 Binding::Post,
716 "https://sp.example.com/acs",
717 )],
718 ..Default::default()
719 },
720 EntitySetting::default(),
721 )
722 }
723
724 #[test]
725 fn create_unsigned_login_request_redirect_round_trips() -> Result<(), Box<dyn std::error::Error>>
726 {
727 let ctx = unsigned_sp()?.create_login_request(&unsigned_idp()?, Binding::Redirect, None)?;
728 let url = Url::parse(&ctx.context)?;
729 let (_, value) = url
730 .query_pairs()
731 .find(|(k, _)| k == "SAMLRequest")
732 .ok_or("missing SAMLRequest")?;
733 let xml = String::from_utf8(deflate_raw_decode(&base64_decode(&value)?)?)?;
734 assert!(xml.contains("AssertionConsumerServiceURL=\"https://sp.example.com/acs\""));
735 assert!(url.query_pairs().all(|(k, _)| k != "Signature"));
736 Ok(())
737 }
738
739 #[test]
740 fn create_unsigned_login_request_post_is_base64() -> Result<(), Box<dyn std::error::Error>> {
741 let ctx = unsigned_sp()?.create_login_request(&unsigned_idp()?, Binding::Post, None)?;
742 let xml = String::from_utf8(base64_decode(&ctx.context)?)?;
743 assert!(xml.starts_with("<samlp:AuthnRequest"));
744 Ok(())
745 }
746
747 #[test]
748 fn custom_tag_replacement_overrides_request() -> Result<(), Box<dyn std::error::Error>> {
749 let replace = |_t: &str| {
750 (
751 "_custom".to_string(),
752 "<samlp:AuthnRequest ID=\"_custom\"/>".to_string(),
753 )
754 };
755 let ctx = unsigned_sp()?.create_login_request(
756 &unsigned_idp()?,
757 Binding::Post,
758 Some(&replace as &dyn Fn(&str) -> (String, String)),
759 )?;
760 assert_eq!(ctx.id, "_custom");
761 let xml = String::from_utf8(base64_decode(&ctx.context)?)?;
762 assert!(xml.contains("ID=\"_custom\""));
763 Ok(())
764 }
765}
766
767#[cfg(all(test, feature = "crypto-bergshamra"))]
768mod crypto_tests {
769 use super::*;
770 use crate::binding::base64_decode;
771 use crate::constants::signature_algorithm::RSA_SHA256;
772 use crate::crypto::verify_signature;
773 use crate::entity::User;
774 use crate::idp::LoginResponseOptions;
775 use crate::metadata::{Endpoint, IdpMetadataConfig};
776
777 const IDP_CERT: &str = include_str!("../tests/fixtures/key/idp_cert.cer");
778 const SP_PRIVKEY: &str = include_str!("../tests/fixtures/key/sp_privkey.pem");
779 const SP_SIGNING_CERT: &str = include_str!("../tests/fixtures/key/sp_signing_cert.cer");
780
781 fn signing_idp() -> Result<IdentityProvider, SamlError> {
782 IdentityProvider::from_config(
783 &IdpMetadataConfig {
784 entity_id: "https://idp.example.com/metadata".into(),
785 signing_certs: vec![IDP_CERT.into()],
786 want_authn_requests_signed: true,
787 single_sign_on_service: vec![
788 Endpoint::new(Binding::Redirect, "https://idp/sso"),
789 Endpoint::new(Binding::Post, "https://idp/sso"),
790 ],
791 ..Default::default()
792 },
793 EntitySetting::default(),
794 )
795 }
796
797 #[test]
798 fn parse_signed_response_extracts_name_id() -> Result<(), Box<dyn std::error::Error>> {
799 let idp = IdentityProvider::from_config(
800 &IdpMetadataConfig {
801 entity_id: "https://idp.example.com/metadata".into(),
802 signing_certs: vec![SP_SIGNING_CERT.into()],
803 single_sign_on_service: vec![Endpoint::new(Binding::Post, "https://idp/sso")],
804 ..Default::default()
805 },
806 EntitySetting {
807 private_key: Some(SP_PRIVKEY.into()),
808 signing_cert: Some(SP_SIGNING_CERT.into()),
809 request_signature_algorithm: RSA_SHA256.into(),
810 ..Default::default()
811 },
812 )?;
813 let sp = ServiceProvider::from_config(
814 &SpMetadataConfig {
815 entity_id: "https://sp.example.com/metadata".into(),
816 assertion_consumer_service: vec![Endpoint::new(
817 Binding::Post,
818 "http://sp.example.com/demo1/index.php?acs",
819 )],
820 ..Default::default()
821 },
822 EntitySetting::default(),
823 )?;
824 let request_id = "_41e758fee373d51639552c4b040b1090e97f6685";
825 let name_id = "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7";
826 let ctx = idp.create_login_response(
827 &sp,
828 Binding::Post,
829 &User::new(name_id),
830 &LoginResponseOptions {
831 in_response_to: Some(request_id),
832 ..Default::default()
833 },
834 )?;
835 let request = HttpRequest::post(vec![("SAMLResponse".into(), ctx.context)]);
836 let result =
837 sp.parse_login_response_with_request_id(&idp, Binding::Post, &request, request_id)?;
838 assert_eq!(result.extract.get_str("nameID"), Some(name_id));
839 Ok(())
840 }
841
842 #[test]
843 fn create_signed_post_request_verifies() -> Result<(), Box<dyn std::error::Error>> {
844 let sp = ServiceProvider::from_config(
845 &SpMetadataConfig {
846 entity_id: "https://sp.example.com/metadata".into(),
847 authn_requests_signed: true,
848 assertion_consumer_service: vec![Endpoint::new(Binding::Post, "https://sp/acs")],
849 ..Default::default()
850 },
851 EntitySetting {
852 private_key: Some(SP_PRIVKEY.into()),
853 signing_cert: Some(SP_SIGNING_CERT.into()),
854 request_signature_algorithm: RSA_SHA256.into(),
855 ..Default::default()
856 },
857 )?;
858 let ctx = sp.create_login_request(&signing_idp()?, Binding::Post, None)?;
859 let signed_xml = String::from_utf8(base64_decode(&ctx.context)?)?;
860 let (verified, _) = verify_signature(&signed_xml, &[SP_SIGNING_CERT.to_string()])?;
861 assert!(
862 verified,
863 "signed AuthnRequest should verify with the SP cert"
864 );
865 Ok(())
866 }
867
868 #[test]
869 fn create_signed_redirect_request_has_signature() -> Result<(), Box<dyn std::error::Error>> {
870 let sp = ServiceProvider::from_config(
871 &SpMetadataConfig {
872 entity_id: "https://sp.example.com/metadata".into(),
873 authn_requests_signed: true,
874 assertion_consumer_service: vec![Endpoint::new(Binding::Post, "https://sp/acs")],
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 ctx = sp.create_login_request(&signing_idp()?, Binding::Redirect, None)?;
885 assert!(ctx.context.contains("&SigAlg="));
886 assert!(ctx.context.contains("&Signature="));
887 Ok(())
888 }
889}