Access SAML (Security Assertion Markup Language 2.0) IdP (Identity Provider) to do authentication via cosocket, from the SP (service provider) perspective.
This project is based on player-two/saml.
Supported Protocols:
- Authentication Request Protocol
- Single Logout Protocol
Bindings:
- HTTP Redirect Binding
- HTTP POST Binding
Features not supported:
- SAML encryption
- IdP discovery
- Back-channel logout
The preferred way to install this library is to use Luarocks:
luarocks install lua-resty-saml
local resty_saml = require "resty.saml"
local opts = {
sp_issuer = "sp",
idp_uri = "http://127.0.0.1:8080/realms/test/protocol/saml",
idp_cert = "xxx",
login_callback_uri = "/acs",
logout_uri = "/logout",
logout_callback_uri = "/sls",
logout_redirect_uri = "/",
sp_cert = "xxx",
sp_private_key = "xxx",
}
local saml = resty_saml.new(opts)
local data = saml:authenticate()To load this module:
local resty_saml = require "resty.saml"
local saml = resty_saml.new(opts)
opts is a table of below items:
| key | type | default value | Description |
|---|---|---|---|
sp_issuer |
string | None | SP name to access IdP. |
idp_uri |
string | None | URI of IdP. |
idp_cert |
string | None | IdP Certificate, used to verify saml response. |
idp_issuers |
array of strings | None | Issuers accepted on a login response; every assertion it carries has to name one. Unset accepts any issuer the idp_cert signs for, which is not the same as an empty list: that one accepts nobody. |
login_callback_uri |
string | None | redirect uri used to callback the SP from IdP after login. |
logout_uri |
string | None | logout uri to trigger logout. |
logout_callback_uri |
string | None | redirect uri used to callback the SP from IdP after logout. |
logout_redirect_uri |
string | None | redirect uri after sucessful logout. |
sp_cert |
string | None | SP Certificate, used to sign the saml request. |
sp_private_key |
string | None | SP private key. |
sp_acs_url |
string | built from the request | Absolute URL of this SP's assertion consumer service. It is announced to the IdP, every SubjectConfirmationData/@Recipient has to name it, and a Destination has to name it on a response carrying one. Unset, it is assembled from the request's scheme and host, which is only as trustworthy as whatever sits in front: set it wherever the ingress does not normalise Forwarded and X-Forwarded-*, or terminates TLS without setting X-Forwarded-Proto. |
sp_audiences |
array of strings | { sp_issuer } |
Audiences this SP answers to. An assertion carrying an AudienceRestriction has to name one of them; an assertion carrying none is unrestricted. |
clock_skew |
number | 60 |
Seconds of clock difference tolerated against the IdP when weighing NotBefore and NotOnOrAfter. |
replay_dict |
string | None | Name of an lua_shared_dict in which to remember the assertions this instance has already accepted, so it accepts none of them twice. Unset leaves them untracked. See Remembering assertions for what the zone has to hold and how far the guarantee reaches. |
replay_ttl |
number | 600 |
Seconds to remember an assertion when nothing bounds its acceptance: no NotOnOrAfter on its Conditions and none on a satisfiable subject confirmation. A bounded one is remembered until acceptance ends, plus clock_skew, capped at a day. |
An ID is minted for every AuthnRequest this SP sends and kept on the session as
saml_request_id. On the way back, a SubjectConfirmationData naming a different
request refuses the login, so an assertion captured from one login cannot be
presented in another. Response/@InResponseTo is weighed the same way,
though what it is worth depends on what the IdP signed: an IdP signing the whole
Response covers it, while one signing only the assertion, the common shape, leaves
it outside the signature, where the party replaying an assertion deletes it. Rely on
the copy inside the assertion, and read this one as catching a misdirected answer.
That guarantee is worth what the IdP sends. Profile 4.1.4.2 asks an IdP answering an
AuthnRequest to name it, and every mainstream IdP does, but an IdP that leaves
InResponseTo out, or sends no SubjectConfirmation at all, keeps working and gets
no binding. Refusing it would trade a working login for protection against another
party's misconfiguration, and no attacker can produce the shape: the value sits
inside the signature, so it cannot be stripped from a captured assertion.
One note for upgrading. A session minted before this SP kept the ID has nothing for
the assertion to name, so the login is started again rather than refused. The window
lasts as long as an AuthnRequest is outstanding across the upgrade.
Set replay_dict and every assertion this instance accepts is remembered for as
long as it could still be used, within the bounds below, and presenting one that is
remembered is refused. Leave it unset and assertions go untracked, which is what
happened before the option existed.
The guarantee is per instance. An lua_shared_dict is shared between the workers
of one gateway and nowhere else, so a captured assertion replayed through a load
balancer lands on a replica that has never seen it and is accepted. Across replicas
the binding in Binding a response to the request
is what carries the weight, since it travels in the user's own session, and this
option is the defence for the deployments that binding leaves uncovered: the ones
whose IdP sends no InResponseTo.
Size the zone for what it holds. One entry per assertion accepted, held for as
long as that assertion could still be used. A response normally carries one, so an SP
taking ten logins a second against an IdP issuing ten-minute assertions holds around
six thousand entries at once: 1m is too small for that and a busy deployment wants
more. A zone with no room leaves that assertion untracked and logs an error naming
the assertion and the zone, rather than evicting an entry that is still protecting
somebody else. A response carrying several assertions can end up partly tracked,
which is the safe direction: a later replay still collides on whichever of them was
recorded.
The record is bounded even where acceptance is not. An assertion with no usable
expiry is remembered for replay_ttl and accepted for good, so it is refusable only
inside that window; one the IdP made valid beyond a day is remembered for the day
and accepted again past it. Both need an IdP far outside shipped defaults, where
the delivery window is minutes and the assertion window at most an hour, and the
alternative is a record nothing reclaims. The limit an operator can move is
replay_ttl; the day cap is fixed.
Two things it deliberately does not do. An assertion carrying <saml:OneTimeUse/>
is still refused outright, so an IdP asking for exactly this protection cannot log in
even with the option on; that is tracked separately and the two do not meet yet. And
re-submitting a response that already logged in is refused, which is what a browser
does when it loses the redirect that ends a login. Returning to the application starts
a fresh login, and the IdP will not ask for a password again.
Request IDs and RelayState both come from resty.jit-uuid, which is seeded when
this module is first loaded, from the clock and the process ID. Loading resty.saml
from init_by_lua therefore seeds once in the master, and every worker forked after
it inherits that same sequence. Require this module from init_worker_by_lua, or
call uuid.seed() there yourself.
syntax: data = saml:authenticate()