saml-rs 0.6.1

Pure-Rust SAML 2.0 Service Provider and Identity Provider support.
Documentation
use crate::constants::namespace;
use crate::error::SamlError;
use crate::model::Status;
use crate::template::{write_login_response_attribute_statement, LoginResponseAttribute};
use crate::xml::write::XmlWriter;

const VERSION: &str = "2.0";
const XMLNS_XS: &str = "http://www.w3.org/2001/XMLSchema";
const XMLNS_XSI: &str = "http://www.w3.org/2001/XMLSchema-instance";
const BEARER_CONFIRMATION: &str = "urn:oasis:names:tc:SAML:2.0:cm:bearer";
const UNSPECIFIED_AUTHN_CONTEXT: &str = "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified";

pub(super) struct AuthnStatementXml<'a> {
    pub(super) authn_instant: &'a str,
    pub(super) session_index: Option<&'a str>,
}

pub(super) struct LoginResponseXml<'a> {
    pub(super) protocol_prefix: &'a str,
    pub(super) assertion_prefix: &'a str,
    pub(super) response_id: &'a str,
    pub(super) assertion_id: &'a str,
    pub(super) issue_instant: &'a str,
    pub(super) destination: &'a str,
    pub(super) subject_recipient: &'a str,
    pub(super) issuer: &'a str,
    pub(super) status: &'a Status,
    pub(super) subject_confirmation_not_on_or_after: &'a str,
    pub(super) conditions_not_before: &'a str,
    pub(super) conditions_not_on_or_after: &'a str,
    pub(super) audience: &'a str,
    pub(super) name_id_format: &'a str,
    pub(super) name_id: &'a str,
    pub(super) in_response_to: Option<&'a str>,
    pub(super) authn_statement: Option<AuthnStatementXml<'a>>,
    pub(super) attributes: &'a [LoginResponseAttribute],
    pub(super) user_attributes: &'a [(String, String)],
}

pub(super) fn render_default_login_response(
    input: &LoginResponseXml<'_>,
) -> Result<String, SamlError> {
    let response_name = qname(input.protocol_prefix, "Response");
    let protocol_xmlns = format!("xmlns:{}", input.protocol_prefix);
    let assertion_xmlns = format!("xmlns:{}", input.assertion_prefix);
    let assertion_name = qname(input.assertion_prefix, "Assertion");

    let mut response_attrs = vec![
        (protocol_xmlns.as_str(), namespace::PROTOCOL),
        (assertion_xmlns.as_str(), namespace::ASSERTION),
        ("ID", input.response_id),
        ("Version", VERSION),
        ("IssueInstant", input.issue_instant),
        ("Destination", input.destination),
    ];
    if let Some(in_response_to) = input.in_response_to {
        response_attrs.push(("InResponseTo", in_response_to));
    }

    let mut writer = XmlWriter::new();
    writer.start(&response_name, &response_attrs);
    writer.text_element(&qname(input.assertion_prefix, "Issuer"), &[], input.issuer);
    write_status(&mut writer, input.protocol_prefix, input.status);

    writer.start(
        &assertion_name,
        &[
            ("xmlns:xsi", XMLNS_XSI),
            ("xmlns:xs", XMLNS_XS),
            (assertion_xmlns.as_str(), namespace::ASSERTION),
            ("ID", input.assertion_id),
            ("Version", VERSION),
            ("IssueInstant", input.issue_instant),
        ],
    );
    writer.text_element(&qname(input.assertion_prefix, "Issuer"), &[], input.issuer);
    writer.start(&qname(input.assertion_prefix, "Subject"), &[]);
    writer.text_element(
        &qname(input.assertion_prefix, "NameID"),
        &[("Format", input.name_id_format)],
        input.name_id,
    );
    writer.start(
        &qname(input.assertion_prefix, "SubjectConfirmation"),
        &[("Method", BEARER_CONFIRMATION)],
    );
    let mut confirmation_attrs = vec![
        ("NotOnOrAfter", input.subject_confirmation_not_on_or_after),
        ("Recipient", input.subject_recipient),
    ];
    if let Some(in_response_to) = input.in_response_to {
        confirmation_attrs.push(("InResponseTo", in_response_to));
    }
    writer.empty(
        &qname(input.assertion_prefix, "SubjectConfirmationData"),
        &confirmation_attrs,
    );
    writer.end(&qname(input.assertion_prefix, "SubjectConfirmation"));
    writer.end(&qname(input.assertion_prefix, "Subject"));
    writer.start(
        &qname(input.assertion_prefix, "Conditions"),
        &[
            ("NotBefore", input.conditions_not_before),
            ("NotOnOrAfter", input.conditions_not_on_or_after),
        ],
    );
    writer.start(&qname(input.assertion_prefix, "AudienceRestriction"), &[]);
    writer.text_element(
        &qname(input.assertion_prefix, "Audience"),
        &[],
        input.audience,
    );
    writer.end(&qname(input.assertion_prefix, "AudienceRestriction"));
    writer.end(&qname(input.assertion_prefix, "Conditions"));
    if let Some(statement) = input.authn_statement.as_ref() {
        write_authn_statement(&mut writer, input.assertion_prefix, statement);
    }
    write_login_response_attribute_statement(
        &mut writer,
        input.attributes,
        input.user_attributes,
        input.assertion_prefix,
    )?;
    writer.end(&assertion_name);
    writer.end(&response_name);
    Ok(writer.finish())
}

