SAML 2.0 Service Provider (SP) library for Elixir/Phoenix applications.
Originally built from the Samly codebase (by handnot2), before the Dropbox fork was created. Dropbox's fork has since been declared unmaintained. ExSaml is the actively maintained successor, with enhanced security, configurable caching, and streamlined routing.
- SP-initiated and IdP-initiated SSO flows
- Single Logout (SLO) support
- SP metadata generation
- Multi-IdP support with per-IdP configuration
- IdP identification via path segment or subdomain
- Pluggable assertion storage (ETS, Session, Nebulex cache)
- Relay state cache with anti-replay protection
- Security headers plug (CSP with nonce, X-Frame-Options, etc.)
- Support for many IdP types: ADFS, Azure AD, Google, Keycloak, Okta, OneLogin, PingFederate, PingOne, IBM Security Verify, LemonLDAP
Add ex_saml to your dependencies in mix.exs:
def deps do
[
{:ex_saml, "~> 1.0"}
]
endconfig :ex_saml, ExSaml.Provider,
service_providers: [
%{
id: "my_sp",
entity_id: "urn:myapp:sp",
certfile: "path/to/sp.crt",
keyfile: "path/to/sp.key",
# Optional
contact_name: "Admin",
contact_email: "admin@example.com",
org_name: "My Org",
org_displayname: "My Organization",
org_url: "https://example.com"
}
]You can also provide cert and key directly instead of file paths.
config :ex_saml, ExSaml.Provider,
identity_providers: [
%{
id: "my_idp",
sp_id: "my_sp",
base_url: "https://myapp.example.com",
metadata_file: "path/to/idp_metadata.xml",
# Or inline: metadata: "<EntityDescriptor ...>",
nameid_format: :email,
sign_requests: true,
sign_metadata: true,
signed_assertion_in_resp: true,
signed_envelopes_in_resp: false,
allow_idp_initiated_flow: true,
use_redirect_for_req: false,
use_redirect_for_slo: false,
allowed_target_urls: ["https://myapp.example.com/dashboard"]
}
],
idp_id_from: :path_segment # or :subdomainSupported nameid_format values: :email, :x509, :windows, :krb, :persistent, :transient.
Choose where authenticated assertions are stored:
# ETS (default)
config :ex_saml, ExSaml.State,
store: ExSaml.State.ETS
# Plug Session
config :ex_saml, ExSaml.State,
store: ExSaml.State.Session
# Nebulex Cache
config :ex_saml, ExSaml.State,
store: ExSaml.State.CacheConfigure the Nebulex cache module used for assertions and relay state:
config :ex_saml, cache: MyApp.CacheFor loading providers from a database at runtime:
config :ex_saml,
service_providers_accessor: &MyApp.Saml.service_providers/0,
identity_providers_accessor: &MyApp.Saml.identity_providers/0Add the provider to your application's supervision tree:
children = [
ExSaml.Provider
]Forward SAML routes in your Phoenix router:
forward "/sso", ExSaml.RouterThis exposes:
POST /sso/auth/signin/:idp_id- Initiate sign-inPOST /sso/auth/signout/:idp_id- Initiate sign-outPOST /sso/csp-report- CSP violation report endpoint
SP endpoints (metadata, ACS, SLO) are configured via ExSaml.Helper URI builders and handled by ExSaml.SPHandler.
ExSaml.AuthHandler.request_idp(conn, idp_id)ExSaml.AuthHandler.send_signin_req(conn)ExSaml.AuthHandler.send_signout_req(conn)assertion = ExSaml.get_active_assertion(conn)To get a specific attribute:
email = ExSaml.get_attribute(assertion, "email")The ExSaml.Assertion struct contains:
idp_id- Identity Provider identifiersubject- User identity (name,in_response_to,notonorafter)issuer- IdP entity IDattributes- IdP-provided attributescomputed- Locally computed attributesconditions/authn- Additional SAML metadata
Request
|
v
ExSaml.Router
|-- /auth/* -> ExSaml.AuthRouter -> ExSaml.AuthHandler
|-- /csp-report -> ExSaml.CsprRouter
|
v
ExSaml.SecurityPlug (CSP nonce, security headers)
|
v
ExSaml.Provider (GenServer managing SP/IdP state)
|
v
ExSaml.SPHandler (metadata, ACS, SLO)
|
v
ExSaml.State (assertion storage: ETS | Session | Cache)
ExSaml includes hardened defaults for SAML processing:
- XXE protection — All XML parsing uses
allow_entities: false(CVE-2026-28809) - NotBefore validation — Assertions are rejected if issued in the future (with 5s clock skew tolerance)
- Algorithm whitelist — Unknown signature algorithms return a clean error instead of crashing; RSA-SHA1 is rejected
- Namespace-conformant parsing — All
xmerl_scancalls enforcenamespace_conformant: true
To report a security issue, email security@cryptr.co.
If you're coming from Samly or the Dropbox fork, see the Migration Guide for a step-by-step walkthrough covering module renaming, config changes, removed features, and a migration checklist.
- Security Plug - Centralized security headers with CSP nonce support
- Configurable cache backend - Cache module set via
config.exsinstead of hardcoded - Nonce validation - Cryptographic nonce generated and validated during auth flow
- Relay state anti-replay -
RelayStateCache.take/1atomically reads and deletes relay state - Streamlined routing - Removed unused routes, simplified session handling
Full documentation is available on HexDocs.
See LICENSE for details.