auth_framework/
saml_assertions.rs

1//! SAML 2.0 Assertion Support for WS-Security
2//!
3//! This module provides SAML 2.0 assertion generation and validation for WS-Security scenarios.
4
5use crate::errors::{AuthError, Result};
6use chrono::{DateTime, Duration, Utc};
7use serde::{Deserialize, Serialize};
8// HashMap removed - not currently used but may be needed later
9// use std::collections::HashMap;
10
11/// SAML 2.0 Assertion
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct SamlAssertion {
14    /// Assertion ID
15    pub id: String,
16
17    /// Issuer of the assertion
18    pub issuer: String,
19
20    /// Issue instant
21    pub issue_instant: DateTime<Utc>,
22
23    /// Version (always "2.0" for SAML 2.0)
24    pub version: String,
25
26    /// Subject information
27    pub subject: Option<SamlSubject>,
28
29    /// Conditions (validity constraints)
30    pub conditions: Option<SamlConditions>,
31
32    /// Attribute statements
33    pub attribute_statements: Vec<SamlAttributeStatement>,
34
35    /// Authentication statements
36    pub authn_statements: Vec<SamlAuthnStatement>,
37
38    /// Authorization decision statements
39    pub authz_decision_statements: Vec<SamlAuthzDecisionStatement>,
40}
41
42/// SAML Subject
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct SamlSubject {
45    /// Name identifier
46    pub name_id: Option<SamlNameId>,
47
48    /// Subject confirmations
49    pub subject_confirmations: Vec<SamlSubjectConfirmation>,
50}
51
52/// SAML Name Identifier
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct SamlNameId {
55    /// Value of the name identifier
56    pub value: String,
57
58    /// Format of the name identifier
59    pub format: Option<String>,
60
61    /// Name qualifier
62    pub name_qualifier: Option<String>,
63
64    /// SP name qualifier
65    pub sp_name_qualifier: Option<String>,
66}
67
68/// SAML Subject Confirmation
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct SamlSubjectConfirmation {
71    /// Method (bearer, holder-of-key, etc.)
72    pub method: String,
73
74    /// Subject confirmation data
75    pub subject_confirmation_data: Option<SamlSubjectConfirmationData>,
76}
77
78/// SAML Subject Confirmation Data
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct SamlSubjectConfirmationData {
81    /// Not before timestamp
82    pub not_before: Option<DateTime<Utc>>,
83
84    /// Not on or after timestamp
85    pub not_on_or_after: Option<DateTime<Utc>>,
86
87    /// Recipient URL
88    pub recipient: Option<String>,
89
90    /// In response to (for response assertions)
91    pub in_response_to: Option<String>,
92
93    /// Address restriction
94    pub address: Option<String>,
95}
96
97/// SAML Conditions
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct SamlConditions {
100    /// Not before timestamp
101    pub not_before: Option<DateTime<Utc>>,
102
103    /// Not on or after timestamp
104    pub not_on_or_after: Option<DateTime<Utc>>,
105
106    /// Audience restrictions
107    pub audience_restrictions: Vec<SamlAudienceRestriction>,
108
109    /// One time use
110    pub one_time_use: bool,
111
112    /// Proxy restrictions
113    pub proxy_restriction: Option<SamlProxyRestriction>,
114}
115
116/// SAML Audience Restriction
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct SamlAudienceRestriction {
119    /// Audience URIs
120    pub audiences: Vec<String>,
121}
122
123/// SAML Proxy Restriction
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct SamlProxyRestriction {
126    /// Count limit
127    pub count: Option<u32>,
128
129    /// Allowed audiences
130    pub audiences: Vec<String>,
131}
132
133/// SAML Attribute Statement
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct SamlAttributeStatement {
136    /// Attributes
137    pub attributes: Vec<SamlAttribute>,
138
139    /// Encrypted attributes
140    pub encrypted_attributes: Vec<String>, // Would be proper encrypted elements in production
141}
142
143/// SAML Attribute
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct SamlAttribute {
146    /// Attribute name
147    pub name: String,
148
149    /// Name format
150    pub name_format: Option<String>,
151
152    /// Friendly name
153    pub friendly_name: Option<String>,
154
155    /// Attribute values
156    pub values: Vec<SamlAttributeValue>,
157}
158
159/// SAML Attribute Value
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct SamlAttributeValue {
162    /// Value content
163    pub value: String,
164
165    /// Type information
166    pub type_info: Option<String>,
167}
168
169/// SAML Authentication Statement
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct SamlAuthnStatement {
172    /// Authentication instant
173    pub authn_instant: DateTime<Utc>,
174
175    /// Session index
176    pub session_index: Option<String>,
177
178    /// Session not on or after
179    pub session_not_on_or_after: Option<DateTime<Utc>>,
180
181    /// Authentication context
182    pub authn_context: SamlAuthnContext,
183
184    /// Subject locality
185    pub subject_locality: Option<SamlSubjectLocality>,
186}
187
188/// SAML Authentication Context
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct SamlAuthnContext {
191    /// Authentication context class reference
192    pub authn_context_class_ref: Option<String>,
193
194    /// Authentication context declaration
195    pub authn_context_decl: Option<String>,
196
197    /// Authentication context declaration reference
198    pub authn_context_decl_ref: Option<String>,
199
200    /// Authenticating authorities
201    pub authenticating_authorities: Vec<String>,
202}
203
204/// SAML Subject Locality
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct SamlSubjectLocality {
207    /// IP address
208    pub address: Option<String>,
209
210    /// DNS name
211    pub dns_name: Option<String>,
212}
213
214/// SAML Authorization Decision Statement
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct SamlAuthzDecisionStatement {
217    /// Resource being accessed
218    pub resource: String,
219
220    /// Decision (Permit, Deny, Indeterminate)
221    pub decision: SamlDecision,
222
223    /// Actions being performed
224    pub actions: Vec<SamlAction>,
225
226    /// Evidence supporting the decision
227    pub evidence: Option<SamlEvidence>,
228}
229
230/// SAML Decision
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub enum SamlDecision {
233    /// Access permitted
234    Permit,
235
236    /// Access denied
237    Deny,
238
239    /// Decision cannot be made
240    Indeterminate,
241}
242
243/// SAML Action
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct SamlAction {
246    /// Action value
247    pub value: String,
248
249    /// Action namespace
250    pub namespace: Option<String>,
251}
252
253/// SAML Evidence
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct SamlEvidence {
256    /// Supporting assertions
257    pub assertions: Vec<String>, // References to other assertions
258
259    /// Assertion ID references
260    pub assertion_id_refs: Vec<String>,
261
262    /// Assertion URI references
263    pub assertion_uri_refs: Vec<String>,
264}
265
266/// SAML Assertion Builder
267pub struct SamlAssertionBuilder {
268    /// Current assertion being built
269    assertion: SamlAssertion,
270}
271
272/// SAML Assertion Validator
273pub struct SamlAssertionValidator {
274    /// Allowed clock skew
275    clock_skew: Duration,
276
277    /// Trusted issuers
278    trusted_issuers: Vec<String>,
279
280    /// Expected audiences
281    expected_audiences: Vec<String>,
282}
283
284impl SamlAssertionBuilder {
285    /// Create a new SAML assertion builder
286    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    /// Set the subject
303    pub fn with_subject(mut self, subject: SamlSubject) -> Self {
304        self.assertion.subject = Some(subject);
305        self
306    }
307
308    /// Set conditions
309    pub fn with_conditions(mut self, conditions: SamlConditions) -> Self {
310        self.assertion.conditions = Some(conditions);
311        self
312    }
313
314    /// Add an attribute statement
315    pub fn with_attribute_statement(mut self, statement: SamlAttributeStatement) -> Self {
316        self.assertion.attribute_statements.push(statement);
317        self
318    }
319
320    /// Add an authentication statement
321    pub fn with_authn_statement(mut self, statement: SamlAuthnStatement) -> Self {
322        self.assertion.authn_statements.push(statement);
323        self
324    }
325
326    /// Add an authorization decision statement
327    pub fn with_authz_decision_statement(mut self, statement: SamlAuthzDecisionStatement) -> Self {
328        self.assertion.authz_decision_statements.push(statement);
329        self
330    }
331
332    /// Add a simple attribute
333    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        // Find or create attribute statement
345        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    /// Set validity period
362    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            // Update existing conditions
369            conditions.not_before = Some(not_before);
370            conditions.not_on_or_after = Some(not_on_or_after);
371        } else {
372            // Create new conditions
373            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    /// Add audience restriction
386    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    /// Build the assertion
416    pub fn build(self) -> SamlAssertion {
417        self.assertion
418    }
419
420    /// Build and convert to XML
421    pub fn build_xml(self) -> Result<String> {
422        let assertion = self.assertion;
423        assertion.to_xml()
424    }
425}
426
427impl SamlAssertionValidator {
428    /// Create a new SAML assertion validator
429    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    /// Set clock skew tolerance
438    pub fn with_clock_skew(mut self, skew: Duration) -> Self {
439        self.clock_skew = skew;
440        self
441    }
442
443    /// Add trusted issuer
444    pub fn with_trusted_issuer(mut self, issuer: &str) -> Self {
445        self.trusted_issuers.push(issuer.to_string());
446        self
447    }
448
449    /// Add expected audience
450    pub fn with_expected_audience(mut self, audience: &str) -> Self {
451        self.expected_audiences.push(audience.to_string());
452        self
453    }
454
455    /// Validate a SAML assertion
456    pub fn validate(&self, assertion: &SamlAssertion) -> Result<()> {
457        // Check issuer
458        if !self.trusted_issuers.is_empty() && !self.trusted_issuers.contains(&assertion.issuer) {
459            return Err(AuthError::auth_method("saml", "Untrusted issuer"));
460        }
461
462        // Check time validity
463        self.validate_timing(assertion)?;
464
465        // Check audience restrictions
466        self.validate_audience(assertion)?;
467
468        // Check subject confirmation (if present)
469        if let Some(ref subject) = assertion.subject {
470            self.validate_subject_confirmation(subject)?;
471        }
472
473        Ok(())
474    }
475
476    /// Validate timing constraints
477    fn validate_timing(&self, assertion: &SamlAssertion) -> Result<()> {
478        let now = Utc::now();
479
480        // Check issue instant (shouldn't be too far in the future)
481        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        // Check conditions timing
489        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    /// Validate audience restrictions
507    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    /// Validate subject confirmation
530    fn validate_subject_confirmation(&self, _subject: &SamlSubject) -> Result<()> {
531        // Simplified validation - would check bearer tokens, holder-of-key, etc.
532        Ok(())
533    }
534}
535
536impl SamlAssertion {
537    /// Convert assertion to XML
538    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        // Issuer
549        xml.push_str(&format!("<saml:Issuer>{}</saml:Issuer>", self.issuer));
550
551        // Subject
552        if let Some(ref subject) = self.subject {
553            xml.push_str(&subject.to_xml()?);
554        }
555
556        // Conditions
557        if let Some(ref conditions) = self.conditions {
558            xml.push_str(&conditions.to_xml()?);
559        }
560
561        // Attribute statements
562        for statement in &self.attribute_statements {
563            xml.push_str(&statement.to_xml()?);
564        }
565
566        // Authentication statements
567        for statement in &self.authn_statements {
568            xml.push_str(&statement.to_xml()?);
569        }
570
571        // Authorization decision statements
572        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    /// Convert subject to XML
584    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    /// Convert name ID to XML
605    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    /// Convert subject confirmation to XML
630    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    /// Convert subject confirmation data to XML
650    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    /// Convert conditions to XML
689    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    /// Convert audience restriction to XML
730    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    /// Convert proxy restriction to XML
747    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    /// Convert attribute statement to XML
770    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        // Encrypted attributes would be handled here
780        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    /// Convert attribute to XML
792    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    /// Convert attribute value to XML
817    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    /// Convert authentication statement to XML
834    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    /// Convert authentication context to XML
869    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    /// Convert subject locality to XML
910    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    /// Convert authorization decision statement to XML
931    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    /// Convert action to XML
961    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    /// Convert evidence to XML
978    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