1use crate::errors::{AuthError, Result};
6use chrono::{DateTime, Duration, Utc};
7use serde::{Deserialize, Serialize};
8#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct SamlAssertion {
14 pub id: String,
16
17 pub issuer: String,
19
20 pub issue_instant: DateTime<Utc>,
22
23 pub version: String,
25
26 pub subject: Option<SamlSubject>,
28
29 pub conditions: Option<SamlConditions>,
31
32 pub attribute_statements: Vec<SamlAttributeStatement>,
34
35 pub authn_statements: Vec<SamlAuthnStatement>,
37
38 pub authz_decision_statements: Vec<SamlAuthzDecisionStatement>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct SamlSubject {
45 pub name_id: Option<SamlNameId>,
47
48 pub subject_confirmations: Vec<SamlSubjectConfirmation>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct SamlNameId {
55 pub value: String,
57
58 pub format: Option<String>,
60
61 pub name_qualifier: Option<String>,
63
64 pub sp_name_qualifier: Option<String>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct SamlSubjectConfirmation {
71 pub method: String,
73
74 pub subject_confirmation_data: Option<SamlSubjectConfirmationData>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct SamlSubjectConfirmationData {
81 pub not_before: Option<DateTime<Utc>>,
83
84 pub not_on_or_after: Option<DateTime<Utc>>,
86
87 pub recipient: Option<String>,
89
90 pub in_response_to: Option<String>,
92
93 pub address: Option<String>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct SamlConditions {
100 pub not_before: Option<DateTime<Utc>>,
102
103 pub not_on_or_after: Option<DateTime<Utc>>,
105
106 pub audience_restrictions: Vec<SamlAudienceRestriction>,
108
109 pub one_time_use: bool,
111
112 pub proxy_restriction: Option<SamlProxyRestriction>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct SamlAudienceRestriction {
119 pub audiences: Vec<String>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct SamlProxyRestriction {
126 pub count: Option<u32>,
128
129 pub audiences: Vec<String>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct SamlAttributeStatement {
136 pub attributes: Vec<SamlAttribute>,
138
139 pub encrypted_attributes: Vec<String>, }
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct SamlAttribute {
146 pub name: String,
148
149 pub name_format: Option<String>,
151
152 pub friendly_name: Option<String>,
154
155 pub values: Vec<SamlAttributeValue>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct SamlAttributeValue {
162 pub value: String,
164
165 pub type_info: Option<String>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct SamlAuthnStatement {
172 pub authn_instant: DateTime<Utc>,
174
175 pub session_index: Option<String>,
177
178 pub session_not_on_or_after: Option<DateTime<Utc>>,
180
181 pub authn_context: SamlAuthnContext,
183
184 pub subject_locality: Option<SamlSubjectLocality>,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct SamlAuthnContext {
191 pub authn_context_class_ref: Option<String>,
193
194 pub authn_context_decl: Option<String>,
196
197 pub authn_context_decl_ref: Option<String>,
199
200 pub authenticating_authorities: Vec<String>,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct SamlSubjectLocality {
207 pub address: Option<String>,
209
210 pub dns_name: Option<String>,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct SamlAuthzDecisionStatement {
217 pub resource: String,
219
220 pub decision: SamlDecision,
222
223 pub actions: Vec<SamlAction>,
225
226 pub evidence: Option<SamlEvidence>,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
232pub enum SamlDecision {
233 Permit,
235
236 Deny,
238
239 Indeterminate,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct SamlAction {
246 pub value: String,
248
249 pub namespace: Option<String>,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct SamlEvidence {
256 pub assertions: Vec<String>, pub assertion_id_refs: Vec<String>,
261
262 pub assertion_uri_refs: Vec<String>,
264}
265
266pub struct SamlAssertionBuilder {
268 assertion: SamlAssertion,
270}
271
272pub struct SamlAssertionValidator {
274 clock_skew: Duration,
276
277 trusted_issuers: Vec<String>,
279
280 expected_audiences: Vec<String>,
282}
283
284impl SamlAssertionBuilder {
285 pub fn new(issuer: &str) -> Self {
287 let assertion = SamlAssertion {
288 id: format!("_{}", uuid::Uuid::new_v4()),
289 issuer: issuer.to_string(),
290 issue_instant: Utc::now(),
291 version: "2.0".to_string(),
292 subject: None,
293 conditions: None,
294 attribute_statements: Vec::new(),
295 authn_statements: Vec::new(),
296 authz_decision_statements: Vec::new(),
297 };
298
299 Self { assertion }
300 }
301
302 pub fn with_subject(mut self, subject: SamlSubject) -> Self {
304 self.assertion.subject = Some(subject);
305 self
306 }
307
308 pub fn with_conditions(mut self, conditions: SamlConditions) -> Self {
310 self.assertion.conditions = Some(conditions);
311 self
312 }
313
314 pub fn with_attribute_statement(mut self, statement: SamlAttributeStatement) -> Self {
316 self.assertion.attribute_statements.push(statement);
317 self
318 }
319
320 pub fn with_authn_statement(mut self, statement: SamlAuthnStatement) -> Self {
322 self.assertion.authn_statements.push(statement);
323 self
324 }
325
326 pub fn with_authz_decision_statement(mut self, statement: SamlAuthzDecisionStatement) -> Self {
328 self.assertion.authz_decision_statements.push(statement);
329 self
330 }
331
332 pub fn with_attribute(mut self, name: &str, value: &str) -> Self {
334 let attribute = SamlAttribute {
335 name: name.to_string(),
336 name_format: Some("urn:oasis:names:tc:SAML:2.0:attrname-format:basic".to_string()),
337 friendly_name: None,
338 values: vec![SamlAttributeValue {
339 value: value.to_string(),
340 type_info: None,
341 }],
342 };
343
344 if self.assertion.attribute_statements.is_empty() {
346 self.assertion
347 .attribute_statements
348 .push(SamlAttributeStatement {
349 attributes: vec![attribute],
350 encrypted_attributes: Vec::new(),
351 });
352 } else {
353 self.assertion.attribute_statements[0]
354 .attributes
355 .push(attribute);
356 }
357
358 self
359 }
360
361 pub fn with_validity_period(
363 mut self,
364 not_before: DateTime<Utc>,
365 not_on_or_after: DateTime<Utc>,
366 ) -> Self {
367 if let Some(ref mut conditions) = self.assertion.conditions {
368 conditions.not_before = Some(not_before);
370 conditions.not_on_or_after = Some(not_on_or_after);
371 } else {
372 let conditions = SamlConditions {
374 not_before: Some(not_before),
375 not_on_or_after: Some(not_on_or_after),
376 audience_restrictions: Vec::new(),
377 one_time_use: false,
378 proxy_restriction: None,
379 };
380 self.assertion.conditions = Some(conditions);
381 }
382 self
383 }
384
385 pub fn with_audience(mut self, audience: &str) -> Self {
387 if let Some(ref mut conditions) = self.assertion.conditions {
388 if conditions.audience_restrictions.is_empty() {
389 conditions
390 .audience_restrictions
391 .push(SamlAudienceRestriction {
392 audiences: vec![audience.to_string()],
393 });
394 } else {
395 conditions.audience_restrictions[0]
396 .audiences
397 .push(audience.to_string());
398 }
399 } else {
400 let conditions = SamlConditions {
401 not_before: None,
402 not_on_or_after: None,
403 audience_restrictions: vec![SamlAudienceRestriction {
404 audiences: vec![audience.to_string()],
405 }],
406 one_time_use: false,
407 proxy_restriction: None,
408 };
409 self.assertion.conditions = Some(conditions);
410 }
411
412 self
413 }
414
415 pub fn build(self) -> SamlAssertion {
417 self.assertion
418 }
419
420 pub fn build_xml(self) -> Result<String> {
422 let assertion = self.assertion;
423 assertion.to_xml()
424 }
425}
426
427impl SamlAssertionValidator {
428 pub fn new() -> Self {
430 Self {
431 clock_skew: Duration::minutes(5),
432 trusted_issuers: Vec::new(),
433 expected_audiences: Vec::new(),
434 }
435 }
436
437 pub fn with_clock_skew(mut self, skew: Duration) -> Self {
439 self.clock_skew = skew;
440 self
441 }
442
443 pub fn with_trusted_issuer(mut self, issuer: &str) -> Self {
445 self.trusted_issuers.push(issuer.to_string());
446 self
447 }
448
449 pub fn with_expected_audience(mut self, audience: &str) -> Self {
451 self.expected_audiences.push(audience.to_string());
452 self
453 }
454
455 pub fn validate(&self, assertion: &SamlAssertion) -> Result<()> {
457 if !self.trusted_issuers.is_empty() && !self.trusted_issuers.contains(&assertion.issuer) {
459 return Err(AuthError::auth_method("saml", "Untrusted issuer"));
460 }
461
462 self.validate_timing(assertion)?;
464
465 self.validate_audience(assertion)?;
467
468 if let Some(ref subject) = assertion.subject {
470 self.validate_subject_confirmation(subject)?;
471 }
472
473 Ok(())
474 }
475
476 fn validate_timing(&self, assertion: &SamlAssertion) -> Result<()> {
478 let now = Utc::now();
479
480 if assertion.issue_instant > now + self.clock_skew {
482 return Err(AuthError::auth_method(
483 "saml",
484 "Assertion issued in the future",
485 ));
486 }
487
488 if let Some(ref conditions) = assertion.conditions {
490 if let Some(not_before) = conditions.not_before
491 && now < not_before - self.clock_skew
492 {
493 return Err(AuthError::auth_method("saml", "Assertion not yet valid"));
494 }
495
496 if let Some(not_on_or_after) = conditions.not_on_or_after
497 && now >= not_on_or_after + self.clock_skew
498 {
499 return Err(AuthError::auth_method("saml", "Assertion has expired"));
500 }
501 }
502
503 Ok(())
504 }
505
506 fn validate_audience(&self, assertion: &SamlAssertion) -> Result<()> {
508 if self.expected_audiences.is_empty() {
509 return Ok(());
510 }
511
512 if let Some(ref conditions) = assertion.conditions {
513 for restriction in &conditions.audience_restrictions {
514 for audience in &restriction.audiences {
515 if self.expected_audiences.contains(audience) {
516 return Ok(());
517 }
518 }
519 }
520
521 if !conditions.audience_restrictions.is_empty() {
522 return Err(AuthError::auth_method("saml", "No matching audience found"));
523 }
524 }
525
526 Ok(())
527 }
528
529 fn validate_subject_confirmation(&self, _subject: &SamlSubject) -> Result<()> {
531 Ok(())
533 }
534}
535
536impl SamlAssertion {
537 pub fn to_xml(&self) -> Result<String> {
539 let mut xml = String::new();
540
541 xml.push_str(&format!(
542 r#"<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{}" IssueInstant="{}" Version="{}">"#,
543 self.id,
544 self.issue_instant.format("%Y-%m-%dT%H:%M:%S%.3fZ"),
545 self.version
546 ));
547
548 xml.push_str(&format!("<saml:Issuer>{}</saml:Issuer>", self.issuer));
550
551 if let Some(ref subject) = self.subject {
553 xml.push_str(&subject.to_xml()?);
554 }
555
556 if let Some(ref conditions) = self.conditions {
558 xml.push_str(&conditions.to_xml()?);
559 }
560
561 for statement in &self.attribute_statements {
563 xml.push_str(&statement.to_xml()?);
564 }
565
566 for statement in &self.authn_statements {
568 xml.push_str(&statement.to_xml()?);
569 }
570
571 for statement in &self.authz_decision_statements {
573 xml.push_str(&statement.to_xml()?);
574 }
575
576 xml.push_str("</saml:Assertion>");
577
578 Ok(xml)
579 }
580}
581
582impl SamlSubject {
583 pub fn to_xml(&self) -> Result<String> {
585 let mut xml = String::new();
586
587 xml.push_str("<saml:Subject>");
588
589 if let Some(ref name_id) = self.name_id {
590 xml.push_str(&name_id.to_xml()?);
591 }
592
593 for confirmation in &self.subject_confirmations {
594 xml.push_str(&confirmation.to_xml()?);
595 }
596
597 xml.push_str("</saml:Subject>");
598
599 Ok(xml)
600 }
601}
602
603impl SamlNameId {
604 pub fn to_xml(&self) -> Result<String> {
606 let mut xml = String::new();
607
608 xml.push_str("<saml:NameID");
609
610 if let Some(ref format) = self.format {
611 xml.push_str(&format!(" Format=\"{}\"", format));
612 }
613
614 if let Some(ref name_qualifier) = self.name_qualifier {
615 xml.push_str(&format!(" NameQualifier=\"{}\"", name_qualifier));
616 }
617
618 if let Some(ref sp_name_qualifier) = self.sp_name_qualifier {
619 xml.push_str(&format!(" SPNameQualifier=\"{}\"", sp_name_qualifier));
620 }
621
622 xml.push_str(&format!(">{}</saml:NameID>", self.value));
623
624 Ok(xml)
625 }
626}
627
628impl SamlSubjectConfirmation {
629 pub fn to_xml(&self) -> Result<String> {
631 let mut xml = String::new();
632
633 xml.push_str(&format!(
634 "<saml:SubjectConfirmation Method=\"{}\">",
635 self.method
636 ));
637
638 if let Some(ref data) = self.subject_confirmation_data {
639 xml.push_str(&data.to_xml()?);
640 }
641
642 xml.push_str("</saml:SubjectConfirmation>");
643
644 Ok(xml)
645 }
646}
647
648impl SamlSubjectConfirmationData {
649 pub fn to_xml(&self) -> Result<String> {
651 let mut xml = String::new();
652
653 xml.push_str("<saml:SubjectConfirmationData");
654
655 if let Some(not_before) = self.not_before {
656 xml.push_str(&format!(
657 " NotBefore=\"{}\"",
658 not_before.format("%Y-%m-%dT%H:%M:%S%.3fZ")
659 ));
660 }
661
662 if let Some(not_on_or_after) = self.not_on_or_after {
663 xml.push_str(&format!(
664 " NotOnOrAfter=\"{}\"",
665 not_on_or_after.format("%Y-%m-%dT%H:%M:%S%.3fZ")
666 ));
667 }
668
669 if let Some(ref recipient) = self.recipient {
670 xml.push_str(&format!(" Recipient=\"{}\"", recipient));
671 }
672
673 if let Some(ref in_response_to) = self.in_response_to {
674 xml.push_str(&format!(" InResponseTo=\"{}\"", in_response_to));
675 }
676
677 if let Some(ref address) = self.address {
678 xml.push_str(&format!(" Address=\"{}\"", address));
679 }
680
681 xml.push_str("/>");
682
683 Ok(xml)
684 }
685}
686
687impl SamlConditions {
688 pub fn to_xml(&self) -> Result<String> {
690 let mut xml = String::new();
691
692 xml.push_str("<saml:Conditions");
693
694 if let Some(not_before) = self.not_before {
695 xml.push_str(&format!(
696 " NotBefore=\"{}\"",
697 not_before.format("%Y-%m-%dT%H:%M:%S%.3fZ")
698 ));
699 }
700
701 if let Some(not_on_or_after) = self.not_on_or_after {
702 xml.push_str(&format!(
703 " NotOnOrAfter=\"{}\"",
704 not_on_or_after.format("%Y-%m-%dT%H:%M:%S%.3fZ")
705 ));
706 }
707
708 xml.push('>');
709
710 for restriction in &self.audience_restrictions {
711 xml.push_str(&restriction.to_xml()?);
712 }
713
714 if self.one_time_use {
715 xml.push_str("<saml:OneTimeUse/>");
716 }
717
718 if let Some(ref proxy_restriction) = self.proxy_restriction {
719 xml.push_str(&proxy_restriction.to_xml()?);
720 }
721
722 xml.push_str("</saml:Conditions>");
723
724 Ok(xml)
725 }
726}
727
728impl SamlAudienceRestriction {
729 pub fn to_xml(&self) -> Result<String> {
731 let mut xml = String::new();
732
733 xml.push_str("<saml:AudienceRestriction>");
734
735 for audience in &self.audiences {
736 xml.push_str(&format!("<saml:Audience>{}</saml:Audience>", audience));
737 }
738
739 xml.push_str("</saml:AudienceRestriction>");
740
741 Ok(xml)
742 }
743}
744
745impl SamlProxyRestriction {
746 pub fn to_xml(&self) -> Result<String> {
748 let mut xml = String::new();
749
750 xml.push_str("<saml:ProxyRestriction");
751
752 if let Some(count) = self.count {
753 xml.push_str(&format!(" Count=\"{}\"", count));
754 }
755
756 xml.push('>');
757
758 for audience in &self.audiences {
759 xml.push_str(&format!("<saml:Audience>{}</saml:Audience>", audience));
760 }
761
762 xml.push_str("</saml:ProxyRestriction>");
763
764 Ok(xml)
765 }
766}
767
768impl SamlAttributeStatement {
769 pub fn to_xml(&self) -> Result<String> {
771 let mut xml = String::new();
772
773 xml.push_str("<saml:AttributeStatement>");
774
775 for attribute in &self.attributes {
776 xml.push_str(&attribute.to_xml()?);
777 }
778
779 for encrypted_attr in &self.encrypted_attributes {
781 xml.push_str(encrypted_attr);
782 }
783
784 xml.push_str("</saml:AttributeStatement>");
785
786 Ok(xml)
787 }
788}
789
790impl SamlAttribute {
791 pub fn to_xml(&self) -> Result<String> {
793 let mut xml = String::new();
794
795 xml.push_str(&format!("<saml:Attribute Name=\"{}\">", self.name));
796
797 if let Some(ref name_format) = self.name_format {
798 xml = xml.replace(">", &format!(" NameFormat=\"{}\">", name_format));
799 }
800
801 if let Some(ref friendly_name) = self.friendly_name {
802 xml = xml.replace(">", &format!(" FriendlyName=\"{}\">", friendly_name));
803 }
804
805 for value in &self.values {
806 xml.push_str(&value.to_xml()?);
807 }
808
809 xml.push_str("</saml:Attribute>");
810
811 Ok(xml)
812 }
813}
814
815impl SamlAttributeValue {
816 pub fn to_xml(&self) -> Result<String> {
818 let mut xml = String::new();
819
820 xml.push_str("<saml:AttributeValue");
821
822 if let Some(ref type_info) = self.type_info {
823 xml.push_str(&format!(" xsi:type=\"{}\"", type_info));
824 }
825
826 xml.push_str(&format!(">{}</saml:AttributeValue>", self.value));
827
828 Ok(xml)
829 }
830}
831
832impl SamlAuthnStatement {
833 pub fn to_xml(&self) -> Result<String> {
835 let mut xml = String::new();
836
837 xml.push_str(&format!(
838 "<saml:AuthnStatement AuthnInstant=\"{}\"",
839 self.authn_instant.format("%Y-%m-%dT%H:%M:%S%.3fZ")
840 ));
841
842 if let Some(ref session_index) = self.session_index {
843 xml.push_str(&format!(" SessionIndex=\"{}\"", session_index));
844 }
845
846 if let Some(session_not_on_or_after) = self.session_not_on_or_after {
847 xml.push_str(&format!(
848 " SessionNotOnOrAfter=\"{}\"",
849 session_not_on_or_after.format("%Y-%m-%dT%H:%M:%S%.3fZ")
850 ));
851 }
852
853 xml.push('>');
854
855 if let Some(ref locality) = self.subject_locality {
856 xml.push_str(&locality.to_xml()?);
857 }
858
859 xml.push_str(&self.authn_context.to_xml()?);
860
861 xml.push_str("</saml:AuthnStatement>");
862
863 Ok(xml)
864 }
865}
866
867impl SamlAuthnContext {
868 pub fn to_xml(&self) -> Result<String> {
870 let mut xml = String::new();
871
872 xml.push_str("<saml:AuthnContext>");
873
874 if let Some(ref class_ref) = self.authn_context_class_ref {
875 xml.push_str(&format!(
876 "<saml:AuthnContextClassRef>{}</saml:AuthnContextClassRef>",
877 class_ref
878 ));
879 }
880
881 if let Some(ref decl) = self.authn_context_decl {
882 xml.push_str(&format!(
883 "<saml:AuthnContextDecl>{}</saml:AuthnContextDecl>",
884 decl
885 ));
886 }
887
888 if let Some(ref decl_ref) = self.authn_context_decl_ref {
889 xml.push_str(&format!(
890 "<saml:AuthnContextDeclRef>{}</saml:AuthnContextDeclRef>",
891 decl_ref
892 ));
893 }
894
895 for authority in &self.authenticating_authorities {
896 xml.push_str(&format!(
897 "<saml:AuthenticatingAuthority>{}</saml:AuthenticatingAuthority>",
898 authority
899 ));
900 }
901
902 xml.push_str("</saml:AuthnContext>");
903
904 Ok(xml)
905 }
906}
907
908impl SamlSubjectLocality {
909 pub fn to_xml(&self) -> Result<String> {
911 let mut xml = String::new();
912
913 xml.push_str("<saml:SubjectLocality");
914
915 if let Some(ref address) = self.address {
916 xml.push_str(&format!(" Address=\"{}\"", address));
917 }
918
919 if let Some(ref dns_name) = self.dns_name {
920 xml.push_str(&format!(" DNSName=\"{}\"", dns_name));
921 }
922
923 xml.push_str("/>");
924
925 Ok(xml)
926 }
927}
928
929impl SamlAuthzDecisionStatement {
930 pub fn to_xml(&self) -> Result<String> {
932 let mut xml = String::new();
933
934 let decision_str = match self.decision {
935 SamlDecision::Permit => "Permit",
936 SamlDecision::Deny => "Deny",
937 SamlDecision::Indeterminate => "Indeterminate",
938 };
939
940 xml.push_str(&format!(
941 "<saml:AuthzDecisionStatement Decision=\"{}\" Resource=\"{}\">",
942 decision_str, self.resource
943 ));
944
945 for action in &self.actions {
946 xml.push_str(&action.to_xml()?);
947 }
948
949 if let Some(ref evidence) = self.evidence {
950 xml.push_str(&evidence.to_xml()?);
951 }
952
953 xml.push_str("</saml:AuthzDecisionStatement>");
954
955 Ok(xml)
956 }
957}
958
959impl SamlAction {
960 pub fn to_xml(&self) -> Result<String> {
962 let mut xml = String::new();
963
964 xml.push_str("<saml:Action");
965
966 if let Some(ref namespace) = self.namespace {
967 xml.push_str(&format!(" Namespace=\"{}\"", namespace));
968 }
969
970 xml.push_str(&format!(">{}</saml:Action>", self.value));
971
972 Ok(xml)
973 }
974}
975
976impl SamlEvidence {
977 pub fn to_xml(&self) -> Result<String> {
979 let mut xml = String::new();
980
981 xml.push_str("<saml:Evidence>");
982
983 for assertion in &self.assertions {
984 xml.push_str(assertion);
985 }
986
987 for id_ref in &self.assertion_id_refs {
988 xml.push_str(&format!(
989 "<saml:AssertionIDRef>{}</saml:AssertionIDRef>",
990 id_ref
991 ));
992 }
993
994 for uri_ref in &self.assertion_uri_refs {
995 xml.push_str(&format!(
996 "<saml:AssertionURIRef>{}</saml:AssertionURIRef>",
997 uri_ref
998 ));
999 }
1000
1001 xml.push_str("</saml:Evidence>");
1002
1003 Ok(xml)
1004 }
1005}
1006
1007impl Default for SamlAssertionValidator {
1008 fn default() -> Self {
1009 Self::new()
1010 }
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015 use super::*;
1016
1017 #[test]
1018 fn test_saml_assertion_builder() {
1019 let assertion = SamlAssertionBuilder::new("https://idp.example.com")
1020 .with_attribute("username", "testuser")
1021 .with_attribute("email", "test@example.com")
1022 .with_audience("https://sp.example.com")
1023 .build();
1024
1025 assert_eq!(assertion.issuer, "https://idp.example.com");
1026 assert_eq!(assertion.version, "2.0");
1027 assert!(!assertion.attribute_statements.is_empty());
1028 assert!(assertion.conditions.is_some());
1029 }
1030
1031 #[test]
1032 fn test_saml_assertion_xml() {
1033 let assertion = SamlAssertionBuilder::new("https://idp.example.com")
1034 .with_attribute("username", "testuser")
1035 .build();
1036
1037 let xml = assertion.to_xml().unwrap();
1038 assert!(xml.contains("<saml:Assertion"));
1039 assert!(xml.contains("https://idp.example.com"));
1040 assert!(xml.contains("testuser"));
1041 assert!(xml.contains("</saml:Assertion>"));
1042 }
1043
1044 #[test]
1045 fn test_saml_assertion_validation() {
1046 let validator = SamlAssertionValidator::new()
1047 .with_trusted_issuer("https://idp.example.com")
1048 .with_expected_audience("https://sp.example.com");
1049
1050 let assertion = SamlAssertionBuilder::new("https://idp.example.com")
1051 .with_audience("https://sp.example.com")
1052 .with_validity_period(
1053 Utc::now() - Duration::minutes(1),
1054 Utc::now() + Duration::hours(1),
1055 )
1056 .build();
1057
1058 assert!(validator.validate(&assertion).is_ok());
1059 }
1060
1061 #[test]
1062 fn test_expired_assertion_validation() {
1063 let validator = SamlAssertionValidator::new();
1064
1065 let assertion = SamlAssertionBuilder::new("https://idp.example.com")
1066 .with_validity_period(
1067 Utc::now() - Duration::hours(2),
1068 Utc::now() - Duration::hours(1),
1069 )
1070 .build();
1071
1072 assert!(validator.validate(&assertion).is_err());
1073 }
1074}
1075
1076