pub(super) struct ErrorLoginResponseXml<'a> {
    pub(super) protocol_prefix: &'a str,
    pub(super) assertion_prefix: &'a str,
    pub(super) response_id: &'a str,
    pub(super) issue_instant: &'a str,
    pub(super) destination: &'a str,
    pub(super) issuer: &'a str,
    pub(super) status: &'a Status,
    pub(super) in_response_to: Option<&'a str>,
}

pub(super) fn render_error_login_response(
    input: &ErrorLoginResponseXml<'_>,
) -> Result<String, SamlError> {
    let response_name = qname(input.protocol_prefix, "Response");
    let protocol_xmlns = format!("xmlns:{}", input.protocol_prefix);
    let assertion_xmlns = format!("xmlns:{}", input.assertion_prefix);
    let mut response_attrs = vec![
        (protocol_xmlns.as_str(), namespace::PROTOCOL),
        (assertion_xmlns.as_str(), namespace::ASSERTION),
        ("ID", input.response_id),
        ("Version", VERSION),
        ("IssueInstant", input.issue_instant),
        ("Destination", input.destination),
    ];
    if let Some(in_response_to) = input.in_response_to {
        response_attrs.push(("InResponseTo", in_response_to));
    }

    let mut writer = XmlWriter::new();
    writer.start(&response_name, &response_attrs);
    writer.text_element(&qname(input.assertion_prefix, "Issuer"), &[], input.issuer);
    write_status(&mut writer, input.protocol_prefix, input.status);
    writer.end(&response_name);
    Ok(writer.finish())
}

fn write_status(writer: &mut XmlWriter, protocol_prefix: &str, status: &Status) {
    let status_name = qname(protocol_prefix, "Status");
    let status_code_name = qname(protocol_prefix, "StatusCode");
    writer.start(&status_name, &[]);
    match status.subordinate() {
        Some(subordinate) => {
            writer.start(&status_code_name, &[("Value", status.top_level().as_uri())]);
            writer.empty(&status_code_name, &[("Value", subordinate.as_uri())]);
            writer.end(&status_code_name);
        }
        None => writer.empty(&status_code_name, &[("Value", status.top_level().as_uri())]),
    }
    writer.end(&status_name);
}

fn write_authn_statement(writer: &mut XmlWriter, prefix: &str, statement: &AuthnStatementXml<'_>) {
    let name = qname(prefix, "AuthnStatement");
    let mut attrs = vec![("AuthnInstant", statement.authn_instant)];
    if let Some(session_index) = statement.session_index {
        attrs.push(("SessionIndex", session_index));
    }
    writer.start(&name, &attrs);
    writer.start(&qname(prefix, "AuthnContext"), &[]);
    writer.text_element(
        &qname(prefix, "AuthnContextClassRef"),
        &[],
        UNSPECIFIED_AUTHN_CONTEXT,
    );
    writer.end(&qname(prefix, "AuthnContext"));
    writer.end(&name);
}

fn qname(prefix: &str, local_name: &str) -> String {
    format!("{prefix}:{local_name}")
}