1use crate::constants::{status_code, Binding, ParserType};
4use crate::entity::{
5 generate_id, iso8601_offset, now_iso8601, 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}
43
44#[derive(Debug, Clone)]
46pub struct IdentityProvider {
47 pub setting: EntitySetting,
49 pub metadata: IdpMetadata,
51}
52
53impl IdentityProvider {
54 pub fn from_metadata(xml: &str, mut setting: EntitySetting) -> Result<Self, SamlError> {
62 let metadata = IdpMetadata::from_xml(xml)?;
63 setting.want_authn_requests_signed = metadata.is_want_authn_requests_signed();
64 let formats = metadata.get_name_id_format();
65 if !formats.is_empty() {
66 setting.name_id_format = formats;
67 }
68 if setting.entity_id.is_none() {
69 setting.entity_id = metadata.get_entity_id().map(str::to_string);
70 }
71 Ok(Self { setting, metadata })
72 }
73
74 pub fn from_config(
82 config: &IdpMetadataConfig,
83 setting: EntitySetting,
84 ) -> Result<Self, SamlError> {
85 let metadata_xml = try_generate_idp_metadata(config)?;
86 Self::from_metadata(&metadata_xml, setting)
87 }
88
89 pub fn metadata_xml(&self) -> &str {
91 self.metadata.get_metadata()
92 }
93
94 fn entity_id(&self) -> String {
95 self.setting
96 .entity_id
97 .clone()
98 .or_else(|| self.metadata.get_entity_id().map(str::to_string))
99 .unwrap_or_default()
100 }
101
102 fn render_login_response(
113 &self,
114 sp: &ServiceProvider,
115 in_response_to: Option<&str>,
116 user: &User,
117 acs: &str,
118 name_id_format: Option<&str>,
119 custom: Option<CustomTagReplacement<'_>>,
120 ) -> Result<(String, String), SamlError> {
121 validate_tag_prefix("protocol", &self.setting.tag_prefix_protocol)?;
122 validate_tag_prefix("assertion", &self.setting.tag_prefix_assertion)?;
123 let tmpl = self.setting.login_response_template.as_ref();
124 let attributes = tmpl.map(|t| t.attributes.as_slice()).unwrap_or(&[]);
125 let has_custom_context = tmpl.and_then(|t| t.context.as_ref()).is_some();
126 if custom.is_none() && !has_custom_context {
127 let now = now_iso8601();
128 let later = iso8601_offset(300);
129 let default_name_id_format = self
130 .setting
131 .name_id_format
132 .first()
133 .cloned()
134 .unwrap_or_default();
135 let name_id_format = name_id_format.unwrap_or(default_name_id_format.as_str());
136 let id = generate_id();
137 let assertion_id = generate_id();
138 let audience = sp.metadata.get_entity_id().unwrap_or_default().to_string();
139 let issuer = self.entity_id();
140 let in_response_to = in_response_to.unwrap_or_default();
141 let xml = render_default_login_response(&LoginResponseXml {
142 protocol_prefix: &self.setting.tag_prefix_protocol,
143 assertion_prefix: &self.setting.tag_prefix_assertion,
144 response_id: &id,
145 assertion_id: &assertion_id,
146 issue_instant: &now,
147 destination: acs,
148 subject_recipient: acs,
149 issuer: &issuer,
150 status_code: status_code::SUCCESS,
151 subject_confirmation_not_on_or_after: &later,
152 conditions_not_before: &now,
153 conditions_not_on_or_after: &later,
154 audience: &audience,
155 name_id_format,
156 name_id: &user.name_id,
157 in_response_to,
158 attributes,
159 user_attributes: &user.attributes,
160 })?;
161 return Ok((id, xml));
162 }
163
164 let base = tmpl
165 .and_then(|t| t.context.as_deref())
166 .unwrap_or(LOGIN_RESPONSE_TEMPLATE);
167 let attribute_statement = if attributes.is_empty() {
168 String::new()
169 } else {
170 attribute_statement_builder(
171 attributes,
172 ATTRIBUTE_TEMPLATE,
173 ATTRIBUTE_STATEMENT_TEMPLATE,
174 )
175 };
176 let prepared = base.replacen("{AttributeStatement}", &attribute_statement, 1);
177 let prepared = apply_tag_prefixes(
178 &prepared,
179 &self.setting.tag_prefix_protocol,
180 &self.setting.tag_prefix_assertion,
181 );
182 if let Some(f) = custom {
183 return Ok(f(&prepared));
184 }
185 let now = now_iso8601();
186 let later = iso8601_offset(300);
187 let default_name_id_format = self
188 .setting
189 .name_id_format
190 .first()
191 .cloned()
192 .unwrap_or_default();
193 let name_id_format = name_id_format.unwrap_or(default_name_id_format.as_str());
194 let id = generate_id();
195 let mut tags: Vec<(&str, String)> = vec![
196 ("ID", id.clone()),
197 ("AssertionID", generate_id()),
198 ("Destination", acs.to_string()),
199 ("SubjectRecipient", acs.to_string()),
200 ("AssertionConsumerServiceURL", acs.to_string()),
201 (
202 "Audience",
203 sp.metadata.get_entity_id().unwrap_or_default().to_string(),
204 ),
205 ("Issuer", self.entity_id()),
206 ("IssueInstant", now.clone()),
207 ("StatusCode", status_code::SUCCESS.to_string()),
208 ("ConditionsNotBefore", now),
209 ("ConditionsNotOnOrAfter", later.clone()),
210 ("SubjectConfirmationDataNotOnOrAfter", later),
211 ("NameIDFormat", name_id_format.to_string()),
212 ("NameID", user.name_id.clone()),
213 (
214 "InResponseTo",
215 in_response_to.unwrap_or_default().to_string(),
216 ),
217 ("AuthnStatement", String::new()),
218 ];
219 let attr_pairs: Vec<(String, String)> = user
222 .attributes
223 .iter()
224 .map(|(tag, value)| (attr_tag(tag), value.clone()))
225 .collect();
226 for (key, value) in &attr_pairs {
227 tags.push((key.as_str(), value.clone()));
228 }
229 Ok((id, replace_tags_by_value(&prepared, &tags)))
230 }
231
232 pub fn create_login_response(
249 &self,
250 sp: &ServiceProvider,
251 binding: Binding,
252 user: &User,
253 options: &LoginResponseOptions<'_>,
254 ) -> Result<BindingContext, SamlError> {
255 self.create_login_response_inner(
256 sp,
257 binding,
258 user,
259 options,
260 LoginResponseOverrides::default(),
261 )
262 }
263
264 pub(crate) fn create_login_response_with_overrides(
265 &self,
266 sp: &ServiceProvider,
267 binding: Binding,
268 user: &User,
269 options: &LoginResponseOptions<'_>,
270 overrides: LoginResponseOverrides<'_>,
271 ) -> Result<BindingContext, SamlError> {
272 self.create_login_response_inner(sp, binding, user, options, overrides)
273 }
274
275 fn create_login_response_inner(
276 &self,
277 sp: &ServiceProvider,
278 binding: Binding,
279 user: &User,
280 options: &LoginResponseOptions<'_>,
281 overrides: LoginResponseOverrides<'_>,
282 ) -> Result<BindingContext, SamlError> {
283 if matches!(binding, Binding::Artifact) {
284 return Err(SamlError::UnsupportedBinding {
285 binding: Binding::Artifact,
286 });
287 }
288 let acs = match overrides.acs {
289 Some(acs) => acs.to_string(),
290 None => sp
291 .metadata
292 .get_assertion_consumer_service(binding)
293 .ok_or_else(|| SamlError::MissingMetadata("AssertionConsumerService".into()))?,
294 };
295 let (id, raw) = self.render_login_response(
296 sp,
297 options.in_response_to,
298 user,
299 &acs,
300 overrides.name_id_format,
301 options.custom,
302 )?;
303 let signed = self.finalize_login_response(sp, binding, &raw, options.encrypt_then_sign)?;
304 let relay = options.relay_state.map(str::to_string);
305 let (context, signature, sig_alg) =
306 self.bind_response(binding, &signed, &acs, relay.as_deref())?;
307 Ok(BindingContext {
308 id,
309 context,
310 relay_state: relay,
311 entity_endpoint: acs,
312 binding,
313 request_type: "SAMLResponse",
314 signature,
315 sig_alg,
316 })
317 }
318
319 #[cfg(feature = "crypto-bergshamra")]
321 fn bind_response(
322 &self,
323 binding: Binding,
324 xml: &str,
325 acs: &str,
326 relay_state: Option<&str>,
327 ) -> Result<(String, Option<String>, Option<String>), SamlError> {
328 use crate::binding::{append_signature, base64_encode, build_redirect_octet};
329 use crate::crypto::{construct_message_signature, keys::load_private_key};
330
331 match binding {
332 Binding::Post => Ok((base64_encode(xml.as_bytes()), None, None)),
333 Binding::Redirect => {
334 let sig_alg = &self.setting.request_signature_algorithm;
335 let key = load_private_key(
336 self.setting.private_key.as_deref().unwrap_or_default(),
337 self.setting.private_key_pass.as_deref(),
338 )?;
339 let octet =
340 build_redirect_octet(ParserType::SamlResponse, xml, relay_state, sig_alg)?;
341 let sig = construct_message_signature(&octet, &key, sig_alg)?;
342 Ok((append_signature(acs, &octet, &sig), None, None))
343 }
344 Binding::SimpleSign => {
345 let sig_alg = &self.setting.request_signature_algorithm;
346 let key = load_private_key(
347 self.setting.private_key.as_deref().unwrap_or_default(),
348 self.setting.private_key_pass.as_deref(),
349 )?;
350 let octet = crate::binding::build_simplesign_octet(
351 ParserType::SamlResponse.query_param(),
352 xml,
353 relay_state,
354 sig_alg,
355 );
356 let sig = construct_message_signature(&octet, &key, sig_alg)?;
357 Ok((
358 base64_encode(xml.as_bytes()),
359 Some(sig),
360 Some(sig_alg.clone()),
361 ))
362 }
363 Binding::Artifact => Err(SamlError::UnsupportedBinding {
364 binding: Binding::Artifact,
365 }),
366 }
367 }
368
369 #[cfg(not(feature = "crypto-bergshamra"))]
370 fn bind_response(
371 &self,
372 _binding: Binding,
373 _xml: &str,
374 _acs: &str,
375 _relay_state: Option<&str>,
376 ) -> Result<(String, Option<String>, Option<String>), SamlError> {
377 Err(SamlError::Unsupported(
378 "createLoginResponse requires feature crypto-bergshamra".into(),
379 ))
380 }
381
382 #[cfg(feature = "crypto-bergshamra")]
383 fn finalize_login_response(
384 &self,
385 sp: &ServiceProvider,
386 binding: Binding,
387 raw: &str,
388 _encrypt_then_sign: bool,
389 ) -> Result<String, SamlError> {
390 use crate::crypto::{construct_saml_signature, encrypt_assertion, keys::load_private_key};
391
392 let key_pem = self
393 .setting
394 .private_key
395 .as_deref()
396 .ok_or_else(|| SamlError::MissingKey("private_key".into()))?;
397 let cert = self
398 .setting
399 .signing_cert
400 .as_deref()
401 .ok_or_else(|| SamlError::MissingKey("signing_cert".into()))?;
402 let sig_alg = &self.setting.request_signature_algorithm;
403 let key = load_private_key(key_pem, self.setting.private_key_pass.as_deref())?;
404
405 let want_assertions_signed = sp.metadata.is_want_assertions_signed();
406 let sign_message =
409 binding == Binding::Post && (sp.setting.want_message_signed || !want_assertions_signed);
410 let mut xml = raw.to_string();
411
412 if want_assertions_signed {
414 xml = construct_saml_signature(
415 &xml,
416 false,
417 &key,
418 cert,
419 sig_alg,
420 &sp.setting.transformation_algorithms,
421 None,
422 )?;
423 }
424 if sign_message && !self.setting.is_assertion_encrypted {
428 xml = construct_saml_signature(
429 &xml,
430 true,
431 &key,
432 cert,
433 sig_alg,
434 &sp.setting.transformation_algorithms,
435 self.setting.signature_config.as_ref(),
436 )?;
437 }
438 if self.setting.is_assertion_encrypted {
439 let encrypt_cert = sp
440 .metadata
441 .get_x509_certificate(crate::constants::CertUse::Encryption)
442 .ok_or_else(|| SamlError::MissingMetadata("encryption certificate".into()))?;
443 xml = encrypt_assertion(
444 &xml,
445 &encrypt_cert,
446 &self.setting.data_encryption_algorithm,
447 &self.setting.key_encryption_algorithm,
448 &self.setting.tag_prefix_encrypted_assertion,
449 )?;
450 }
451 if sign_message && self.setting.is_assertion_encrypted {
452 xml = construct_saml_signature(
453 &xml,
454 true,
455 &key,
456 cert,
457 sig_alg,
458 &sp.setting.transformation_algorithms,
459 self.setting.signature_config.as_ref(),
460 )?;
461 }
462 Ok(xml)
463 }
464
465 #[cfg(not(feature = "crypto-bergshamra"))]
466 fn finalize_login_response(
467 &self,
468 _sp: &ServiceProvider,
469 _binding: Binding,
470 _raw: &str,
471 _encrypt_then_sign: bool,
472 ) -> Result<String, SamlError> {
473 Err(SamlError::Unsupported(
474 "createLoginResponse requires feature crypto-bergshamra".into(),
475 ))
476 }
477
478 pub fn parse_login_request(
491 &self,
492 sp: &ServiceProvider,
493 binding: Binding,
494 request: &HttpRequest,
495 ) -> Result<FlowResult, SamlError> {
496 self.parse_login_request_at_inner(sp, binding, request, None, self.setting.clock_drifts)
497 }
498
499 pub(crate) fn parse_login_request_at(
500 &self,
501 sp: &ServiceProvider,
502 binding: Binding,
503 request: &HttpRequest,
504 now: SystemTime,
505 clock_drifts: (i64, i64),
506 ) -> Result<FlowResult, SamlError> {
507 self.parse_login_request_at_inner(sp, binding, request, Some(now), clock_drifts)
508 }
509
510 fn parse_login_request_at_inner(
511 &self,
512 sp: &ServiceProvider,
513 binding: Binding,
514 request: &HttpRequest,
515 now: Option<SystemTime>,
516 clock_drifts: (i64, i64),
517 ) -> Result<FlowResult, SamlError> {
518 let signing_certs = sp
519 .metadata
520 .x509_certificates(crate::constants::CertUse::Signing);
521 flow(
522 &FlowOptions {
523 binding: Some(binding),
524 parser_type: Some(ParserType::SamlRequest),
525 check_signature: self.metadata.is_want_authn_requests_signed(),
526 from_issuer: sp.metadata.get_entity_id(),
527 signing_certs: &signing_certs,
528 decrypt_key: None,
529 decrypt_key_pass: None,
530 allow_insecure_software_rsa_key_transport_decryption: false,
531 clock_drifts,
532 now,
533 redirect_inflate_max_bytes: self.setting.redirect_inflate_max_bytes,
534 xml_limits: self.setting.xml_limits,
535 expected_audience: None,
536 expected_in_response_to: None,
537 },
538 request,
539 )
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546 use crate::constants::Binding;
547 use crate::metadata::{Endpoint, SpMetadataConfig};
548
549 const IDPMETA: &str = include_str!("../tests/fixtures/idpmeta.xml");
550
551 fn unsigned_idp() -> Result<IdentityProvider, SamlError> {
552 IdentityProvider::from_config(
553 &IdpMetadataConfig {
554 entity_id: "https://idp.example.com/metadata".into(),
555 single_sign_on_service: vec![Endpoint::new(Binding::Post, "https://idp/sso")],
556 ..Default::default()
557 },
558 EntitySetting::default(),
559 )
560 }
561
562 fn unsigned_sp(entity_id: &str) -> Result<ServiceProvider, SamlError> {
563 ServiceProvider::from_config(
564 &SpMetadataConfig {
565 entity_id: entity_id.into(),
566 assertion_consumer_service: vec![Endpoint::new(Binding::Post, "https://sp/acs")],
567 ..Default::default()
568 },
569 EntitySetting::default(),
570 )
571 }
572
573 #[test]
574 fn from_metadata_merges_flags() -> Result<(), Box<dyn std::error::Error>> {
575 let idp = IdentityProvider::from_metadata(IDPMETA, EntitySetting::default())?;
576 assert!(idp.setting.want_authn_requests_signed);
577 assert_eq!(
578 idp.metadata
579 .get_single_sign_on_service(Binding::Redirect)
580 .as_deref(),
581 Some("https://idp.example.org/sso/SingleSignOnService")
582 );
583 Ok(())
584 }
585
586 #[test]
587 fn idp_from_config_rejects_missing_sso() {
588 let cfg = IdpMetadataConfig {
589 entity_id: "https://idp.example.com/metadata".into(),
590 ..Default::default()
591 };
592
593 let result = IdentityProvider::from_config(&cfg, EntitySetting::default());
594
595 assert!(matches!(
596 result,
597 Err(SamlError::MissingMetadata(name)) if name == "SingleSignOnService"
598 ));
599 }
600
601 #[test]
602 fn parse_login_request_accepts_matching_sp_issuer() -> Result<(), Box<dyn std::error::Error>> {
603 let idp = unsigned_idp()?;
604 let sp = unsigned_sp("https://sp.example.com/metadata")?;
605 let ctx = sp.create_login_request(&idp, Binding::Post, None)?;
606 let request = HttpRequest::post(vec![("SAMLRequest".into(), ctx.context)]);
607
608 let result = idp.parse_login_request(&sp, Binding::Post, &request)?;
609
610 assert_eq!(
611 result.extract.get_str("issuer"),
612 Some("https://sp.example.com/metadata")
613 );
614 Ok(())
615 }
616
617 #[test]
618 fn parse_login_request_rejects_unexpected_sp_issuer() -> Result<(), Box<dyn std::error::Error>>
619 {
620 let idp = unsigned_idp()?;
621 let expected_sp = unsigned_sp("https://expected-sp.example.com/metadata")?;
622 let attacker_sp = unsigned_sp("https://attacker-sp.example.com/metadata")?;
623 let ctx = attacker_sp.create_login_request(&idp, Binding::Post, None)?;
624 let request = HttpRequest::post(vec![("SAMLRequest".into(), ctx.context)]);
625
626 let result = idp.parse_login_request(&expected_sp, Binding::Post, &request);
627
628 assert!(matches!(result, Err(SamlError::IssuerMismatch { .. })));
629 Ok(())
630 }
631}
632
633#[cfg(all(test, feature = "crypto-bergshamra"))]
634mod crypto_tests {
635 use super::*;
636 use crate::constants::signature_algorithm::RSA_SHA256;
637 use crate::metadata::{Endpoint, SpMetadataConfig};
638
639 const PRIVKEY: &str = include_str!("../tests/fixtures/key/sp_privkey.pem");
641 const CERT: &str = include_str!("../tests/fixtures/key/sp_signing_cert.cer");
642
643 fn signing_setting() -> EntitySetting {
644 EntitySetting {
645 private_key: Some(PRIVKEY.into()),
646 signing_cert: Some(CERT.into()),
647 request_signature_algorithm: RSA_SHA256.into(),
648 ..Default::default()
649 }
650 }
651
652 fn idp() -> Result<IdentityProvider, SamlError> {
653 IdentityProvider::from_config(
654 &IdpMetadataConfig {
655 entity_id: "https://idp.example.com/metadata".into(),
656 signing_certs: vec![CERT.into()],
657 want_authn_requests_signed: true,
658 single_sign_on_service: vec![Endpoint::new(Binding::Post, "https://idp/sso")],
659 ..Default::default()
660 },
661 signing_setting(),
662 )
663 }
664
665 fn signed_sp(entity_id: &str) -> Result<ServiceProvider, SamlError> {
666 ServiceProvider::from_config(
667 &SpMetadataConfig {
668 entity_id: entity_id.into(),
669 authn_requests_signed: true,
670 want_assertions_signed: true,
671 signing_certs: vec![CERT.into()],
672 assertion_consumer_service: vec![Endpoint::new(Binding::Post, "https://sp/acs")],
673 ..Default::default()
674 },
675 signing_setting(),
676 )
677 }
678
679 fn sp() -> Result<ServiceProvider, SamlError> {
680 signed_sp("https://sp.example.com/metadata")
681 }
682
683 #[test]
684 fn idp_response_consumed_by_sp() -> Result<(), Box<dyn std::error::Error>> {
685 let (idp, sp) = (idp()?, sp()?);
686 let ctx = idp.create_login_response(
687 &sp,
688 Binding::Post,
689 &User::new("user@example.com"),
690 &LoginResponseOptions {
691 in_response_to: Some("_req123"),
692 ..Default::default()
693 },
694 )?;
695 let request = HttpRequest::post(vec![("SAMLResponse".into(), ctx.context)]);
696 let result =
697 sp.parse_login_response_with_request_id(&idp, Binding::Post, &request, "_req123")?;
698 assert_eq!(result.extract.get_str("nameID"), Some("user@example.com"));
699 assert_eq!(
700 result.extract.get_str("issuer"),
701 Some("https://idp.example.com/metadata")
702 );
703 Ok(())
704 }
705
706 #[test]
707 fn login_response_with_attributes() -> Result<(), Box<dyn std::error::Error>> {
708 use crate::template::{LoginResponseAttribute, LoginResponseTemplate};
709 let mut setting = signing_setting();
710 setting.login_response_template = Some(LoginResponseTemplate {
711 context: None,
712 attributes: vec![LoginResponseAttribute {
713 name: "mail".into(),
714 name_format: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic".into(),
715 value_xsi_type: "xs:string".into(),
716 value_tag: "email".into(),
717 value_xmlns_xs: None,
718 value_xmlns_xsi: None,
719 }],
720 });
721 let idp = IdentityProvider::from_config(
722 &IdpMetadataConfig {
723 entity_id: "https://idp.example.com/metadata".into(),
724 signing_certs: vec![CERT.into()],
725 want_authn_requests_signed: true,
726 single_sign_on_service: vec![Endpoint::new(Binding::Post, "https://idp/sso")],
727 ..Default::default()
728 },
729 setting,
730 )?;
731 let sp = sp()?;
732 let user = User {
733 name_id: "alice@example.com".into(),
734 attributes: vec![("email".into(), "alice@example.com".into())],
735 session_index: None,
736 };
737 let ctx = idp.create_login_response(
738 &sp,
739 Binding::Post,
740 &user,
741 &LoginResponseOptions {
742 in_response_to: Some("_r1"),
743 ..Default::default()
744 },
745 )?;
746 let request = HttpRequest::post(vec![("SAMLResponse".into(), ctx.context)]);
747 let parsed =
748 sp.parse_login_response_with_request_id(&idp, Binding::Post, &request, "_r1")?;
749 assert_eq!(
750 parsed.extract.get_str("attributes.mail"),
751 Some("alice@example.com")
752 );
753 Ok(())
754 }
755
756 #[test]
757 fn login_response_escapes_attribute_xml_markup() -> Result<(), Box<dyn std::error::Error>> {
758 use crate::binding::base64_decode;
759 use crate::template::{LoginResponseAttribute, LoginResponseTemplate};
760
761 let mut setting = signing_setting();
762 setting.login_response_template = Some(LoginResponseTemplate {
763 context: None,
764 attributes: vec![LoginResponseAttribute {
765 name: "mail".into(),
766 name_format: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic".into(),
767 value_xsi_type: "xs:string".into(),
768 value_tag: "email".into(),
769 value_xmlns_xs: None,
770 value_xmlns_xsi: None,
771 }],
772 });
773 let idp = IdentityProvider::from_config(
774 &IdpMetadataConfig {
775 entity_id: "https://idp.example.com/metadata".into(),
776 signing_certs: vec![CERT.into()],
777 want_authn_requests_signed: true,
778 single_sign_on_service: vec![Endpoint::new(Binding::Post, "https://idp/sso")],
779 ..Default::default()
780 },
781 setting,
782 )?;
783 let sp = sp()?;
784 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";
785 let user = User {
786 name_id: "alice@example.com".into(),
787 attributes: vec![("email".into(), injection.into())],
788 session_index: None,
789 };
790 let ctx = idp.create_login_response(
791 &sp,
792 Binding::Post,
793 &user,
794 &LoginResponseOptions {
795 in_response_to: Some("_r1"),
796 ..Default::default()
797 },
798 )?;
799
800 let xml = String::from_utf8(base64_decode(&ctx.context)?)?;
801 assert!(xml.contains("<ds:Signature"));
802 assert!(xml.contains("alpha</saml:AttributeValue>"));
803 assert!(xml.contains("<saml:AttributeValue xmlns:xs=""));
804 assert!(!xml.contains("alpha</saml:AttributeValue><saml:AttributeValue"));
805 assert_eq!(xml.matches("<saml:AttributeValue ").count(), 1);
806
807 let request = HttpRequest::post(vec![("SAMLResponse".into(), ctx.context)]);
808 let parsed =
809 sp.parse_login_response_with_request_id(&idp, Binding::Post, &request, "_r1")?;
810 assert_eq!(parsed.extract.get_str("attributes.mail"), Some(injection));
811 Ok(())
812 }
813
814 #[test]
815 fn parse_signed_login_request() -> Result<(), Box<dyn std::error::Error>> {
816 use crate::binding::base64_decode;
817 let (idp, sp) = (idp()?, sp()?);
818 let ctx = sp.create_login_request(&idp, Binding::Post, None)?;
819 let request = HttpRequest::post(vec![("SAMLRequest".into(), ctx.context.clone())]);
820 let result = idp.parse_login_request(&sp, Binding::Post, &request)?;
821 let signed_xml = String::from_utf8(base64_decode(&ctx.context)?)?;
822 assert!(signed_xml.contains("<ds:Signature"));
823 assert_eq!(result.extract.get_str("request.id"), Some(ctx.id.as_str()));
824 Ok(())
825 }
826
827 #[test]
828 fn parse_signed_login_request_rejects_unexpected_sp_issuer(
829 ) -> Result<(), Box<dyn std::error::Error>> {
830 let idp = idp()?;
831 let expected_sp = sp()?;
832 let attacker_sp = signed_sp("https://attacker-sp.example.com/metadata")?;
833 let ctx = attacker_sp.create_login_request(&idp, Binding::Post, None)?;
834 let request = HttpRequest::post(vec![("SAMLRequest".into(), ctx.context)]);
835
836 let result = idp.parse_login_request(&expected_sp, Binding::Post, &request);
837
838 assert!(matches!(result, Err(SamlError::IssuerMismatch { .. })));
839 Ok(())
840 }
841}