-
Notifications
You must be signed in to change notification settings - Fork 2
fix: bind the assertion to the request this SP issued #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: fix/assertion-conditions
Are you sure you want to change the base?
Changes from all commits
a9fa958
d59bd4a
c2edc13
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -158,13 +158,13 @@ local AUTHN_REQUEST = [[ | |
| </samlp:AuthnRequest> | ||
| ]] | ||
|
|
||
| local function authn_request(opts) | ||
| local function authn_request(opts, request_id) | ||
| return interp(AUTHN_REQUEST, { | ||
| acs_url = sp_acs_url(opts), | ||
| destination = opts.idp_uri, | ||
| issue_instant = os.date("!%Y-%m-%dT%TZ"), | ||
| issuer = opts.sp_issuer, | ||
| uuid = generate_saml_id(), | ||
| uuid = request_id, | ||
| auth_protocol_binding_method = opts.auth_protocol_binding_method, | ||
| }) | ||
| end | ||
|
|
@@ -214,13 +214,17 @@ local function login(self, opts) | |
|
|
||
| local state = uuid.generate_v4() | ||
| local request_uri = ngx.var.request_uri | ||
| -- kept so the callback can tell the answer to this request from the answer | ||
| -- to some other one | ||
| local request_id = generate_saml_id() | ||
|
|
||
| sess:set("saml_state", state) | ||
| sess:set("saml_request_id", request_id) | ||
| sess:set("request_uri", request_uri) | ||
| sess:save() | ||
|
|
||
| local query_str, err = create_redirect(self.sign_key, { | ||
| SAMLRequest = authn_request(opts), | ||
| SAMLRequest = authn_request(opts, request_id), | ||
| SigAlg = RSA_SHA_512_HREF, | ||
| RelayState = state, | ||
| }) | ||
|
|
@@ -332,8 +336,11 @@ end | |
| -- The assertion may be presented to whoever the Recipient names, for as long as | ||
| -- the confirmation data allows. Several confirmations can be offered and any one | ||
| -- of them being satisfiable is enough. | ||
| local function confirmation_ok(confirmation, acs_url, now, skew) | ||
| if confirmation.recipient and confirmation.recipient ~= acs_url then | ||
| local function confirmation_ok(confirmation, expected, now, skew) | ||
| if confirmation.recipient and confirmation.recipient ~= expected.acs_url then | ||
| return false | ||
| end | ||
| if confirmation.in_response_to and confirmation.in_response_to ~= expected.request_id then | ||
| return false | ||
| end | ||
| return (time_bounds_ok(confirmation.not_before, confirmation.not_on_or_after, now, skew)) | ||
|
|
@@ -342,7 +349,7 @@ end | |
|
|
||
| -- Every top-level assertion the verified signature left in the document is one | ||
| -- the readers draw identity from, so every one of them has to hold up. | ||
| local function assertions_acceptable(opts, assertions, acs_url, now) | ||
| local function assertions_acceptable(opts, assertions, expected, now) | ||
| local skew = opts.clock_skew or DEFAULT_CLOCK_SKEW | ||
| local accepted = opts.sp_audiences or { opts.sp_issuer } | ||
|
|
||
|
|
@@ -373,7 +380,7 @@ local function assertions_acceptable(opts, assertions, acs_url, now) | |
| if #confirmations > 0 then | ||
| local satisfiable = false | ||
| for _, confirmation in ipairs(confirmations) do | ||
| if confirmation_ok(confirmation, acs_url, now, skew) then | ||
| if confirmation_ok(confirmation, expected, now, skew) then | ||
| satisfiable = true | ||
| break | ||
| end | ||
|
|
@@ -425,10 +432,21 @@ local function login_callback(self, opts) | |
| ngx.exit(ngx.HTTP_UNAUTHORIZED) | ||
| end | ||
|
|
||
| local acs_url = sp_acs_url(opts) | ||
| local expected = { | ||
| acs_url = sp_acs_url(opts), | ||
| request_id = sess:get("saml_request_id"), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Failing closed is the right instinct in general, but a nil Either way this is user-visible, and #43 is the only one of the three that does not touch the README — the new |
||
| } | ||
|
|
||
| -- the Response is often left unsigned, so this only catches a stray answer; | ||
| -- the binding that holds is the one inside the signed assertion below | ||
| local in_response_to = saml.doc_in_response_to(doc) | ||
| if in_response_to and in_response_to ~= expected.request_id then | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Neither The outer check here reads an attribute on the The inner one at line 334 is the one meant to hold, but it is the same I ran the combination through your harness: an IdP-signed assertion whose TEST 18 and 19 both feed a wrong ID, which is the easy half; there is no test for a removed one. If this is meant to be a binding, it needs to be "at least one of the two was present and matched" — ideally opt-in so IdPs that genuinely do not send it keep working. Worth noting this is also reachable via the empty- |
||
| ngx.log(ngx.ERR, "response from IdP answers request ", in_response_to) | ||
| ngx.exit(ngx.HTTP_UNAUTHORIZED) | ||
| end | ||
|
|
||
| local destination = saml.doc_destination(doc) | ||
| if destination and destination ~= acs_url then | ||
| if destination and destination ~= expected.acs_url then | ||
| ngx.log(ngx.ERR, "response from IdP is addressed to ", destination) | ||
| ngx.exit(ngx.HTTP_UNAUTHORIZED) | ||
| end | ||
|
|
@@ -439,7 +457,7 @@ local function login_callback(self, opts) | |
| ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) | ||
| end | ||
|
|
||
| local acceptable, reason = assertions_acceptable(opts, assertions, acs_url, ngx.time()) | ||
| local acceptable, reason = assertions_acceptable(opts, assertions, expected, ngx.time()) | ||
| if not acceptable then | ||
| ngx.log(ngx.ERR, "response from IdP rejected: ", reason) | ||
| ngx.exit(ngx.HTTP_UNAUTHORIZED) | ||
|
|
@@ -478,6 +496,7 @@ local function login_callback(self, opts) | |
|
|
||
| -- clear temporary authentication state no longer needed after successful login | ||
| sess:set("saml_state", nil) | ||
| sess:set("saml_request_id", nil) | ||
| sess:set("request_uri", nil) | ||
| sess:save() | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor and pre-existing, but this PR is what makes it load-bearing.
uuid.seed()runs at module scope (line 3), and jit-uuid seeds withngx.time() + ngx.worker.pid(). Ifresty.samlis first required frominit_by_lua, that is the master's pid and the forked workers all inherit the same PRNG state, so every worker emits the same UUID sequence.Until now that only affected
saml_state; now it is also the request ID theInResponseTochecks pin against. Worth a README note that the module has to be required — oruuid.seed()called — frominit_worker_by_lua.