samael 0.0.21

A SAML2 library for Rust
use crate::attribute::{Attribute, AttributeValue};
use crate::schema::{
    Assertion, AttributeStatement, AudienceRestriction, AuthnContext, AuthnContextClassRef,
    AuthnStatement, Conditions, Issuer, Response, Status, StatusCode, Subject, SubjectConfirmation,
    SubjectConfirmationData, SubjectNameID,
};
use crate::signature::Signature;
use chrono::Utc;

use super::sp_extractor::RequiredAttribute;
use crate::crypto;
use crate::crypto::CertificateDer;

fn build_conditions(audience: &str) -> Conditions {
    Conditions {
        not_before: None,
        not_on_or_after: None,
        audience_restrictions: Some(vec![AudienceRestriction {
            audience: vec![audience.to_string()],
        }]),
        one_time_use: None,
        proxy_restriction: None,
    }
}

fn build_authn_statement(class: &str) -> AuthnStatement {
    AuthnStatement {
        authn_instant: Some(Utc::now()),
        session_index: None,
        session_not_on_or_after: None,
        subject_locality: None,
        authn_context: Some(AuthnContext {
            value: Some(AuthnContextClassRef {
                value: Some(class.to_string()),
            }),
        }),
    }
}

pub struct ResponseAttribute<'a> {
    pub required_attribute: RequiredAttribute,
    pub value: &'a str,
}

fn build_attributes(formats_names_values: &[ResponseAttribute]) -> Vec<Attribute> {
    formats_names_values
        .iter()
        .map(|attr| Attribute {
            friendly_name: None,
            name: Some(attr.required_attribute.name.clone()),
            name_format: attr.required_attribute.format.clone(),
            values: vec![AttributeValue {
                attribute_type: Some("xs:string".to_string()),
                value: Some(attr.value.to_string()),
            }],
        })
        .collect()
}

fn build_assertion(
    name_id: &str,
    request_id: &str,
    issuer: Issuer,
    recipient: &str,
    audience: &str,
    attributes: &[ResponseAttribute],
) -> Assertion {
    let assertion_id = crypto::gen_saml_assertion_id();

    Assertion {
        id: assertion_id,
        issue_instant: Utc::now(),
        version: "2.0".to_string(),
        issuer,
        signature: None,
        subject: Some(Subject {
            name_id: Some(SubjectNameID {
                format: Some("urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified".to_string()),
                value: name_id.to_owned(),
            }),
            subject_confirmations: Some(vec![SubjectConfirmation {
                method: Some("urn:oasis:names:tc:SAML:2.0:cm:bearer".to_string()),
                name_id: None,
                subject_confirmation_data: Some(SubjectConfirmationData {
                    not_before: None,
                    not_on_or_after: None,
                    recipient: Some(recipient.to_owned()),
                    in_response_to: Some(request_id.to_owned()),
                    address: None,
                    content: None,
                }),
            }]),
        }),
        conditions: Some(build_conditions(audience)),
        authn_statements: Some(vec![build_authn_statement(
            "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified",
        )]),
        attribute_statements: Some(vec![AttributeStatement {
            attributes: build_attributes(attributes),
        }]),
    }
}

fn build_response(
    name_id: &str,
    issuer: &str,
    request_id: &str,
    attributes: &[ResponseAttribute],
    destination: &str,
    audience: &str,
    x509_cert: &CertificateDer,
) -> Response {
    let issuer = Issuer {
        value: Some(issuer.to_string()),
        ..Default::default()
    };

    let response_id = crypto::gen_saml_response_id();

    Response {
        id: response_id.clone(),
        in_response_to: Some(request_id.to_owned()),
        version: "2.0".to_string(),
        issue_instant: Utc::now(),
        destination: Some(destination.to_string()),
        consent: None,
        issuer: Some(issuer.clone()),
        signature: Some(Signature::template(&response_id, x509_cert)),
        status: Some(Status {
            status_code: StatusCode {
                value: Some("urn:oasis:names:tc:SAML:2.0:status:Success".to_string()),
            },
            status_message: None,
            status_detail: None,
        }),
        encrypted_assertion: None,
        assertion: Some(build_assertion(
            name_id,
            request_id,
            issuer,
            destination,
            audience,
            attributes,
        )),
    }
}

pub fn build_response_template(
    cert_der: &CertificateDer,
    name_id: &str,
    audience: &str,
    issuer: &str,
    acs_url: &str,
    request_id: &str,
    attributes: &[ResponseAttribute],
) -> Response {
    build_response(
        name_id, issuer, request_id, attributes, acs_url, audience, cert_der,
    )
}