rustauth-saml 0.3.0

SAML 2.0 service-provider support for RustAuth.
Documentation
#[cfg(feature = "saml-signed")]
use opensaml::constants::Binding;
use opensaml::constants::ParserType;
#[cfg(feature = "saml-signed")]
use opensaml::flow::{flow, FlowOptions, HttpRequest};

#[cfg(feature = "saml-signed")]
use crate::bridge::opensaml_error_code;
#[cfg(feature = "saml-signed")]
use crate::options::SamlConfig;

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct SamlSignatureInfo {
    pub count: usize,
    pub response: bool,
    pub assertion: bool,
    pub logout_request: bool,
    pub logout_response: bool,
}

impl SamlSignatureInfo {
    pub fn is_signed(self) -> bool {
        self.count > 0
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SamlSignedElement {
    Response,
    Assertion,
    LogoutRequest,
    LogoutResponse,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VerifiedSamlSignature {
    pub element: SamlSignedElement,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SamlSignatureValidationError {
    NotImplemented,
    MissingCertificate,
    AmbiguousSignature,
    Invalid,
}

impl SamlSignatureValidationError {
    pub fn code(self) -> &'static str {
        match self {
            Self::NotImplemented => "SAML_SIGNATURE_VALIDATION_NOT_IMPLEMENTED",
            Self::MissingCertificate => "SAML_CERTIFICATE_REQUIRED",
            Self::AmbiguousSignature => "SAML_SIGNATURE_AMBIGUOUS",
            Self::Invalid => "SAML_SIGNATURE_INVALID",
        }
    }
}

pub async fn verify_signed_saml_response(
    encoded_response: &str,
    signature: SamlSignatureInfo,
    cert: &str,
) -> Result<VerifiedSamlSignature, SamlSignatureValidationError> {
    if !signature.is_signed() {
        return Err(SamlSignatureValidationError::Invalid);
    }
    verify_post_message(encoded_response, cert, ParserType::SamlResponse, signature)
}

pub async fn verify_signed_logout_request(
    encoded_request: &str,
    signature: SamlSignatureInfo,
    cert: &str,
) -> Result<VerifiedSamlSignature, SamlSignatureValidationError> {
    if !signature.is_signed() {
        return Err(SamlSignatureValidationError::Invalid);
    }
    verify_post_message(encoded_request, cert, ParserType::LogoutRequest, signature).map(|_| {
        VerifiedSamlSignature {
            element: SamlSignedElement::LogoutRequest,
        }
    })
}

pub async fn verify_signed_logout_response(
    encoded_response: &str,
    signature: SamlSignatureInfo,
    cert: &str,
) -> Result<VerifiedSamlSignature, SamlSignatureValidationError> {
    if !signature.is_signed() {
        return Err(SamlSignatureValidationError::Invalid);
    }
    verify_post_message(
        encoded_response,
        cert,
        ParserType::LogoutResponse,
        signature,
    )
    .map(|_| VerifiedSamlSignature {
        element: SamlSignedElement::LogoutResponse,
    })
}

pub fn verify_redirect_logout_request(
    path_and_query: &str,
    cert: &str,
) -> Result<(), SamlSignatureValidationError> {
    verify_redirect_signature(path_and_query, cert, ParserType::LogoutRequest)
}

pub fn verify_redirect_logout_response(
    path_and_query: &str,
    cert: &str,
) -> Result<(), SamlSignatureValidationError> {
    verify_redirect_signature(path_and_query, cert, ParserType::LogoutResponse)
}

fn verify_post_message(
    encoded: &str,
    cert: &str,
    parser_type: ParserType,
    signature: SamlSignatureInfo,
) -> Result<VerifiedSamlSignature, SamlSignatureValidationError> {
    #[cfg(feature = "saml-signed")]
    {
        let compact = encoded.split_whitespace().collect::<String>();
        let param = parser_type.query_param();
        let request = HttpRequest::post(vec![(param.to_owned(), compact)]);
        let certs = [cert.to_owned()];
        flow(
            &FlowOptions {
                binding: Some(Binding::Post),
                parser_type: Some(parser_type),
                check_signature: true,
                from_issuer: None,
                signing_certs: &certs,
                decrypt_key: None,
                decrypt_key_pass: None,
                clock_drifts: (0, 0),
                expected_audience: None,
                expected_in_response_to: None,
            },
            &request,
        )
        .map_err(map_verify_error)?;
        let element = if signature.assertion {
            SamlSignedElement::Assertion
        } else {
            SamlSignedElement::Response
        };
        Ok(VerifiedSamlSignature { element })
    }
    #[cfg(not(feature = "saml-signed"))]
    {
        let _ = (encoded, cert, parser_type, signature);
        Err(SamlSignatureValidationError::NotImplemented)
    }
}

fn verify_redirect_signature(
    path_and_query: &str,
    cert: &str,
    parser_type: ParserType,
) -> Result<(), SamlSignatureValidationError> {
    #[cfg(feature = "saml-signed")]
    {
        use opensaml::binding::{base64_decode, deflate_raw_decode};

        let url = url::Url::parse(&format!("https://example.test{path_and_query}"))
            .map_err(|_| SamlSignatureValidationError::Invalid)?;
        let query: Vec<(String, String)> = url
            .query_pairs()
            .map(|(key, value)| (key.into_owned(), value.into_owned()))
            .collect();
        let param = parser_type.query_param();
        let message = query
            .iter()
            .find(|(key, _)| key == param)
            .map(|(_, value)| value.clone())
            .ok_or(SamlSignatureValidationError::Invalid)?;
        let relay_state = query
            .iter()
            .find(|(key, _)| key == "RelayState")
            .map(|(_, value)| value.as_str());
        let sig_alg = query
            .iter()
            .find(|(key, _)| key == "SigAlg")
            .map(|(_, value)| value.as_str())
            .ok_or(SamlSignatureValidationError::Invalid)?;
        let xml = String::from_utf8(
            deflate_raw_decode(
                &base64_decode(&message).map_err(|_| SamlSignatureValidationError::Invalid)?,
            )
            .map_err(|_| SamlSignatureValidationError::Invalid)?,
        )
        .map_err(|_| SamlSignatureValidationError::Invalid)?;
        let octet =
            opensaml::binding::build_redirect_octet(parser_type, &xml, relay_state, sig_alg)
                .map_err(|_| SamlSignatureValidationError::Invalid)?;
        let mut request = HttpRequest::redirect(query);
        request.octet_string = Some(octet);
        let certs = [cert.to_owned()];
        flow(
            &FlowOptions {
                binding: Some(Binding::Redirect),
                parser_type: Some(parser_type),
                check_signature: true,
                from_issuer: None,
                signing_certs: &certs,
                decrypt_key: None,
                decrypt_key_pass: None,
                clock_drifts: (0, 0),
                expected_audience: None,
                expected_in_response_to: None,
            },
            &request,
        )
        .map_err(map_verify_error)?;
        Ok(())
    }
    #[cfg(not(feature = "saml-signed"))]
    {
        let _ = (path_and_query, cert, parser_type);
        Err(SamlSignatureValidationError::NotImplemented)
    }
}

#[cfg(feature = "saml-signed")]
fn map_verify_error(error: opensaml::error::OpenSamlError) -> SamlSignatureValidationError {
    match opensaml_error_code(&error) {
        "SAML_SIGNATURE_VALIDATION_NOT_IMPLEMENTED" => SamlSignatureValidationError::NotImplemented,
        "SAML_CERTIFICATE_REQUIRED" => SamlSignatureValidationError::MissingCertificate,
        _ => SamlSignatureValidationError::Invalid,
    }
}

#[cfg(feature = "saml-signed")]
pub fn verify_saml_response_with_config(
    encoded_response: &str,
    config: &SamlConfig,
    base_url: &str,
    provider_id: &str,
    build_options: &crate::bridge::SpBuildOptions,
    in_response_to: Option<&str>,
) -> Result<VerifiedSamlSignature, SamlSignatureValidationError> {
    let sp = crate::bridge::create_service_provider(config, base_url, provider_id, build_options)
        .map_err(|_| SamlSignatureValidationError::Invalid)?;
    let idp = crate::bridge::create_identity_provider(config)
        .map_err(|_| SamlSignatureValidationError::Invalid)?;
    crate::bridge::parse_login_response(&sp, &idp, encoded_response, in_response_to, true)
        .map_err(map_verify_error)?;
    Ok(VerifiedSamlSignature {
        element: SamlSignedElement::Assertion,
    })
}