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