use base64::Engine as _;
use samael::{
crypto::AllowedSignatureAlgorithm,
metadata::EntityDescriptor,
service_provider::{ServiceProvider, ServiceProviderBuilder},
};
use super::SamlError;
const DEFAULT_EMAIL_ATTRS: &[&str] = &[
"urn:oid:0.9.2342.19200300.100.1.3",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
"email",
"mail",
"emailAddress",
];
const DEFAULT_NAME_ATTRS: &[&str] = &[
"urn:oid:2.16.840.1.113730.3.1.241",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
"displayName",
"name",
"cn",
];
fn default_allowed_algorithms() -> Vec<AllowedSignatureAlgorithm> {
vec![
AllowedSignatureAlgorithm::RsaSha256,
AllowedSignatureAlgorithm::RsaSha384,
AllowedSignatureAlgorithm::RsaSha512,
AllowedSignatureAlgorithm::EcdsaSha256,
AllowedSignatureAlgorithm::EcdsaSha384,
AllowedSignatureAlgorithm::EcdsaSha512,
]
}
#[derive(Debug, Clone)]
pub struct SamlAttributeMapping {
pub email: Vec<String>,
pub display_name: Vec<String>,
}
impl Default for SamlAttributeMapping {
fn default() -> Self {
Self {
email: DEFAULT_EMAIL_ATTRS.iter().map(|s| (*s).to_string()).collect(),
display_name: DEFAULT_NAME_ATTRS.iter().map(|s| (*s).to_string()).collect(),
}
}
}
pub struct SamlIdpConfig {
pub idp_name: String,
pub tenant_id: Option<String>,
pub trust_asserted_email: bool,
pub attribute_mapping: SamlAttributeMapping,
pub(crate) sp: ServiceProvider,
}
impl std::fmt::Debug for SamlIdpConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SamlIdpConfig")
.field("idp_name", &self.idp_name)
.field("tenant_id", &self.tenant_id)
.field("trust_asserted_email", &self.trust_asserted_email)
.field("attribute_mapping", &self.attribute_mapping)
.finish_non_exhaustive()
}
}
impl SamlIdpConfig {
#[must_use]
pub fn builder(
idp_name: impl Into<String>,
sp_entity_id: impl Into<String>,
acs_url: impl Into<String>,
) -> SamlIdpConfigBuilder {
SamlIdpConfigBuilder {
idp_name: idp_name.into(),
sp_entity_id: sp_entity_id.into(),
acs_url: acs_url.into(),
idp_metadata: None,
tenant_id: None,
trust_asserted_email: false,
attribute_mapping: SamlAttributeMapping::default(),
}
}
#[must_use]
pub fn provider_key(&self) -> String {
super::saml_provider_key(&self.idp_name)
}
#[must_use]
pub fn sso_redirect_url(&self) -> Option<String> {
self.sp.sso_binding_location(samael::metadata::HTTP_REDIRECT_BINDING)
}
pub(crate) const fn service_provider(&self) -> &ServiceProvider {
&self.sp
}
}
#[derive(Debug)]
pub struct SamlIdpConfigBuilder {
idp_name: String,
sp_entity_id: String,
acs_url: String,
idp_metadata: Option<EntityDescriptor>,
tenant_id: Option<String>,
trust_asserted_email: bool,
attribute_mapping: SamlAttributeMapping,
}
impl SamlIdpConfigBuilder {
pub fn idp_metadata_xml(mut self, xml: &str) -> Result<Self, SamlError> {
let descriptor: EntityDescriptor = xml
.parse()
.map_err(|e| SamlError::Config(format!("invalid IdP metadata XML: {e}")))?;
self.idp_metadata = Some(descriptor);
Ok(self)
}
pub fn idp_parts(
self,
idp_entity_id: &str,
sso_redirect_url: &str,
signing_cert_der: &[u8],
) -> Result<Self, SamlError> {
let xml = idp_metadata_xml_from_parts(idp_entity_id, sso_redirect_url, signing_cert_der);
self.idp_metadata_xml(&xml)
}
#[must_use]
pub fn tenant_id(mut self, tenant_id: Option<String>) -> Self {
self.tenant_id = tenant_id;
self
}
#[must_use]
pub const fn trust_asserted_email(mut self, trust: bool) -> Self {
self.trust_asserted_email = trust;
self
}
#[must_use]
pub fn attribute_mapping(mut self, mapping: SamlAttributeMapping) -> Self {
self.attribute_mapping = mapping;
self
}
pub fn build(self) -> Result<SamlIdpConfig, SamlError> {
let idp_metadata = self
.idp_metadata
.ok_or_else(|| SamlError::Config("IdP metadata not supplied".to_string()))?;
let sp = ServiceProviderBuilder::default()
.entity_id(Some(self.sp_entity_id))
.acs_url(Some(self.acs_url))
.idp_metadata(idp_metadata)
.allowed_signature_algorithms(Some(default_allowed_algorithms()))
.allow_idp_initiated(false)
.build()
.map_err(|e| SamlError::Config(format!("service provider build failed: {e}")))?;
Ok(SamlIdpConfig {
idp_name: self.idp_name,
tenant_id: self.tenant_id,
trust_asserted_email: self.trust_asserted_email,
attribute_mapping: self.attribute_mapping,
sp,
})
}
}
fn idp_metadata_xml_from_parts(
idp_entity_id: &str,
sso_redirect_url: &str,
signing_cert_der: &[u8],
) -> String {
let cert_b64 = base64::engine::general_purpose::STANDARD.encode(signing_cert_der);
format!(
r#"<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="{idp_entity_id}">
<IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="signing">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data><X509Certificate>{cert_b64}</X509Certificate></X509Data>
</KeyInfo>
</KeyDescriptor>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="{sso_redirect_url}"/>
</IDPSSODescriptor>
</EntityDescriptor>"#
)
}