ExSaml
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.
Features
- 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
Installation
Add ex_saml to your dependencies in mix.exs:
def deps do
[
{:ex_saml, "~> 1.0"}
]
endConfiguration
Service Provider
config :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.
Identity Provider
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 :subdomain
Supported nameid_format values: :email, :x509, :windows, :krb, :persistent, :transient.
State Store
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.CacheCache
Configure the Nebulex cache module used for assertions and relay state:
config :ex_saml, cache: MyApp.CacheDynamic Provider Loading
For 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/0Setup
Supervision Tree
Add the provider to your application's supervision tree:
children = [
ExSaml.Provider
]Router
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.
Usage
Requesting an IdP Directly
ExSaml.AuthHandler.request_idp(conn, idp_id)Initiating Sign-In
ExSaml.AuthHandler.send_signin_req(conn)Initiating Sign-Out
ExSaml.AuthHandler.send_signout_req(conn)Retrieving the Active Assertion
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
Architecture
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)Migrating from Samly
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.
Key Differences
- 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
Documentation
Full documentation is available on HexDocs.
License
See LICENSE for details.