1use crate::constants::{Binding, ParserType};
4use crate::entity::{
5 capture_idp_issuance_window, generate_id, BindingContext, CustomTagReplacement, EntitySetting,
6 User,
7};
8use crate::error::SamlError;
9use crate::flow::{FlowOptions, FlowResult, HttpRequest};
10use crate::metadata::{try_generate_idp_metadata, IdpMetadata, IdpMetadataConfig};
11use crate::model::{Status, TopLevelStatusCode};
12use crate::sp::{ServiceProvider, WebBrowserSsoProducer};
13use crate::template::{
14 apply_tag_prefixes, attr_tag, attribute_statement_builder, replace_tags_by_value,
15 validate_tag_prefix, ATTRIBUTE_STATEMENT_TEMPLATE, ATTRIBUTE_TEMPLATE, LOGIN_RESPONSE_TEMPLATE,
16};
17
18mod login_response;
19
20use login_response::{
21 render_default_login_response, render_error_login_response, ErrorLoginResponseXml,
22 LoginResponseXml,
23};
24use std::time::SystemTime;
25
26#[derive(Default)]
28pub struct LoginResponseOptions<'a> {
29 pub in_response_to: Option<&'a str>,
31 pub relay_state: Option<&'a str>,
33 pub encrypt_then_sign: bool,
38 pub custom: Option<CustomTagReplacement<'a>>,
40}
41
42#[derive(Debug, Clone, Copy, Default)]
43pub(crate) struct LoginResponseOverrides<'a> {
44 pub(crate) acs: Option<&'a str>,
45 pub(crate) name_id_format: Option<&'a str>,
46 pub(crate) issuance_lifetime: Option<time::Duration>,
47 pub(crate) web_browser_sso_producer: WebBrowserSsoProducer,
48 pub(crate) status: Option<&'a Status>,
49}
50
51#[derive(Clone, Copy)]
52struct LoginResponseRendering<'a> {
53 name_id_format: Option<&'a str>,
54 custom: Option<CustomTagReplacement<'a>>,
55 issuance_lifetime: time::Duration,
56 web_browser_sso_producer: WebBrowserSsoProducer,
57 status: Option<&'a Status>,
58}
59
60#[derive(Debug, Clone)]
62pub struct IdentityProvider {
63 pub setting: EntitySetting,
65 pub metadata: IdpMetadata,
67}
68
69impl IdentityProvider {
70 pub fn from_metadata(xml: &str, mut setting: EntitySetting) -> Result<Self, SamlError> {
78 let metadata = IdpMetadata::from_xml(xml)?;
79 setting.want_authn_requests_signed = metadata.is_want_authn_requests_signed();
80 let formats = metadata.get_name_id_format();
81 if !formats.is_empty() {
82 setting.name_id_format = formats;
83 }
84 if setting.entity_id.is_none() {
85 setting.entity_id = metadata.get_entity_id().map(str::to_string);
86 }
87 Ok(Self { setting, metadata })
88 }
89
90 pub fn from_config(
98 config: &IdpMetadataConfig,
99 setting: EntitySetting,
100 ) -> Result<Self, SamlError> {
101 let metadata_xml = try_generate_idp_metadata(config)?;
102 Self::from_metadata(&metadata_xml, setting)
103 }
104
105 pub fn metadata_xml(&self) -> &str {
107 self.metadata.get_metadata()
108 }
109
110 fn entity_id(&self) -> String {
111 self.setting
112 .entity_id
113 .clone()
114 .or_else(|| self.metadata.get_entity_id().map(str::to_string))
115 .unwrap_or_default()
116 }
117
118 fn render_login_response(
129 &self,
130 sp: &ServiceProvider,
131 in_response_to: Option<&str>,
132 user: &User,
133 acs: &str,
134 rendering: LoginResponseRendering<'_>,
135 ) -> Result<(String, String), SamlError> {
136 validate_tag_prefix("protocol", &self.setting.tag_prefix_protocol)?;
137 validate_tag_prefix("assertion", &self.setting.tag_prefix_assertion)?;
138 let tmpl = self.setting.login_response_template.as_ref();
139 let attributes = tmpl.map(|t| t.attributes.as_slice()).unwrap_or(&[]);
140 let has_custom_context = tmpl.and_then(|t| t.context.as_ref()).is_some();
141 let fallback_status = Status::success();
142 let status = rendering.status.unwrap_or(&fallback_status);
143 let error_response = status.top_level() != TopLevelStatusCode::Success;
144 let uses_template = rendering.custom.is_some() || has_custom_context;
145 if rendering.web_browser_sso_producer.follows() && uses_template {
146 return Err(SamlError::Invalid(
147 "Web Browser SSO generation rules require the built-in login response renderer"
148 .into(),
149 ));
150 }
151 if error_response && uses_template {
152 return Err(SamlError::Invalid(
153 "an error Response omits assertions and cannot use a login response template"
154 .into(),
155 ));
156 }
157 if status.subordinate().is_some() && uses_template {
158 return Err(SamlError::Invalid(
159 "login response template StatusCode placeholder cannot carry a subordinate status code"
160 .into(),
161 ));
162 }
163 if error_response {
164 let window = capture_idp_issuance_window(rendering.issuance_lifetime)?;
165 let id = generate_id();
166 let issuer = self.entity_id();
167 let emitted_in_response_to =
168 response_in_response_to(rendering.web_browser_sso_producer, in_response_to);
169 let xml = render_error_login_response(&ErrorLoginResponseXml {
170 protocol_prefix: &self.setting.tag_prefix_protocol,
171 assertion_prefix: &self.setting.tag_prefix_assertion,
172 response_id: &id,
173 issue_instant: &window.issue_instant,
174 destination: acs,
175 issuer: &issuer,
176 status,
177 in_response_to: emitted_in_response_to,
178 })?;
179 return Ok((id, xml));
180 }
181 if rendering.custom.is_none() && !has_custom_context {
182 let window = capture_idp_issuance_window(rendering.issuance_lifetime)?;
183 let default_name_id_format = self
184 .setting
185 .name_id_format
186 .first()
187 .cloned()
188 .unwrap_or_default();
189 let name_id_format = rendering
190 .name_id_format
191 .unwrap_or(default_name_id_format.as_str());
192 let id = generate_id();
193 let assertion_id = generate_id();
194 let audience = sp.metadata.get_entity_id().unwrap_or_default().to_string();
195 let issuer = self.entity_id();
196 let emitted_in_response_to =
197 response_in_response_to(rendering.web_browser_sso_producer, in_response_to);
198 let supports_single_logout = !self.metadata.get_support_bindings().is_empty();
199 let session_index = (rendering.web_browser_sso_producer.follows()
200 && supports_single_logout)
201 .then_some(assertion_id.as_str());
202 let xml = render_default_login_response(&LoginResponseXml {
203 protocol_prefix: &self.setting.tag_prefix_protocol,
204 assertion_prefix: &self.setting.tag_prefix_assertion,
205 response_id: &id,
206 assertion_id: &assertion_id,
207 issue_instant: &window.issue_instant,
208 destination: acs,
209 subject_recipient: acs,
210 issuer: &issuer,
211 status,
212 subject_confirmation_not_on_or_after: &window.expiration,
213 conditions_not_before: &window.issue_instant,
214 conditions_not_on_or_after: &window.expiration,
215 audience: &audience,
216 name_id_format,
217 name_id: &user.name_id,
218 in_response_to: emitted_in_response_to,
219 authn_statement: rendering.web_browser_sso_producer.follows().then_some(
220 login_response::AuthnStatementXml {
221 authn_instant: &window.issue_instant,
222 session_index,
223 },
224 ),
225 attributes,
226 user_attributes: &user.attributes,
227 })?;
228 return Ok((id, xml));
229 }
230
231 let base = tmpl
232 .and_then(|t| t.context.as_deref())
233 .unwrap_or(LOGIN_RESPONSE_TEMPLATE);
234 let attribute_statement = if attributes.is_empty() {
235 String::new()
236 } else {
237 attribute_statement_builder(
238 attributes,
239 ATTRIBUTE_TEMPLATE,
240 ATTRIBUTE_STATEMENT_TEMPLATE,
241 )
242 };
243 let prepared = base.replacen("{AttributeStatement}", &attribute_statement, 1);
244 let prepared = apply_tag_prefixes(
245 &prepared,
246 &self.setting.tag_prefix_protocol,
247 &self.setting.tag_prefix_assertion,
248 );
249 if let Some(f) = rendering.custom {
250 return Ok(f(&prepared));
251 }
252 let window = capture_idp_issuance_window(rendering.issuance_lifetime)?;
253 let default_name_id_format = self
254 .setting
255 .name_id_format
256 .first()
257 .cloned()
258 .unwrap_or_default();
259 let name_id_format = rendering
260 .name_id_format
261 .unwrap_or(default_name_id_format.as_str());
262 let id = generate_id();
263 let mut tags: Vec<(&str, String)> = vec![
264 ("ID", id.clone()),
265 ("AssertionID", generate_id()),
266 ("Destination", acs.to_string()),
267 ("SubjectRecipient", acs.to_string()),
268 ("AssertionConsumerServiceURL", acs.to_string()),
269 (
270 "Audience",
271 sp.metadata.get_entity_id().unwrap_or_default().to_string(),
272 ),
273 ("Issuer", self.entity_id()),
274 ("IssueInstant", window.issue_instant.clone()),
275 ("StatusCode", status.top_level().as_uri().to_string()),
276 ("ConditionsNotBefore", window.issue_instant),
277 ("ConditionsNotOnOrAfter", window.expiration.clone()),
278 ("SubjectConfirmationDataNotOnOrAfter", window.expiration),
279 ("NameIDFormat", name_id_format.to_string()),
280 ("NameID", user.name_id.clone()),
281 (
282 "InResponseTo",
283 in_response_to.unwrap_or_default().to_string(),
284 ),
285 ("AuthnStatement", String::new()),
286 ];
287 let attr_pairs: Vec<(String, String)> = user
290 .attributes
291 .iter()
292 .map(|(tag, value)| (attr_tag(tag), value.clone()))
293 .collect();
294 for (key, value) in &attr_pairs {
295 tags.push((key.as_str(), value.clone()));
296 }
297 Ok((id, replace_tags_by_value(&prepared, &tags)))
298 }
299
300 pub fn create_login_response(
317 &self,
318 sp: &ServiceProvider,
319 binding: Binding,
320 user: &User,
321 options: &LoginResponseOptions<'_>,
322 ) -> Result<BindingContext, SamlError> {
323 self.create_login_response_inner(
324 sp,
325 binding,
326 user,
327 options,
328 LoginResponseOverrides::default(),
329 )
330 }
331
332 pub(crate) fn create_login_response_with_overrides(
333 &self,
334 sp: &ServiceProvider,
335 binding: Binding,
336 user: &User,
337 options: &LoginResponseOptions<'_>,
338 overrides: LoginResponseOverrides<'_>,
339 ) -> Result<BindingContext, SamlError> {
340 self.create_login_response_inner(sp, binding, user, options, overrides)
341 }
342
343 fn create_login_response_inner(
344 &self,
345 sp: &ServiceProvider,
346 binding: Binding,
347 user: &User,
348 options: &LoginResponseOptions<'_>,
349 overrides: LoginResponseOverrides<'_>,
350 ) -> Result<BindingContext, SamlError> {
351 if matches!(binding, Binding::Artifact) {
352 return Err(SamlError::UnsupportedBinding {
353 binding: Binding::Artifact,
354 });
355 }
356 let acs = match overrides.acs {
357 Some(acs) => acs.to_string(),
358 None => sp
359 .metadata
360 .get_assertion_consumer_service(binding)
361 .ok_or_else(|| SamlError::MissingMetadata("AssertionConsumerService".into()))?,
362 };
363 let (id, raw) = self.render_login_response(
364 sp,
365 options.in_response_to,
366 user,
367 &acs,
368 LoginResponseRendering {
369 name_id_format: overrides.name_id_format,
370 custom: options.custom,
371 issuance_lifetime: overrides
372 .issuance_lifetime
373 .unwrap_or(time::Duration::seconds(300)),
374 web_browser_sso_producer: overrides.web_browser_sso_producer,
375 status: overrides.status,
376 },
377 )?;
378 let error_response = overrides
379 .status
380 .is_some_and(|status| status.top_level() != TopLevelStatusCode::Success);
381 let signed = self.finalize_login_response(
382 sp,
383 binding,
384 &raw,
385 options.encrypt_then_sign,
386 error_response,
387 )?;
388 let relay = options.relay_state.map(str::to_string);
389 let (context, signature, sig_alg) =
390 self.bind_response(binding, &signed, &acs, relay.as_deref())?;
391 Ok(BindingContext {
392 id,
393 context,
394 relay_state: relay,
395 entity_endpoint: acs,
396 binding,
397 request_type: "SAMLResponse",
398 signature,
399 sig_alg,
400 })
401 }
402
403 #[cfg(any(
405 feature = "crypto-rustcrypto",
406 feature = "crypto-aws-lc",
407 feature = "crypto-fips"
408 ))]
409 fn bind_response(
410 &self,
411 binding: Binding,
412 xml: &str,
413 acs: &str,
414 relay_state: Option<&str>,
415 ) -> Result<(String, Option<String>, Option<String>), SamlError> {
416 use crate::binding::{append_signature, base64_encode, build_redirect_octet};
417 use crate::crypto::{construct_message_signature, keys::load_private_key};
418
419 match binding {
420 Binding::Post => Ok((base64_encode(xml.as_bytes()), None, None)),
421 Binding::Redirect => {
422 let sig_alg = &self.setting.request_signature_algorithm;
423 let key = load_private_key(
424 self.setting.private_key.as_deref().unwrap_or_default(),
425 self.setting.private_key_pass.as_deref(),
426 )?;
427 let octet =
428 build_redirect_octet(ParserType::SamlResponse, xml, relay_state, sig_alg)?;
429 let sig = construct_message_signature(&octet, &key, sig_alg)?;
430 Ok((append_signature(acs, &octet, &sig), None, None))
431 }
432 Binding::SimpleSign => {
433 let sig_alg = &self.setting.request_signature_algorithm;
434 let key = load_private_key(
435 self.setting.private_key.as_deref().unwrap_or_default(),
436 self.setting.private_key_pass.as_deref(),
437 )?;
438 let octet = crate::binding::build_simplesign_octet(
439 ParserType::SamlResponse.query_param(),
440 xml,
441 relay_state,
442 sig_alg,
443 );
444 let sig = construct_message_signature(&octet, &key, sig_alg)?;
445 Ok((
446 base64_encode(xml.as_bytes()),
447 Some(sig),
448 Some(sig_alg.clone()),
449 ))
450 }
451 Binding::Artifact => Err(SamlError::UnsupportedBinding {
452 binding: Binding::Artifact,
453 }),
454 }
455 }
456
457 #[cfg(not(any(
458 feature = "crypto-rustcrypto",
459 feature = "crypto-aws-lc",
460 feature = "crypto-fips"
461 )))]
462 fn bind_response(
463 &self,
464 _binding: Binding,
465 _xml: &str,
466 _acs: &str,
467 _relay_state: Option<&str>,
468 ) -> Result<(String, Option<String>, Option<String>), SamlError> {
469 Err(SamlError::Unsupported(
470 "createLoginResponse requires a crypto provider feature".into(),
471 ))
472 }
473
474 #[cfg(any(
475 feature = "crypto-rustcrypto",
476 feature = "crypto-aws-lc",
477 feature = "crypto-fips"
478 ))]
479 fn finalize_login_response(
480 &self,
481 sp: &ServiceProvider,
482 binding: Binding,
483 raw: &str,
484 _encrypt_then_sign: bool,
485 error_response: bool,
486 ) -> Result<String, SamlError> {
487 use crate::crypto::{construct_saml_signature, encrypt_assertion, keys::load_private_key};
488
489 let key_pem = self
490 .setting
491 .private_key
492 .as_deref()
493 .ok_or_else(|| SamlError::MissingKey("private_key".into()))?;
494 let cert = self
495 .setting
496 .signing_cert
497 .as_deref()
498 .ok_or_else(|| SamlError::MissingKey("signing_cert".into()))?;
499 let sig_alg = &self.setting.request_signature_algorithm;
500 let key = load_private_key(key_pem, self.setting.private_key_pass.as_deref())?;
501
502 let mut xml = raw.to_string();
503 if error_response {
504 if binding == Binding::Post {
506 xml = construct_saml_signature(
507 &xml,
508 true,
509 &key,
510 cert,
511 sig_alg,
512 &sp.setting.transformation_algorithms,
513 self.setting.signature_config.as_ref(),
514 )?;
515 }
516 return Ok(xml);
517 }
518
519 let want_assertions_signed = sp.metadata.is_want_assertions_signed();
520 let sign_message =
523 binding == Binding::Post && (sp.setting.want_message_signed || !want_assertions_signed);
524
525 if want_assertions_signed {
527 xml = construct_saml_signature(
528 &xml,
529 false,
530 &key,
531 cert,
532 sig_alg,
533 &sp.setting.transformation_algorithms,
534 None,
535 )?;
536 }
537 if sign_message && !self.setting.is_assertion_encrypted {
541 xml = construct_saml_signature(
542 &xml,
543 true,
544 &key,
545 cert,
546 sig_alg,
547 &sp.setting.transformation_algorithms,
548 self.setting.signature_config.as_ref(),
549 )?;
550 }
551 if self.setting.is_assertion_encrypted {
552 let encrypt_cert = sp
553 .metadata
554 .get_x509_certificate(crate::constants::CertUse::Encryption)
555 .ok_or_else(|| SamlError::MissingMetadata("encryption certificate".into()))?;
556 xml = encrypt_assertion(
557 &xml,
558 &encrypt_cert,
559 &self.setting.data_encryption_algorithm,
560 &self.setting.key_encryption_algorithm,
561 &self.setting.tag_prefix_encrypted_assertion,
562 )?;
563 }
564 if sign_message && self.setting.is_assertion_encrypted {
565 xml = construct_saml_signature(
566 &xml,
567 true,
568 &key,
569 cert,
570 sig_alg,
571 &sp.setting.transformation_algorithms,
572 self.setting.signature_config.as_ref(),
573 )?;
574 }
575 Ok(xml)
576 }
577
578 #[cfg(not(any(
579 feature = "crypto-rustcrypto",
580 feature = "crypto-aws-lc",
581 feature = "crypto-fips"
582 )))]
583 fn finalize_login_response(
584 &self,
585 _sp: &ServiceProvider,
586 _binding: Binding,
587 _raw: &str,
588 _encrypt_then_sign: bool,
589 _error_response: bool,
590 ) -> Result<String, SamlError> {
591 Err(SamlError::Unsupported(
592 "createLoginResponse requires a crypto provider feature".into(),
593 ))
594 }
595
596 pub fn parse_login_request(
609 &self,
610 sp: &ServiceProvider,
611 binding: Binding,
612 request: &HttpRequest,
613 ) -> Result<FlowResult, SamlError> {
614 Ok(self
615 .parse_login_request_at_inner(sp, binding, request, None, self.setting.clock_drifts)?
616 .0)
617 }
618
619 pub(crate) fn parse_login_request_at(
620 &self,
621 sp: &ServiceProvider,
622 binding: Binding,
623 request: &HttpRequest,
624 now: SystemTime,
625 clock_drifts: (i64, i64),
626 ) -> Result<(FlowResult, bool), SamlError> {
627 self.parse_login_request_at_inner(sp, binding, request, Some(now), clock_drifts)
628 }
629
630 fn parse_login_request_at_inner(
631 &self,
632 sp: &ServiceProvider,
633 binding: Binding,
634 request: &HttpRequest,
635 now: Option<SystemTime>,
636 clock_drifts: (i64, i64),
637 ) -> Result<(FlowResult, bool), SamlError> {
638 let signing_certs = sp
639 .metadata
640 .x509_certificates(crate::constants::CertUse::Signing);
641 crate::flow::flow_with_authentication(
642 &FlowOptions {
643 binding: Some(binding),
644 parser_type: Some(ParserType::SamlRequest),
645 check_signature: self.metadata.is_want_authn_requests_signed(),
646 verify_signature_if_present: self.setting.verify_authn_request_signature_if_present,
647 require_audience_restriction: true,
648 strict_xml_signature_profile: false,
649 from_issuer: sp.metadata.get_entity_id(),
650 signing_certs: &signing_certs,
651 decrypt_key: None,
652 decrypt_key_pass: None,
653 allow_insecure_software_rsa_key_transport_decryption: false,
654 clock_drifts,
655 now,
656 redirect_inflate_max_bytes: self.setting.redirect_inflate_max_bytes,
657 xml_limits: self.setting.xml_limits,
658 expected_audience: None,
659 expected_in_response_to: None,
660 },
661 request,
662 )
663 }
664}
665
666fn response_in_response_to(
667 producer: WebBrowserSsoProducer,
668 in_response_to: Option<&str>,
669) -> Option<&str> {
670 let raw = in_response_to.unwrap_or("");
671 if producer.follows() && raw.is_empty() {
672 None
673 } else {
674 Some(raw)
675 }
676}
677
678#[cfg(test)]
679mod tests {
680 use super::*;
681 use crate::constants::Binding;
682 use crate::metadata::{Endpoint, SpMetadataConfig};
683
684 const IDPMETA: &str = include_str!("../tests/fixtures/idpmeta.xml");
685
686 fn unsigned_idp() -> Result<IdentityProvider, SamlError> {
687 IdentityProvider::from_config(
688 &IdpMetadataConfig {
689 entity_id: "https://idp.example.com/metadata".into(),
690 single_sign_on_service: vec![Endpoint::new(Binding::Post, "https://idp/sso")],
691 ..Default::default()
692 },
693 EntitySetting::default(),
694 )
695 }
696
697 fn unsigned_sp(entity_id: &str) -> Result<ServiceProvider, SamlError> {
698 ServiceProvider::from_config(
699 &SpMetadataConfig {
700 entity_id: entity_id.into(),
701 assertion_consumer_service: vec![Endpoint::new(Binding::Post, "https://sp/acs")],
702 ..Default::default()
703 },
704 EntitySetting::default(),
705 )
706 }
707
708 #[test]
709 fn from_metadata_merges_flags() -> Result<(), Box<dyn std::error::Error>> {
710 let idp = IdentityProvider::from_metadata(IDPMETA, EntitySetting::default())?;
711 assert!(idp.setting.want_authn_requests_signed);
712 assert_eq!(
713 idp.metadata
714 .get_single_sign_on_service(Binding::Redirect)
715 .as_deref(),
716 Some("https://idp.example.org/sso/SingleSignOnService")
717 );
718 Ok(())
719 }
720
721 #[test]
722 fn idp_from_config_rejects_missing_sso() {
723 let cfg = IdpMetadataConfig {
724 entity_id: "https://idp.example.com/metadata".into(),
725 ..Default::default()
726 };
727
728 let result = IdentityProvider::from_config(&cfg, EntitySetting::default());
729
730 assert!(matches!(
731 result,
732 Err(SamlError::MissingMetadata(name)) if name == "SingleSignOnService"
733 ));
734 }
735
736 #[test]
737 fn parse_login_request_accepts_matching_sp_issuer() -> Result<(), Box<dyn std::error::Error>> {
738 let idp = unsigned_idp()?;
739 let sp = unsigned_sp("https://sp.example.com/metadata")?;
740 let ctx = sp.create_login_request(&idp, Binding::Post, None)?;
741 let request = HttpRequest::post(vec![("SAMLRequest".into(), ctx.context)]);
742
743 let result = idp.parse_login_request(&sp, Binding::Post, &request)?;
744
745 assert_eq!(
746 result.extract.get_str("issuer"),
747 Some("https://sp.example.com/metadata")
748 );
749 Ok(())
750 }
751
752 #[test]
753 fn parse_login_request_rejects_unexpected_sp_issuer() -> Result<(), Box<dyn std::error::Error>>
754 {
755 let idp = unsigned_idp()?;
756 let expected_sp = unsigned_sp("https://expected-sp.example.com/metadata")?;
757 let attacker_sp = unsigned_sp("https://attacker-sp.example.com/metadata")?;
758 let ctx = attacker_sp.create_login_request(&idp, Binding::Post, None)?;
759 let request = HttpRequest::post(vec![("SAMLRequest".into(), ctx.context)]);
760
761 let result = idp.parse_login_request(&expected_sp, Binding::Post, &request);
762
763 assert!(matches!(result, Err(SamlError::IssuerMismatch { .. })));
764 Ok(())
765 }
766}
767
768#[cfg(all(
769 test,
770 any(
771 feature = "crypto-rustcrypto",
772 feature = "crypto-aws-lc",
773 feature = "crypto-fips"
774 )
775))]
776mod crypto_tests {
777 use super::*;
778 use crate::constants::signature_algorithm::RSA_SHA256;
779 use crate::metadata::{Endpoint, SpMetadataConfig};
780
781 const PRIVKEY: &str = include_str!("../tests/fixtures/key/sp_privkey.pem");
783 const CERT: &str = include_str!("../tests/fixtures/key/sp_signing_cert.cer");
784
785 fn signing_setting() -> EntitySetting {
786 EntitySetting {
787 private_key: Some(PRIVKEY.into()),
788 signing_cert: Some(CERT.into()),
789 request_signature_algorithm: RSA_SHA256.into(),
790 ..Default::default()
791 }
792 }
793
794 fn idp_with_setting(setting: EntitySetting) -> Result<IdentityProvider, SamlError> {
795 IdentityProvider::from_config(
796 &IdpMetadataConfig {
797 entity_id: "https://idp.example.com/metadata".into(),
798 signing_certs: vec![CERT.into()],
799 want_authn_requests_signed: true,
800 single_sign_on_service: vec![Endpoint::new(Binding::Post, "https://idp/sso")],
801 ..Default::default()
802 },
803 setting,
804 )
805 }
806
807 fn idp() -> Result<IdentityProvider, SamlError> {
808 idp_with_setting(signing_setting())
809 }
810
811 fn signed_sp(entity_id: &str) -> Result<ServiceProvider, SamlError> {
812 ServiceProvider::from_config(
813 &SpMetadataConfig {
814 entity_id: entity_id.into(),
815 authn_requests_signed: true,
816 want_assertions_signed: true,
817 signing_certs: vec![CERT.into()],
818 assertion_consumer_service: vec![Endpoint::new(Binding::Post, "https://sp/acs")],
819 ..Default::default()
820 },
821 signing_setting(),
822 )
823 }
824
825 fn sp() -> Result<ServiceProvider, SamlError> {
826 signed_sp("https://sp.example.com/metadata")
827 }
828
829 #[test]
830 fn idp_response_consumed_by_sp() -> Result<(), Box<dyn std::error::Error>> {
831 let (idp, sp) = (idp()?, sp()?);
832 let ctx = idp.create_login_response(
833 &sp,
834 Binding::Post,
835 &User::new("user@example.com"),
836 &LoginResponseOptions {
837 in_response_to: Some("_req123"),
838 ..Default::default()
839 },
840 )?;
841 let request = HttpRequest::post(vec![("SAMLResponse".into(), ctx.context)]);
842 let result =
843 sp.parse_login_response_with_request_id(&idp, Binding::Post, &request, "_req123")?;
844 assert_eq!(result.extract.get_str("nameID"), Some("user@example.com"));
845 assert_eq!(
846 result.extract.get_str("issuer"),
847 Some("https://idp.example.com/metadata")
848 );
849 Ok(())
850 }
851
852 #[test]
853 fn raw_login_response_defaults_to_five_minute_issuance_window_for_standard_renderers(
854 ) -> Result<(), Box<dyn std::error::Error>> {
855 use crate::binding::base64_decode;
856 use crate::template::{LoginResponseTemplate, LOGIN_RESPONSE_TEMPLATE};
857 use crate::xml::dom::parse;
858 use time::{format_description::well_known::Rfc3339, OffsetDateTime};
859
860 let mut custom_template = signing_setting();
861 custom_template.login_response_template = Some(LoginResponseTemplate {
862 context: Some(LOGIN_RESPONSE_TEMPLATE.into()),
863 attributes: Vec::new(),
864 });
865
866 for setting in [signing_setting(), custom_template] {
867 let idp = idp_with_setting(setting)?;
868 let sp = sp()?;
869 let ctx = idp.create_login_response(
870 &sp,
871 Binding::Post,
872 &User::new("user@example.com"),
873 &LoginResponseOptions {
874 in_response_to: Some("_req123"),
875 ..Default::default()
876 },
877 )?;
878 let xml = String::from_utf8(base64_decode(&ctx.context)?)?;
879 let document = parse(&xml)?;
880 let response_issue_instant = document
881 .root
882 .attr("IssueInstant")
883 .ok_or("missing Response IssueInstant")?;
884 let assertion = document
885 .root
886 .children
887 .iter()
888 .find(|node| node.local_name == "Assertion")
889 .ok_or("missing Assertion")?;
890 let assertion_issue_instant = assertion
891 .attr("IssueInstant")
892 .ok_or("missing Assertion IssueInstant")?;
893 let conditions = assertion
894 .children
895 .iter()
896 .find(|node| node.local_name == "Conditions")
897 .ok_or("missing Conditions")?;
898 let conditions_not_before = conditions
899 .attr("NotBefore")
900 .ok_or("missing Conditions NotBefore")?;
901 let conditions_expiration = conditions
902 .attr("NotOnOrAfter")
903 .ok_or("missing Conditions NotOnOrAfter")?;
904 let bearer_expiration = assertion
905 .children
906 .iter()
907 .find(|node| node.local_name == "Subject")
908 .and_then(|node| {
909 node.children
910 .iter()
911 .find(|node| node.local_name == "SubjectConfirmation")
912 })
913 .and_then(|node| {
914 node.children
915 .iter()
916 .find(|node| node.local_name == "SubjectConfirmationData")
917 })
918 .and_then(|node| node.attr("NotOnOrAfter"))
919 .ok_or("missing SubjectConfirmationData NotOnOrAfter")?;
920
921 assert_eq!(response_issue_instant, assertion_issue_instant);
922 assert_eq!(response_issue_instant, conditions_not_before);
923 assert_eq!(conditions_expiration, bearer_expiration);
924 assert_eq!(
925 OffsetDateTime::parse(conditions_expiration, &Rfc3339)?
926 - OffsetDateTime::parse(response_issue_instant, &Rfc3339)?,
927 time::Duration::minutes(5)
928 );
929 }
930 Ok(())
931 }
932
933 #[test]
934 fn login_response_with_attributes() -> Result<(), Box<dyn std::error::Error>> {
935 use crate::template::{LoginResponseAttribute, LoginResponseTemplate};
936 let mut setting = signing_setting();
937 setting.login_response_template = Some(LoginResponseTemplate {
938 context: None,
939 attributes: vec![LoginResponseAttribute {
940 name: "mail".into(),
941 name_format: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic".into(),
942 value_xsi_type: "xs:string".into(),
943 value_tag: "email".into(),
944 value_xmlns_xs: None,
945 value_xmlns_xsi: None,
946 }],
947 });
948 let idp = IdentityProvider::from_config(
949 &IdpMetadataConfig {
950 entity_id: "https://idp.example.com/metadata".into(),
951 signing_certs: vec![CERT.into()],
952 want_authn_requests_signed: true,
953 single_sign_on_service: vec![Endpoint::new(Binding::Post, "https://idp/sso")],
954 ..Default::default()
955 },
956 setting,
957 )?;
958 let sp = sp()?;
959 let user = User {
960 name_id: "alice@example.com".into(),
961 attributes: vec![("email".into(), "alice@example.com".into())],
962 session_index: None,
963 };
964 let ctx = idp.create_login_response(
965 &sp,
966 Binding::Post,
967 &user,
968 &LoginResponseOptions {
969 in_response_to: Some("_r1"),
970 ..Default::default()
971 },
972 )?;
973 let request = HttpRequest::post(vec![("SAMLResponse".into(), ctx.context)]);
974 let parsed =
975 sp.parse_login_response_with_request_id(&idp, Binding::Post, &request, "_r1")?;
976 assert_eq!(
977 parsed.extract.get_str("attributes.mail"),
978 Some("alice@example.com")
979 );
980 Ok(())
981 }
982
983 #[test]
984 fn login_response_escapes_attribute_xml_markup() -> Result<(), Box<dyn std::error::Error>> {
985 use crate::binding::base64_decode;
986 use crate::template::{LoginResponseAttribute, LoginResponseTemplate};
987
988 let mut setting = signing_setting();
989 setting.login_response_template = Some(LoginResponseTemplate {
990 context: None,
991 attributes: vec![LoginResponseAttribute {
992 name: "mail".into(),
993 name_format: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic".into(),
994 value_xsi_type: "xs:string".into(),
995 value_tag: "email".into(),
996 value_xmlns_xs: None,
997 value_xmlns_xsi: None,
998 }],
999 });
1000 let idp = IdentityProvider::from_config(
1001 &IdpMetadataConfig {
1002 entity_id: "https://idp.example.com/metadata".into(),
1003 signing_certs: vec![CERT.into()],
1004 want_authn_requests_signed: true,
1005 single_sign_on_service: vec![Endpoint::new(Binding::Post, "https://idp/sso")],
1006 ..Default::default()
1007 },
1008 setting,
1009 )?;
1010 let sp = sp()?;
1011 let injection = "alpha</saml:AttributeValue><saml:AttributeValue xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"xs:string\">omega";
1012 let user = User {
1013 name_id: "alice@example.com".into(),
1014 attributes: vec![("email".into(), injection.into())],
1015 session_index: None,
1016 };
1017 let ctx = idp.create_login_response(
1018 &sp,
1019 Binding::Post,
1020 &user,
1021 &LoginResponseOptions {
1022 in_response_to: Some("_r1"),
1023 ..Default::default()
1024 },
1025 )?;
1026
1027 let xml = String::from_utf8(base64_decode(&ctx.context)?)?;
1028 assert!(xml.contains("<ds:Signature"));
1029 assert!(xml.contains("alpha</saml:AttributeValue>"));
1030 assert!(xml.contains("<saml:AttributeValue xmlns:xs=""));
1031 assert!(!xml.contains("alpha</saml:AttributeValue><saml:AttributeValue"));
1032 assert_eq!(xml.matches("<saml:AttributeValue ").count(), 1);
1033
1034 let request = HttpRequest::post(vec![("SAMLResponse".into(), ctx.context)]);
1035 let parsed =
1036 sp.parse_login_response_with_request_id(&idp, Binding::Post, &request, "_r1")?;
1037 assert_eq!(parsed.extract.get_str("attributes.mail"), Some(injection));
1038 Ok(())
1039 }
1040
1041 #[test]
1042 fn parse_signed_login_request() -> Result<(), Box<dyn std::error::Error>> {
1043 use crate::binding::base64_decode;
1044 let (idp, sp) = (idp()?, sp()?);
1045 let ctx = sp.create_login_request(&idp, Binding::Post, None)?;
1046 let request = HttpRequest::post(vec![("SAMLRequest".into(), ctx.context.clone())]);
1047 let result = idp.parse_login_request(&sp, Binding::Post, &request)?;
1048 let signed_xml = String::from_utf8(base64_decode(&ctx.context)?)?;
1049 assert!(signed_xml.contains("<ds:Signature"));
1050 assert_eq!(result.extract.get_str("request.id"), Some(ctx.id.as_str()));
1051 Ok(())
1052 }
1053
1054 #[test]
1055 fn parse_signed_login_request_rejects_unexpected_sp_issuer(
1056 ) -> Result<(), Box<dyn std::error::Error>> {
1057 let idp = idp()?;
1058 let expected_sp = sp()?;
1059 let attacker_sp = signed_sp("https://attacker-sp.example.com/metadata")?;
1060 let ctx = attacker_sp.create_login_request(&idp, Binding::Post, None)?;
1061 let request = HttpRequest::post(vec![("SAMLRequest".into(), ctx.context)]);
1062
1063 let result = idp.parse_login_request(&expected_sp, Binding::Post, &request);
1064
1065 assert!(matches!(result, Err(SamlError::IssuerMismatch { .. })));
1066 Ok(())
1067 }
1068}