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, ResponseSignatureRequirement,
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(any(
393 feature = "crypto-rustcrypto",
394 feature = "crypto-aws-lc",
395 feature = "crypto-fips"
396 ))]
397 fn signed_request_context(
398 &self,
399 binding: Binding,
400 xml: &str,
401 destination: String,
402 relay_state: Option<String>,
403 id: String,
404 ) -> Result<BindingContext, SamlError> {
405 use crate::binding::{append_signature, build_redirect_octet};
406 use crate::crypto::{
407 construct_message_signature, construct_saml_signature, keys::load_private_key,
408 };
409
410 let key_pem = self
411 .setting
412 .private_key
413 .as_deref()
414 .ok_or_else(|| SamlError::MissingKey("private_key".into()))?;
415 let cert = self
416 .setting
417 .signing_cert
418 .as_deref()
419 .ok_or_else(|| SamlError::MissingKey("signing_cert".into()))?;
420 let sig_alg = &self.setting.request_signature_algorithm;
421 let key = load_private_key(key_pem, self.setting.private_key_pass.as_deref())?;
422
423 let (context, signature, sig_alg_out) = match binding {
424 Binding::Redirect => {
425 let octet = build_redirect_octet(
426 ParserType::SamlRequest,
427 xml,
428 relay_state.as_deref(),
429 sig_alg,
430 )?;
431 let sig = construct_message_signature(&octet, &key, sig_alg)?;
432 (append_signature(&destination, &octet, &sig), None, None)
433 }
434 Binding::Post => {
435 let signed = construct_saml_signature(
436 xml,
437 true,
438 &key,
439 cert,
440 sig_alg,
441 &self.setting.transformation_algorithms,
442 self.setting.signature_config.as_ref(),
443 )?;
444 (base64_encode(signed.as_bytes()), None, None)
445 }
446 Binding::SimpleSign => {
447 let octet = crate::binding::build_simplesign_octet(
448 ParserType::SamlRequest.query_param(),
449 xml,
450 relay_state.as_deref(),
451 sig_alg,
452 );
453 let sig = construct_message_signature(&octet, &key, sig_alg)?;
454 (
455 base64_encode(xml.as_bytes()),
456 Some(sig),
457 Some(sig_alg.clone()),
458 )
459 }
460 Binding::Artifact => {
461 return Err(SamlError::UnsupportedBinding {
462 binding: Binding::Artifact,
463 });
464 }
465 };
466 Ok(BindingContext {
467 id,
468 context,
469 relay_state,
470 entity_endpoint: destination,
471 binding,
472 request_type: "SAMLRequest",
473 signature,
474 sig_alg: sig_alg_out,
475 })
476 }
477
478 #[cfg(not(any(
479 feature = "crypto-rustcrypto",
480 feature = "crypto-aws-lc",
481 feature = "crypto-fips"
482 )))]
483 fn signed_request_context(
484 &self,
485 _binding: Binding,
486 _xml: &str,
487 _destination: String,
488 _relay_state: Option<String>,
489 _id: String,
490 ) -> Result<BindingContext, SamlError> {
491 Err(SamlError::Unsupported(
492 "signing AuthnRequest requires a crypto provider feature".into(),
493 ))
494 }
495
496 pub fn parse_login_response(
515 &self,
516 idp: &IdentityProvider,
517 binding: Binding,
518 request: &HttpRequest,
519 ) -> Result<FlowResult, SamlError> {
520 self.parse_unsolicited_login_response(idp, binding, request)
521 }
522
523 pub fn parse_unsolicited_login_response(
537 &self,
538 idp: &IdentityProvider,
539 binding: Binding,
540 request: &HttpRequest,
541 ) -> Result<FlowResult, SamlError> {
542 self.parse_login_response_inner(
543 idp,
544 binding,
545 request,
546 LoginResponseCorrelation::Unsolicited,
547 LoginResponseParseOptions::compatibility(self.setting.clock_drifts),
548 )
549 }
550
551 pub(crate) fn parse_unsolicited_login_response_at(
552 &self,
553 idp: &IdentityProvider,
554 binding: Binding,
555 request: &HttpRequest,
556 now: SystemTime,
557 clock_drifts: (i64, i64),
558 ) -> Result<FlowResult, SamlError> {
559 self.parse_login_response_inner(
560 idp,
561 binding,
562 request,
563 LoginResponseCorrelation::Unsolicited,
564 LoginResponseParseOptions::at(now, clock_drifts),
565 )
566 }
567
568 pub fn parse_login_response_with_request_id(
585 &self,
586 idp: &IdentityProvider,
587 binding: Binding,
588 request: &HttpRequest,
589 request_id: &str,
590 ) -> Result<FlowResult, SamlError> {
591 if request_id.is_empty() {
592 return Err(SamlError::InvalidInResponseTo);
593 }
594 self.parse_login_response_inner(
595 idp,
596 binding,
597 request,
598 LoginResponseCorrelation::MessageId(request_id),
599 LoginResponseParseOptions::compatibility(self.setting.clock_drifts),
600 )
601 }
602
603 pub(crate) fn parse_login_response_with_request_id_at(
604 &self,
605 idp: &IdentityProvider,
606 binding: Binding,
607 request: &HttpRequest,
608 request_id: &str,
609 options: LoginResponseParseOptions<'_>,
610 ) -> Result<FlowResult, SamlError> {
611 if request_id.is_empty() {
612 return Err(SamlError::InvalidInResponseTo);
613 }
614 self.parse_login_response_inner(
615 idp,
616 binding,
617 request,
618 LoginResponseCorrelation::MessageId(request_id),
619 options,
620 )
621 }
622
623 fn parse_login_response_inner(
624 &self,
625 idp: &IdentityProvider,
626 binding: Binding,
627 request: &HttpRequest,
628 correlation: LoginResponseCorrelation<'_>,
629 options: LoginResponseParseOptions<'_>,
630 ) -> Result<FlowResult, SamlError> {
631 let signing_certs = idp.metadata.x509_certificates(CertUse::Signing);
632 let decrypt_key = if self.setting.is_assertion_encrypted {
633 self.setting.enc_private_key.as_deref()
634 } else {
635 None
636 };
637 let audience = self.entity_id();
638 let expected_in_response_to = match correlation {
639 LoginResponseCorrelation::MessageId(request_id) => Some(request_id),
640 LoginResponseCorrelation::Unsolicited => None,
641 };
642 let recipient = match options.expected_recipient {
643 Some(recipient) => recipient.to_string(),
644 None => self
645 .metadata
646 .get_assertion_consumer_service(binding)
647 .ok_or_else(|| SamlError::MissingMetadata("AssertionConsumerService".into()))?,
648 };
649 let result = flow_with_expected_recipient(
650 &FlowOptions {
651 binding: Some(binding),
652 parser_type: Some(ParserType::SamlResponse),
653 check_signature: true,
654 from_issuer: idp.metadata.get_entity_id(),
655 signing_certs: &signing_certs,
656 decrypt_key,
657 decrypt_key_pass: self.setting.enc_private_key_pass.as_deref(),
658 allow_insecure_software_rsa_key_transport_decryption: self
659 .setting
660 .allow_insecure_software_rsa_key_transport_decryption,
661 clock_drifts: options.clock_drifts,
662 now: options.now,
663 redirect_inflate_max_bytes: self.setting.redirect_inflate_max_bytes,
664 xml_limits: self.setting.xml_limits,
665 expected_audience: self.setting.validate_audience.then_some(audience.as_str()),
666 expected_in_response_to,
667 },
668 request,
669 recipient.as_str(),
670 if self.setting.want_assertions_signed {
671 AssertionSignatureRequirement::Direct
672 } else {
673 AssertionSignatureRequirement::Compatible
674 },
675 if self.setting.want_message_signed {
676 ResponseSignatureRequirement::Required
677 } else if self.setting.want_encrypted_cbc_response_signed {
678 ResponseSignatureRequirement::RequiredForEncryptedCbc
679 } else {
680 ResponseSignatureRequirement::Optional
681 },
682 )?;
683 if matches!(correlation, LoginResponseCorrelation::Unsolicited)
684 && result
685 .extract
686 .get_str("response.inResponseTo")
687 .is_some_and(|actual| !actual.is_empty())
688 {
689 return Err(SamlError::in_response_to_mismatch(
690 None,
691 result.extract.get_str("response.inResponseTo"),
692 ));
693 }
694 if matches!(correlation, LoginResponseCorrelation::Unsolicited) {
695 reject_unsolicited_request_bound_bearer_confirmations(
696 &result.extract,
697 self.setting.xml_limits,
698 )?;
699 }
700 Ok(result)
701 }
702}
703
704#[cfg(test)]
705mod tests {
706 use super::*;
707 use crate::binding::{base64_decode, deflate_raw_decode};
708 use crate::metadata::{Endpoint, IdpMetadataConfig};
709 use url::Url;
710
711 fn unsigned_idp() -> Result<IdentityProvider, SamlError> {
712 IdentityProvider::from_config(
713 &IdpMetadataConfig {
714 entity_id: "https://idp.example.com/metadata".into(),
715 single_sign_on_service: vec![
716 Endpoint::new(Binding::Redirect, "https://idp.example.com/sso"),
717 Endpoint::new(Binding::Post, "https://idp.example.com/sso"),
718 ],
719 ..Default::default()
720 },
721 EntitySetting::default(),
722 )
723 }
724
725 fn unsigned_sp() -> Result<ServiceProvider, SamlError> {
726 ServiceProvider::from_config(
727 &SpMetadataConfig {
728 entity_id: "https://sp.example.com/metadata".into(),
729 assertion_consumer_service: vec![Endpoint::new(
730 Binding::Post,
731 "https://sp.example.com/acs",
732 )],
733 ..Default::default()
734 },
735 EntitySetting::default(),
736 )
737 }
738
739 #[test]
740 fn create_unsigned_login_request_redirect_round_trips() -> Result<(), Box<dyn std::error::Error>>
741 {
742 let ctx = unsigned_sp()?.create_login_request(&unsigned_idp()?, Binding::Redirect, None)?;
743 let url = Url::parse(&ctx.context)?;
744 let (_, value) = url
745 .query_pairs()
746 .find(|(k, _)| k == "SAMLRequest")
747 .ok_or("missing SAMLRequest")?;
748 let xml = String::from_utf8(deflate_raw_decode(&base64_decode(&value)?)?)?;
749 assert!(xml.contains("AssertionConsumerServiceURL=\"https://sp.example.com/acs\""));
750 assert!(url.query_pairs().all(|(k, _)| k != "Signature"));
751 Ok(())
752 }
753
754 #[test]
755 fn create_unsigned_login_request_post_is_base64() -> Result<(), Box<dyn std::error::Error>> {
756 let ctx = unsigned_sp()?.create_login_request(&unsigned_idp()?, Binding::Post, None)?;
757 let xml = String::from_utf8(base64_decode(&ctx.context)?)?;
758 assert!(xml.starts_with("<samlp:AuthnRequest"));
759 Ok(())
760 }
761
762 #[test]
763 fn custom_tag_replacement_overrides_request() -> Result<(), Box<dyn std::error::Error>> {
764 let replace = |_t: &str| {
765 (
766 "_custom".to_string(),
767 "<samlp:AuthnRequest ID=\"_custom\"/>".to_string(),
768 )
769 };
770 let ctx = unsigned_sp()?.create_login_request(
771 &unsigned_idp()?,
772 Binding::Post,
773 Some(&replace as &dyn Fn(&str) -> (String, String)),
774 )?;
775 assert_eq!(ctx.id, "_custom");
776 let xml = String::from_utf8(base64_decode(&ctx.context)?)?;
777 assert!(xml.contains("ID=\"_custom\""));
778 Ok(())
779 }
780}
781
782#[cfg(all(
783 test,
784 any(
785 feature = "crypto-rustcrypto",
786 feature = "crypto-aws-lc",
787 feature = "crypto-fips"
788 )
789))]
790mod crypto_tests {
791 use super::*;
792 use crate::binding::base64_decode;
793 use crate::constants::signature_algorithm::RSA_SHA256;
794 use crate::crypto::verify_signature;
795 use crate::entity::User;
796 use crate::idp::LoginResponseOptions;
797 use crate::metadata::{Endpoint, IdpMetadataConfig};
798
799 const IDP_CERT: &str = include_str!("../tests/fixtures/key/idp_cert.cer");
800 const SP_PRIVKEY: &str = include_str!("../tests/fixtures/key/sp_privkey.pem");
801 const SP_SIGNING_CERT: &str = include_str!("../tests/fixtures/key/sp_signing_cert.cer");
802
803 fn signing_idp() -> Result<IdentityProvider, SamlError> {
804 IdentityProvider::from_config(
805 &IdpMetadataConfig {
806 entity_id: "https://idp.example.com/metadata".into(),
807 signing_certs: vec![IDP_CERT.into()],
808 want_authn_requests_signed: true,
809 single_sign_on_service: vec![
810 Endpoint::new(Binding::Redirect, "https://idp/sso"),
811 Endpoint::new(Binding::Post, "https://idp/sso"),
812 ],
813 ..Default::default()
814 },
815 EntitySetting::default(),
816 )
817 }
818
819 #[test]
820 fn parse_signed_response_extracts_name_id() -> Result<(), Box<dyn std::error::Error>> {
821 let idp = IdentityProvider::from_config(
822 &IdpMetadataConfig {
823 entity_id: "https://idp.example.com/metadata".into(),
824 signing_certs: vec![SP_SIGNING_CERT.into()],
825 single_sign_on_service: vec![Endpoint::new(Binding::Post, "https://idp/sso")],
826 ..Default::default()
827 },
828 EntitySetting {
829 private_key: Some(SP_PRIVKEY.into()),
830 signing_cert: Some(SP_SIGNING_CERT.into()),
831 request_signature_algorithm: RSA_SHA256.into(),
832 ..Default::default()
833 },
834 )?;
835 let sp = ServiceProvider::from_config(
836 &SpMetadataConfig {
837 entity_id: "https://sp.example.com/metadata".into(),
838 assertion_consumer_service: vec![Endpoint::new(
839 Binding::Post,
840 "http://sp.example.com/demo1/index.php?acs",
841 )],
842 ..Default::default()
843 },
844 EntitySetting::default(),
845 )?;
846 let request_id = "_41e758fee373d51639552c4b040b1090e97f6685";
847 let name_id = "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7";
848 let ctx = idp.create_login_response(
849 &sp,
850 Binding::Post,
851 &User::new(name_id),
852 &LoginResponseOptions {
853 in_response_to: Some(request_id),
854 ..Default::default()
855 },
856 )?;
857 let request = HttpRequest::post(vec![("SAMLResponse".into(), ctx.context)]);
858 let result =
859 sp.parse_login_response_with_request_id(&idp, Binding::Post, &request, request_id)?;
860 assert_eq!(result.extract.get_str("nameID"), Some(name_id));
861 Ok(())
862 }
863
864 #[test]
865 fn create_signed_post_request_verifies() -> Result<(), Box<dyn std::error::Error>> {
866 let sp = ServiceProvider::from_config(
867 &SpMetadataConfig {
868 entity_id: "https://sp.example.com/metadata".into(),
869 authn_requests_signed: true,
870 assertion_consumer_service: vec![Endpoint::new(Binding::Post, "https://sp/acs")],
871 ..Default::default()
872 },
873 EntitySetting {
874 private_key: Some(SP_PRIVKEY.into()),
875 signing_cert: Some(SP_SIGNING_CERT.into()),
876 request_signature_algorithm: RSA_SHA256.into(),
877 ..Default::default()
878 },
879 )?;
880 let ctx = sp.create_login_request(&signing_idp()?, Binding::Post, None)?;
881 let signed_xml = String::from_utf8(base64_decode(&ctx.context)?)?;
882 let (verified, _) = verify_signature(&signed_xml, &[SP_SIGNING_CERT.to_string()])?;
883 assert!(
884 verified,
885 "signed AuthnRequest should verify with the SP cert"
886 );
887 Ok(())
888 }
889
890 #[test]
891 fn create_signed_redirect_request_has_signature() -> Result<(), Box<dyn std::error::Error>> {
892 let sp = ServiceProvider::from_config(
893 &SpMetadataConfig {
894 entity_id: "https://sp.example.com/metadata".into(),
895 authn_requests_signed: true,
896 assertion_consumer_service: vec![Endpoint::new(Binding::Post, "https://sp/acs")],
897 ..Default::default()
898 },
899 EntitySetting {
900 private_key: Some(SP_PRIVKEY.into()),
901 signing_cert: Some(SP_SIGNING_CERT.into()),
902 request_signature_algorithm: RSA_SHA256.into(),
903 ..Default::default()
904 },
905 )?;
906 let ctx = sp.create_login_request(&signing_idp()?, Binding::Redirect, None)?;
907 assert!(ctx.context.contains("&SigAlg="));
908 assert!(ctx.context.contains("&Signature="));
909 Ok(())
910 }
911}