You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add SAML metadata validation with optional strict mode
Context
ex_saml should provide a first-class validation function for SAML metadata documents (both SP and IdP), usable before a document is exported to a partner or imported from one.
The goal is to detect:
XML / SAML structural issues (spec violations per OASIS).
Interoperability issues that pass with tolerant IdPs (Okta, Auth0, Google Workspace, Keycloak default) but fail with stricter ones (Entra ID, ADFS, Shibboleth strict, PingFederate strict, SAP IAS).
Real-world cases this would catch:
AssertionConsumerService declared with HTTP-Redirect binding.
entityID not formatted as an absolute URI.
Missing NameIDFormat.
Root CA certificate used as an operational SAML signing certificate.
Shared certificate for both signing and encryption KeyDescriptors.
Signature using RSA-SHA1 or other deprecated algorithms.
Why validate IdP metadata from an SP library
Even though ex_saml is an SP-focused library, a very common SP deployment pattern is to let an administrator upload IdP metadata through a web form (or paste it in) before it is persisted and used at runtime. Validating that payload at the boundary — before it is stored or wired into the provider loader — is a first-class SP concern:
Reject malformed documents early with an actionable error list, rather than failing later during an SSO attempt.
Surface interop warnings (deprecated algorithms, expired certs, missing NameIDFormat, non-HTTPS endpoints) to the admin while they still have the source document in hand.
Keep the runtime path (ExSaml.IdpData.load_provider/2) focused on loading valid configuration, not on producing user-facing diagnostics.
This use case must be explicitly called out in the public documentation (see PLAN → PR 5).
strict: false# default — spec conformance onlyignore: []# list of violation codes to silenceallow_http_endpoints: false# allow non-HTTPS endpoint locationscert_min_remaining_days: 30# warning threshold for near-expiry certsentity_id_domain_check: false# opt in to :entity_id_endpoint_domain_mismatch
<md:AssertionConsumerService> must not use HTTP-Redirect.
At least one <md:AssertionConsumerService> uses HTTP-POST, as required by the Web Browser SSO Profile (Profiles §4.1.3.5, Conformance §3.1).
Error code: :missing_http_post_acs.
Can be silenced via ignore: [:missing_http_post_acs] for SP metadata exclusively targeting ECP (PAOS) or Artifact profiles, which is a rare deployment.
All <md:AssertionConsumerService> entries have unique index values.
At most one <md:AssertionConsumerService> has isDefault="true".
<md:SingleLogoutService> bindings, when present, are in the allowed set:
HTTP-Redirect
HTTP-POST
HTTP-Artifact
SOAP
If a <ds:Signature> is present, validate its structure only:
expected nodes are present (SignedInfo, SignatureValue, Reference, etc.)
declared digest and signature algorithms are recognized
cryptographic verification against a trust anchor is out of scope
Every <md:KeyDescriptor> contains a parseable X.509 certificate.
Every X.509 certificate is not expired as of DateTime.utc_now/0.
This is treated as an error by default because expired certificates are not usable in practice for SAML exchanges.
It can be ignored for legacy scenarios with ignore: [:certificate_expired].
Best practices
These rules are warnings by default and errors when strict: true.
entityID should be an absolute URI according to RFC 3986.
<md:EntityDescriptor> declares either validUntil or cacheDuration.
All endpoint Location attributes are absolute HTTPS URLs.
Can be relaxed with allow_http_endpoints: true.
Signing certificate is not a CA certificate.
BasicConstraints should be CA:FALSE or absent.
Signing certificate KeyUsage includes digitalSignature.
Signing certificate KeyUsage does not include keyCertSign.
Separate certificates are used for use="signing" and use="encryption" KeyDescriptors.
At least one <md:NameIDFormat> is declared in the SP descriptor.
AuthnRequestsSigned="true" is set on SPSSODescriptor.
WantAssertionsSigned="true" is set on SPSSODescriptor.
Signature algorithms are in a modern allow-list:
RSA-SHA256
RSA-SHA384
RSA-SHA512
ECDSA-SHA256+
Deprecated algorithms such as RSA-SHA1, DSA-SHA1, or MD5 are rejected in strict mode.
Certificate validity window has more than cert_min_remaining_days days remaining.
<md:Organization> is present.
At least one <md:ContactPerson contactType="technical"> is present.
Opt-in lint: entityID domain is consistent with endpoint domains when entityID is an HTTPS URI.
This rule stays as :warning even in strict mode, and requires explicit opt-in via entity_id_domain_check: true to be surfaced.
Rationale: multi-tenant SAML-as-a-service providers (Cryptr, WorkOS, FusionAuth hosted, etc.) legitimately issue metadata where the entityID lives under the tenant's own domain while endpoints live under the provider's domain. This is not a misconfiguration.
The rule exists to catch accidental copy-paste or provisioning mismatches in single-tenant deployments, where this alignment is expected.
The entityID URI rule is intentionally stricter than what many permissive IdPs accept. The goal is interoperability with enterprise IdPs and predictable metadata exchange.
Example
# Default mode — tolerates non-URI entityID but catches ACS Redirectiex>ExSaml.Metadata.validate(xml){:error,%ExSaml.Metadata.ValidationResult{errors: [%{code: :invalid_acs_binding,severity: :error,message: "HTTP-Redirect binding is not valid for AssertionConsumerService",path: "/EntityDescriptor/SPSSODescriptor/AssertionConsumerService[2]",spec_reference: "SAML 2.0 Bindings §3.4.3, Profiles §4.1.3.5"}],warnings: [%{code: :entity_id_not_uri,severity: :warning,message: "entityID should be an absolute URI for interoperability",path: "/EntityDescriptor/@entityID",spec_reference: "SAML 2.0 Core §8.3.6, RFC 3986"}]}}
# Strict mode — promotes best-practice warnings to errorsiex>ExSaml.Metadata.validate(xml,strict: true){:error,%ExSaml.Metadata.ValidationResult{errors: [%{code: :invalid_acs_binding,severity: :error,message: "HTTP-Redirect binding is not valid for AssertionConsumerService",path: "/EntityDescriptor/SPSSODescriptor/AssertionConsumerService[2]",spec_reference: "SAML 2.0 Bindings §3.4.3, Profiles §4.1.3.5"},%{code: :entity_id_not_uri,severity: :error,message: "entityID should be an absolute URI for interoperability",path: "/EntityDescriptor/@entityID",spec_reference: "SAML 2.0 Core §8.3.6, RFC 3986"}],warnings: []}}
# Tolerate a known legacy issueiex>ExSaml.Metadata.validate(xml,strict: true,ignore: [:entity_id_not_uri]){:error,%ExSaml.Metadata.ValidationResult{errors: [%{code: :invalid_acs_binding,severity: :error,message: "HTTP-Redirect binding is not valid for AssertionConsumerService",path: "/EntityDescriptor/SPSSODescriptor/AssertionConsumerService[2]",spec_reference: "SAML 2.0 Bindings §3.4.3, Profiles §4.1.3.5"}],warnings: []}}
entity_id_domain_check: true with matching and mismatching domains
ignore: option suppressing specified codes
ExDoc documentation listing every violation code, severity by mode, and spec reference.
README section with before/after usage example.
README or ExDoc section explaining the difference between:
metadata structure validation
certificate linting
cryptographic XML signature verification
Non-goals
Out of scope for this issue:
Cryptographic verification of the XML signature against a trust anchor.
This should be handled separately by something like ExSaml.Metadata.verify_signature/2.
Full PKIX chain validation.
No trust store handling.
No root / intermediate chain building.
No CRL / OCSP revocation checks.
Fetching remote metadata over HTTP(S).
Auto-fixing or rewriting non-conformant metadata.
<md:EntitiesDescriptor> federation bundles with multiple entities.
Validation of <md:AttributeConsumingService>.
Validation of <mdui:UIInfo> extensions.
PLAN
Each checkbox below is delivered as its own pull request. Ordering is suggested — a later PR can be started once its predecessor is merged. Rule codes referenced here map to the "Suggested violation codes" list above.
Always-error half of certificate linting: :ca_certificate_used_for_signing, :invalid_signing_key_usage, :shared_signing_and_encryption_certificate may live here or in PR 3 depending on severity choice — decide during PR 2 review.
Fixtures: expired cert, Root CA used as signing cert, shared signing/encryption cert.
README or ExDoc section explaining the difference between metadata structure validation, certificate linting, and cryptographic XML signature verification.
ExDoc listing every violation code, its severity by mode, and its spec reference.
Explicit documentation of the admin-uploaded IdP metadata use case (form upload before persisting), with a code snippet showing how to call validate/2 from a controller or changeset.
Findings from reading the current codebase (lib/ex_saml/core/saml.ex, lib/ex_saml/idp_data.ex, lib/ex_saml/core/sp_metadata.ex, lib/ex_saml/core/idp_metadata.ex, mix.exs).
What exists today
IdP metadata is already parsed, but tolerantly: ExSaml.Core.Saml.decode_idp_metadata/1 (lib/ex_saml/core/saml.ex:204) only returns :bad_entity or :missing_sso_location; other structural defects surface as logs in lib/ex_saml/idp_data.ex (see :116, :196) rather than as an actionable diagnostic.
SP metadata is generated in lib/ex_saml/core/saml.ex:1188-1282.
The internal structs ExSaml.Core.SpMetadata (lib/ex_saml/core/sp_metadata.ex) and ExSaml.Core.IdpMetadata (lib/ex_saml/core/idp_metadata.ex) exist, but no public ExSaml.Metadata module. The proposed API slots in next to them without touching internals.
Natural insertion points
Add a new top-level ExSaml.Metadata module. The validator takes a raw XML binary, so it is fully decoupled from the provider runtime (no dependency on service_providers_accessor, IdpData.load_providers/2, or ETS state) — validation is unit-testable without bootstrapping providers.
sweet_xml + xmerl are already used throughout the library — reuse them for the XPath selectors that populate the path field of each violation.
:public_key and :crypto are already declared in mix.exs:27extra_applications — certificate inspection (expiration, BasicConstraints, KeyUsage) requires no new dependency.
The existing fixtures under test/data/*.xml (Azure, OneLogin, Shibboleth, SimpleSAML, TestShib) provide a ready-made "real-world IdP" corpus; new crafted fixtures will only need to cover the specific violation cases listed per PR above.
Latent non-conformance uncovered during this review
b4d8ee7 / 4761838 — CI security and quality checks.
Structural metadata validation is the complementary input-validation layer: it hardens the boundary where external metadata enters the library, filling the remaining gap between "we parse XML safely" and "we only trust well-formed, spec-conformant SAML documents".
Add SAML metadata validation with optional strict mode
Context
ex_samlshould provide a first-class validation function for SAML metadata documents (both SP and IdP), usable before a document is exported to a partner or imported from one.The goal is to detect:
Real-world cases this would catch:
AssertionConsumerServicedeclared with HTTP-Redirect binding.entityIDnot formatted as an absolute URI.NameIDFormat.signingandencryptionKeyDescriptors.Why validate IdP metadata from an SP library
Even though
ex_samlis an SP-focused library, a very common SP deployment pattern is to let an administrator upload IdP metadata through a web form (or paste it in) before it is persisted and used at runtime. Validating that payload at the boundary — before it is stored or wired into the provider loader — is a first-class SP concern:NameIDFormat, non-HTTPS endpoints) to the admin while they still have the source document in hand.ExSaml.IdpData.load_provider/2) focused on loading valid configuration, not on producing user-facing diagnostics.This use case must be explicitly called out in the public documentation (see PLAN → PR 5).
Proposed API
Options:
Return type:
Return semantics:
{:ok, %ValidationResult{errors: [], warnings: warnings}}when no error-level violation is found.{:error, %ValidationResult{errors: errors, warnings: warnings}}when at least one error-level violation is found.errorsandwarnings.Validation rules
Rules are split into specification conformance and best practices.
strict: true.ignore: [:some_code].Always enforced: SAML spec conformance
These rules map to MUST / MUST NOT expectations in SAML metadata and related OASIS specifications.
XML is well-formed.
Root element is
<md:EntityDescriptor>.<md:EntitiesDescriptor>is not supported in v1 and should return a clear:entities_descriptor_not_supportederror.entityIDis present.entityIDlength is ≤ 1024 characters.At least one
<md:SPSSODescriptor>or<md:IDPSSODescriptor>is present.protocolSupportEnumerationcontainsurn:oasis:names:tc:SAML:2.0:protocol.For SP metadata: at least one
<md:AssertionConsumerService>is declared.For IdP metadata: at least one
<md:SingleSignOnService>is declared.Every
<md:AssertionConsumerService>uses a permitted binding:urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POSTurn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifacturn:oasis:names:tc:SAML:2.0:bindings:PAOS<md:AssertionConsumerService>must not use HTTP-Redirect.At least one
<md:AssertionConsumerService>uses HTTP-POST, as required by the Web Browser SSO Profile (Profiles §4.1.3.5, Conformance §3.1).:missing_http_post_acs.ignore: [:missing_http_post_acs]for SP metadata exclusively targeting ECP (PAOS) or Artifact profiles, which is a rare deployment.All
<md:AssertionConsumerService>entries have uniqueindexvalues.At most one
<md:AssertionConsumerService>hasisDefault="true".<md:SingleLogoutService>bindings, when present, are in the allowed set:If a
<ds:Signature>is present, validate its structure only:SignedInfo,SignatureValue,Reference, etc.)Every
<md:KeyDescriptor>contains a parseable X.509 certificate.Every X.509 certificate is not expired as of
DateTime.utc_now/0.ignore: [:certificate_expired].Best practices
These rules are warnings by default and errors when
strict: true.entityIDshould be an absolute URI according to RFC 3986.<md:EntityDescriptor>declares eithervalidUntilorcacheDuration.All endpoint
Locationattributes are absolute HTTPS URLs.allow_http_endpoints: true.Signing certificate is not a CA certificate.
BasicConstraintsshould beCA:FALSEor absent.Signing certificate
KeyUsageincludesdigitalSignature.Signing certificate
KeyUsagedoes not includekeyCertSign.Separate certificates are used for
use="signing"anduse="encryption"KeyDescriptors.At least one
<md:NameIDFormat>is declared in the SP descriptor.AuthnRequestsSigned="true"is set onSPSSODescriptor.WantAssertionsSigned="true"is set onSPSSODescriptor.Signature algorithms are in a modern allow-list:
Deprecated algorithms such as RSA-SHA1, DSA-SHA1, or MD5 are rejected in strict mode.
Certificate validity window has more than
cert_min_remaining_daysdays remaining.<md:Organization>is present.At least one
<md:ContactPerson contactType="technical">is present.Opt-in lint:
entityIDdomain is consistent with endpoint domains whenentityIDis an HTTPS URI.:warningeven in strict mode, and requires explicit opt-in viaentity_id_domain_check: trueto be surfaced.entityIDlives under the tenant's own domain while endpoints live under the provider's domain. This is not a misconfiguration.The
entityIDURI rule is intentionally stricter than what many permissive IdPs accept. The goal is interoperability with enterprise IdPs and predictable metadata exchange.Example
Suggested violation codes
Acceptance criteria
Public functions
ExSaml.Metadata.validate/1andExSaml.Metadata.validate/2.Public struct
ExSaml.Metadata.ValidationResult.violationtype documented via@typeannotation with accompanying ExDoc@typedocexplaining each field and whenpath/spec_referencemay be nil.Every violation includes:
codeseveritymessagepathspec_referenceOptions:
:strict:ignore:allow_http_endpoints:cert_min_remaining_days:entity_id_domain_checkTest fixtures under
test/fixtures/metadata/covering:<md:EntitiesDescriptor>explicitly unsupported in v1ignore: [:missing_http_post_acs](ECP/Artifact-only case)indexisDefault="true"entityIDNameIDFormatallow_http_endpoints: trueentity_id_domain_check: truewith matching and mismatching domainsignore:option suppressing specified codesExDoc documentation listing every violation code, severity by mode, and spec reference.
README section with before/after usage example.
README or ExDoc section explaining the difference between:
Non-goals
Out of scope for this issue:
Cryptographic verification of the XML signature against a trust anchor.
ExSaml.Metadata.verify_signature/2.Full PKIX chain validation.
Fetching remote metadata over HTTP(S).
Auto-fixing or rewriting non-conformant metadata.
<md:EntitiesDescriptor>federation bundles with multiple entities.Validation of
<md:AttributeConsumingService>.Validation of
<mdui:UIInfo>extensions.PLAN
Each checkbox below is delivered as its own pull request. Ordering is suggested — a later PR can be started once its predecessor is merged. Rule codes referenced here map to the "Suggested violation codes" list above.
ExSaml.Metadata.validate/1andExSaml.Metadata.validate/2.ExSaml.Metadata.ValidationResultstruct +violation@type/@typedoc.:ignoreoption plumbing.:invalid_xml,:invalid_root_element,:entities_descriptor_not_supported,:missing_entity_id,:entity_id_too_long,:missing_role_descriptor,:missing_saml2_protocol_support,:missing_acs,:missing_sso_service,:invalid_acs_binding,:missing_http_post_acs,:duplicate_acs_index,:multiple_default_acs,:invalid_slo_binding.<md:EntitiesDescriptor>unsupported, ACS with HTTP-Redirect, ACS missing HTTP-POST (with and withoutignore: [:missing_http_post_acs]), duplicate ACSindex, multipleisDefault="true".:missing_x509_certificate,:invalid_x509_certificate,:certificate_expired(error by default, silence-able viaignore).:invalid_signature_structure,:unknown_signature_algorithm,:unknown_digest_algorithm.:ca_certificate_used_for_signing,:invalid_signing_key_usage,:shared_signing_and_encryption_certificatemay live here or in PR 3 depending on severity choice — decide during PR 2 review.:strict,:cert_min_remaining_days,:allow_http_endpoints.:entity_id_not_uri,:missing_valid_until_or_cache_duration,:non_https_endpoint,:missing_name_id_format,:missing_authn_requests_signed,:missing_want_assertions_signed,:deprecated_signature_algorithm,:certificate_near_expiry,:missing_organization,:missing_technical_contact.entityID, missingNameIDFormat, HTTP endpoint Location,allow_http_endpoints: true, RSA-SHA1 signature algorithm, near-expiry certificate.:entity_id_domain_check.:entity_id_endpoint_domain_mismatch(remains:warningeven in strict mode).validate/2from a controller or changeset.Related but out of scope for this issue
Fix the SP metadata generator to stop emitting an— resolved in 🐛 Fix / Remove HTTP-Redirect AssertionConsumerService from SP metadata #21 (closes 🐛 Bug / Remove HTTP-Redirect AssertionConsumerService from generated SP metadata #19). The generator now advertises a single HTTP-POST ACS, andAssertionConsumerServicewith bindingurn:oasis:names:tc:SAML:2.0:bindings:HTTP-RedirectExSaml.Core.Bindingdocuments the asymmetric request/response binding model.Cohérence avec l'existant
Findings from reading the current codebase (
lib/ex_saml/core/saml.ex,lib/ex_saml/idp_data.ex,lib/ex_saml/core/sp_metadata.ex,lib/ex_saml/core/idp_metadata.ex,mix.exs).What exists today
ExSaml.Core.Saml.decode_idp_metadata/1(lib/ex_saml/core/saml.ex:204) only returns:bad_entityor:missing_sso_location; other structural defects surface as logs inlib/ex_saml/idp_data.ex(see:116,:196) rather than as an actionable diagnostic.lib/ex_saml/core/saml.ex:1188-1282.ExSaml.Core.SpMetadata(lib/ex_saml/core/sp_metadata.ex) andExSaml.Core.IdpMetadata(lib/ex_saml/core/idp_metadata.ex) exist, but no publicExSaml.Metadatamodule. The proposed API slots in next to them without touching internals.Natural insertion points
ExSaml.Metadatamodule. The validator takes a raw XML binary, so it is fully decoupled from the provider runtime (no dependency onservice_providers_accessor,IdpData.load_providers/2, or ETS state) — validation is unit-testable without bootstrapping providers.sweet_xml+xmerlare already used throughout the library — reuse them for the XPath selectors that populate thepathfield of each violation.:public_keyand:cryptoare already declared inmix.exs:27extra_applications— certificate inspection (expiration,BasicConstraints,KeyUsage) requires no new dependency.test/data/*.xml(Azure, OneLogin, Shibboleth, SimpleSAML, TestShib) provide a ready-made "real-world IdP" corpus; new crafted fixtures will only need to cover the specific violation cases listed per PR above.Latent non-conformance uncovered during this review
— resolved in 🐛 Fix / Remove HTTP-Redirect AssertionConsumerService from SP metadata #21 (closes 🐛 Bug / Remove HTTP-Redirect AssertionConsumerService from generated SP metadata #19). The generator is now validator-clean:lib/ex_saml/core/saml.ex:1201-1211emits anAssertionConsumerServicewith bindingHTTP-Redirectinside the SP metadata generatorto_xml/1output round-trips throughExSaml.Metadata.validate/1with zero violations.Alignment with the project's recent direction
Recent commits on the library have pushed defensive hardening at parse/verify time:
48b6fca— XXE fix, NotBefore validation, algorithm whitelist.b4d8ee7/4761838— CI security and quality checks.Structural metadata validation is the complementary input-validation layer: it hardens the boundary where external metadata enters the library, filling the remaining gap between "we parse XML safely" and "we only trust well-formed, spec-conformant SAML documents".
References