rustauth-sso 0.3.0

Single sign-on support for RustAuth.
Documentation
use std::sync::Arc;

mod domain_verification;
#[cfg(feature = "oidc")]
mod oidc;
mod provider_update;
mod providers;
mod registration;
#[cfg(feature = "saml")]
mod saml_acs;
#[cfg(feature = "saml")]
mod saml_config;
#[cfg(feature = "saml")]
mod saml_metadata;
mod sign_in;
#[cfg(feature = "saml")]
mod slo;
mod support;

#[cfg(feature = "saml")]
use http::Method;
use rustauth_core::api::AsyncAuthEndpoint;
#[cfg(feature = "saml")]
use serde_json::json;

#[cfg(feature = "saml")]
use crate::options::SamlConfig;
use crate::options::SsoOptions;
#[cfg(feature = "saml")]
use crate::saml_impl::security::{validate_saml_config_algorithms_with_policy, SamlSecurityError};
#[cfg(feature = "saml")]
use crate::saml_impl::signature::SamlSignatureValidationError;
#[cfg(feature = "saml")]
use crate::utils;

pub fn endpoints(options: Arc<SsoOptions>) -> Vec<AsyncAuthEndpoint> {
    let mut endpoints = vec![
        registration::endpoint(Arc::clone(&options)),
        sign_in::endpoint(Arc::clone(&options)),
        providers::list_endpoint(Arc::clone(&options)),
        providers::get_endpoint(Arc::clone(&options)),
        provider_update::endpoint(Arc::clone(&options)),
        providers::delete_endpoint(Arc::clone(&options)),
    ];
    #[cfg(feature = "oidc")]
    {
        endpoints.push(oidc::callback_endpoint(
            Arc::clone(&options),
            "/sso/callback/:providerId",
        ));
        endpoints.push(oidc::callback_endpoint(
            Arc::clone(&options),
            "/sso/callback",
        ));
    }
    #[cfg(feature = "saml")]
    {
        endpoints.push(saml_metadata::endpoint(Arc::clone(&options)));
        endpoints.push(saml_acs::get_callback_endpoint());
        endpoints.push(saml_acs::endpoint(
            Arc::clone(&options),
            "/sso/saml2/callback/:providerId",
            "handleSAMLCallback",
        ));
        endpoints.push(saml_acs::endpoint(
            Arc::clone(&options),
            "/sso/saml2/sp/acs/:providerId",
            "handleSAMLAssertionConsumerService",
        ));
        endpoints.push(slo::endpoint(Arc::clone(&options), Method::GET));
        endpoints.push(slo::endpoint(Arc::clone(&options), Method::POST));
        endpoints.push(slo::logout_endpoint(Arc::clone(&options)));
    }
    if options.domain_verification.enabled {
        endpoints.push(domain_verification::request_endpoint(Arc::clone(&options)));
        endpoints.push(domain_verification::verify_endpoint(Arc::clone(&options)));
    }
    endpoints
}

#[cfg(feature = "saml")]
pub(super) fn saml_signature_error_response(
    error: SamlSignatureValidationError,
) -> Result<rustauth_core::api::ApiResponse, rustauth_core::error::RustAuthError> {
    utils::json(
        http::StatusCode::BAD_REQUEST,
        &json!({"code": error.code()}),
    )
}
fn optional_http_url(value: Option<&str>) -> bool {
    value.map(is_valid_http_url).unwrap_or(true)
}

fn is_valid_http_url(value: &str) -> bool {
    url::Url::parse(value)
        .map(|url| matches!(url.scheme(), "http" | "https"))
        .unwrap_or(false)
}

#[cfg(feature = "saml")]
fn validate_configured_saml_algorithms(
    config: &SamlConfig,
    options: &SsoOptions,
) -> Result<(), SamlSecurityError> {
    validate_saml_config_algorithms_with_policy(
        config.signature_algorithm.as_deref(),
        config.digest_algorithm.as_deref(),
        options.saml.algorithms.on_deprecated,
        options
            .saml
            .algorithms
            .allowed_signature_algorithms
            .as_deref(),
        options.saml.algorithms.allowed_digest_algorithms.as_deref(),
    )
}

#[cfg(feature = "saml")]
fn saml_algorithm_error_response(
    error: SamlSecurityError,
) -> Result<rustauth_core::api::ApiResponse, rustauth_core::error::RustAuthError> {
    match error {
        SamlSecurityError::UnknownSignatureAlgorithm(algorithm)
        | SamlSecurityError::UnknownDigestAlgorithm(algorithm) => utils::json(
            http::StatusCode::BAD_REQUEST,
            &json!({
                "code": "SAML_UNKNOWN_ALGORITHM",
                "message": format!("SAML algorithm not recognized: {algorithm}")
            }),
        ),
        SamlSecurityError::DeprecatedSignatureAlgorithm(algorithm)
        | SamlSecurityError::DeprecatedDigestAlgorithm(algorithm) => utils::json(
            http::StatusCode::BAD_REQUEST,
            &json!({
                "code": "SAML_DEPRECATED_CONFIG_ALGORITHM",
                "message": format!("SAML config uses deprecated algorithm: {algorithm}")
            }),
        ),
        SamlSecurityError::SignatureAlgorithmNotAllowed(algorithm)
        | SamlSecurityError::DigestAlgorithmNotAllowed(algorithm) => utils::json(
            http::StatusCode::BAD_REQUEST,
            &json!({
                "code": "SAML_ALGORITHM_NOT_ALLOWED",
                "message": format!("SAML algorithm not in allow-list: {algorithm}")
            }),
        ),
        other => utils::json(
            http::StatusCode::BAD_REQUEST,
            &json!({
                "code": "INVALID_SAML_CONFIG",
                "message": other.to_string()
            }),
        ),
    }
}

#[cfg(feature = "saml")]
fn saml_runtime_algorithm_error_code(error: &SamlSecurityError) -> &'static str {
    match error {
        SamlSecurityError::UnknownSignatureAlgorithm(_)
        | SamlSecurityError::UnknownDigestAlgorithm(_)
        | SamlSecurityError::UnknownKeyEncryptionAlgorithm(_)
        | SamlSecurityError::UnknownDataEncryptionAlgorithm(_) => "SAML_UNKNOWN_ALGORITHM",
        SamlSecurityError::DeprecatedSignatureAlgorithm(_)
        | SamlSecurityError::DeprecatedDigestAlgorithm(_)
        | SamlSecurityError::DeprecatedKeyEncryptionAlgorithm(_)
        | SamlSecurityError::DeprecatedDataEncryptionAlgorithm(_) => {
            "SAML_DEPRECATED_RUNTIME_ALGORITHM"
        }
        SamlSecurityError::SignatureAlgorithmNotAllowed(_)
        | SamlSecurityError::DigestAlgorithmNotAllowed(_)
        | SamlSecurityError::KeyEncryptionAlgorithmNotAllowed(_)
        | SamlSecurityError::DataEncryptionAlgorithmNotAllowed(_) => "SAML_ALGORITHM_NOT_ALLOWED",
        _ => "INVALID_SAML_RESPONSE",
    }
}