Skip to content

Commit

Permalink
release 1.7.1
Browse files Browse the repository at this point in the history
Signed-off-by: Hans Zandbelt <[email protected]>
  • Loading branch information
zandbelt committed Feb 18, 2019
2 parents c06e6fd + d7bd9a2 commit 1ead99c
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 16 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ reporting bugs, providing fixes, suggesting useful features or other:
Dmitriy Blok <https://github.com/dmitriyblok>
Oleander Reis <https://github.com/oleeander>
Michael Johansen <https://github.com/mijohansen>
Joshua Erney <https://github.com/JoshTheGoldfish>
16 changes: 16 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
02/18/2019
- release 1.7.1

12/17/2018
- don't select one of the jwt token auth methods if the required key
material is not present; see #238

11/13/2018
- fixed a bad error return value in certain setups of
bearer_jwt_verify; see #234; thanks @JoshTheGoldfish

11/09/2018
- added support for the client_secret_jwt authentication method; see #229

11/08/2018
- added support for the private_key_jwt authentication method; see
#217; thanks @pamiel
>>>>>>> branch 'master' of https://github.com/zmartzone/lua-resty-openidc.git

11/06/2018
- make sure opts.discovery is resolved when "iss" is returned as part of the
authorization response; see #224 ; thanks @mijohansen
Expand Down
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,17 +112,37 @@ http {
-- if the URI starts with a / the full redirect URI becomes
-- ngx.var.scheme.."://"..ngx.var.http_host..opts.redirect_uri
-- unless the scheme was overridden using opts.redirect_uri_scheme or an X-Forwarded-Proto header in the incoming request
redirect_uri = "https://MY_HOST_NAME/redirect_uri"
redirect_uri = "https://MY_HOST_NAME/redirect_uri",
-- up until version 1.6.1 you'd specify
-- redirect_uri_path = "/redirect_uri",
-- and could not set the hostname
-- The discovery endpoint of the OP. Enable to get the URI of all endpoints (Token, introspection, logout...)
discovery = "https://accounts.google.com/.well-known/openid-configuration",
-- For non compliant OPs to OAuth 2.0 RFC 6749 for client Authentication (cf. https://tools.ietf.org/html/rfc6749#section-2.3.1)
-- client_id and client_secret MUST be invariant when url encoded
-- Access to OP Token endpoint requires an authentication. Several authentication modes are supported:
--token_endpoint_auth_method = ["client_secret_basic"|"client_secret_post"|"private_key_jwt"|"client_secret_jwt"],
-- o If token_endpoint_auth_method is set to "client_secret_basic", "client_secret_post", or "client_secret_jwt", authentication to Token endpoint is using client_id and client_secret
-- For non compliant OPs to OAuth 2.0 RFC 6749 for client Authentication (cf. https://tools.ietf.org/html/rfc6749#section-2.3.1)
-- client_id and client_secret MUST be invariant when url encoded
client_id = "<client_id>",
client_secret = "<client_secret>",
-- o If token_endpoint_auth_method is set to "private_key_jwt" authentication to Token endpoint is using client_id, client_rsa_private_key and client_rsa_private_key_id to compute a signed JWT
-- client_rsa_private_key is the RSA private key to be used to sign the JWT generated by lua-resty-openidc for authentication to the OP
-- client_rsa_private_key_id (optional) is the key id to be set in the JWT header to identify which public key the OP shall use to verify the JWT signature
--client_id = "<client_id>",
--client_rsa_private_key=[[-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAiThmpvXBYdur716D2q7fYKirKxzZIU5QrkBGDvUOwg5izcTv
[...]
h2JHukolz9xf6qN61QMLSd83+kwoBr2drp6xg3eGDLIkQCQLrkY=
-----END RSA PRIVATE KEY-----]],
--client_rsa_private_key_id="key id#1",
-- Life duration expressed in seconds of the signed JWT generated by lua-resty-openidc for authentication to the OP.
-- (used when token_endpoint_auth_method is set to "private_key_jwt" or "client_secret_jwt" authentication). Default is 60 seconds.
--client_jwt_assertion_expires_in = 60,
-- When using https to any OP endpoints, enforcement of SSL certificate check can be mandated ("yes") or not ("no").
--ssl_verify = "no",
--authorization_params = { hd="zmartzone.eu" },
--scope = "openid email profile",
-- Refresh the users id_token after 900 seconds without requiring re-authentication
Expand All @@ -136,8 +156,6 @@ http {
-- Whether the redirection after logout should include the id token as an hint (if available). This option is used only if redirect_after_logout_uri is set.
--post_logout_redirect_uri = "https://www.zmartzone.eu/logoutSuccessful",
-- Where does the RP requests that the OP redirects the user after logout. If this option is set to a relative URI, it will be relative to the OP's logout endpoint, not the RP's.
--token_endpoint_auth_method = ["client_secret_basic"|"client_secret_post"],
--ssl_verify = "no"
--accept_none_alg = false
-- if your OpenID Connect Provider doesn't sign its id tokens
Expand Down
65 changes: 56 additions & 9 deletions lib/resty/openidc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,21 @@ local DEBUG = ngx.DEBUG
local ERROR = ngx.ERR
local WARN = ngx.WARN

local function token_auth_method_precondition(method, required_field)
return function(opts)
if not opts[required_field] then
log(DEBUG, "Can't use " .. method .. " without opts." .. required_field)
return false
end
return true
end
end

local supported_token_auth_methods = {
client_secret_basic = true,
client_secret_post = true
client_secret_post = true,
private_key_jwt = token_auth_method_precondition('private_key_jwt', 'client_rsa_private_key'),
client_secret_jwt = token_auth_method_precondition('client_secret_jwt', 'client_secret')
}

local openidc = {
Expand Down Expand Up @@ -405,13 +417,43 @@ function openidc.call_token_endpoint(opts, endpoint, body, auth, endpoint_name,
headers.Authorization = "Basic " .. b64(ngx.escape_uri(opts.client_id) .. ":")
end
log(DEBUG, "client_secret_basic: authorization header '" .. headers.Authorization .. "'")
end
if auth == "client_secret_post" then

elseif auth == "client_secret_post" then
body.client_id = opts.client_id
if opts.client_secret then
body.client_secret = opts.client_secret
end
log(DEBUG, "client_secret_post: client_id and client_secret being sent in POST body")

elseif auth == "private_key_jwt" or auth == "client_secret_jwt" then
local key = auth == "private_key_jwt" and opts.client_rsa_private_key or opts.client_secret
if not key then
return nil, "Can't use " .. auth .. " without a key."
end
body.client_id = opts.client_id
body.client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
local now = ngx.time()
local assertion = {
header = {
typ = "JWT",
alg = auth == "private_key_jwt" and "RS256" or "HS256",
},
payload = {
iss = opts.client_id,
sub = opts.client_id,
aud = endpoint,
jti = ngx.var.request_id,
exp = now + (opts.client_jwt_assertion_expires_in and opts.client_jwt_assertion_expires_in or 60),
iat = now
}
}
if auth == "private_key_jwt" then
assertion.header.kid = opts.client_rsa_private_key_id
end

local r_jwt = require("resty.jwt")
body.client_assertion = r_jwt:sign(key, assertion)
log(DEBUG, auth .. ": client_id, client_assertion_type and client_assertion being sent in POST body")
end
end

Expand Down Expand Up @@ -550,10 +592,15 @@ local function openidc_ensure_discovered_data(opts)
return err
end

local function can_use_token_auth_method(method, opts)
local supported = supported_token_auth_methods[method]
return supported and (type(supported) ~= 'function' or supported(opts))
end

-- get the token endpoint authentication method
local function openidc_get_token_auth_method(opts)

if opts.token_endpoint_auth_method ~= nil and not supported_token_auth_methods[opts.token_endpoint_auth_method] then
if opts.token_endpoint_auth_method ~= nil and not can_use_token_auth_method(opts.token_endpoint_auth_method, opts) then
log(ERROR, "configured value for token_endpoint_auth_method (" .. opts.token_endpoint_auth_method .. ") is not supported, ignoring it")
opts.token_endpoint_auth_method = nil
end
Expand All @@ -577,7 +624,7 @@ local function openidc_get_token_auth_method(opts)
else
for index, value in ipairs(opts.discovery.token_endpoint_auth_methods_supported) do
log(DEBUG, index .. " => " .. value)
if supported_token_auth_methods[value] then
if can_use_token_auth_method(value, opts) then
result = value
log(DEBUG, "no configuration setting for option so select the first supported method specified by the OP: " .. result)
break
Expand Down Expand Up @@ -858,7 +905,7 @@ end
-- parse a JWT and verify its signature (if present)
local function openidc_load_jwt_and_verify_crypto(opts, jwt_string, asymmetric_secret,
symmetric_secret, expected_algs, ...)
local jwt = require("resty.jwt")
local r_jwt = require("resty.jwt")
local enc_hdr, enc_payload, enc_sign = string.match(jwt_string, '^(.+)%.(.+)%.(.*)$')
if enc_payload and (not enc_sign or enc_sign == "") then
local jwt = openidc_load_jwt_none_alg(enc_hdr, enc_payload)
Expand All @@ -872,7 +919,7 @@ symmetric_secret, expected_algs, ...)
end -- otherwise the JWT is invalid and load_jwt produces an error
end

local jwt_obj = jwt:load_jwt(jwt_string, nil)
local jwt_obj = r_jwt:load_jwt(jwt_string, nil)
if not jwt_obj.valid then
local reason = "invalid jwt"
if jwt_obj.reason then
Expand Down Expand Up @@ -920,7 +967,7 @@ symmetric_secret, expected_algs, ...)
jwt_validators.set_system_leeway(opts.iat_slack and opts.iat_slack or 120)
end

jwt_obj = jwt:verify_jwt_obj(secret, jwt_obj, ...)
jwt_obj = r_jwt:verify_jwt_obj(secret, jwt_obj, ...)
if jwt_obj then
log(DEBUG, "jwt: ", cjson.encode(jwt_obj), " ,valid: ", jwt_obj.valid, ", verified: ", jwt_obj.verified)
end
Expand Down Expand Up @@ -1410,7 +1457,7 @@ local function openidc_get_bearer_access_token_from_cookie(opts)

local accept_token_as = opts.auth_accept_token_as or "header"
if accept_token_as:find("cookie") ~= 1 then
return nul, "openidc_get_bearer_access_token_from_cookie called but auth_accept_token_as wants "
return nil, "openidc_get_bearer_access_token_from_cookie called but auth_accept_token_as wants "
.. opts.auth_accept_token_as
end
local divider = accept_token_as:find(':')
Expand Down
135 changes: 134 additions & 1 deletion tests/spec/token_request_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@ describe("when the token endpoint is invoked", function()
it("the request contains the client_secret parameter", function()
assert_token_endpoint_call_contains("client_secret=client_secret")
end)
it("the request doesn't contain a basic auth header", function()
it("the request doesn't contain any basic auth header", function()
assert.is_not.error_log_contains("token authorization header: Basic")
end)
it("the request doesn't contain any client_assertion_type parameter", function()
assert.is_not.error_log_contains("request body for token endpoint call: .*client_assertion_type=")
end)
it("the request doesn't contain any client_assertion parameter", function()
assert.is_not.error_log_contains("request body for token endpoint call: .*client_assertion=.*")
end)
end)

describe("when the token endpoint is invoked using client_secret_basic", function()
Expand All @@ -51,6 +57,12 @@ describe("when the token endpoint is invoked using client_secret_basic", functio
it("the request contains a basic auth header", function()
assert.error_log_contains("token authorization header: Basic")
end)
it("the request doesn't contain any client_assertion_type parameter", function()
assert.is_not.error_log_contains("request body for token endpoint call: .*client_assertion_type=")
end)
it("the request doesn't contain any client_assertion parameter", function()
assert.is_not.error_log_contains("request body for token endpoint call: .*client_assertion=.*")
end)
end)

describe("when no explicit auth method is configured #96", function()
Expand Down Expand Up @@ -84,6 +96,40 @@ describe("when an explicit auth method is configured", function()
end)
end)

describe("when 'private_key_jwt' auth method is configured", function()
test_support.start_server({
oidc_opts = {
discovery = {
token_endpoint_auth_methods_supported = { "client_secret_basic", "client_secret_post", "private_key_jwt" },
},
token_endpoint_auth_method = "private_key_jwt",
client_rsa_private_key = test_support.load("/spec/private_rsa_key.pem")
}
})
teardown(test_support.stop_server)
test_support.login()
it("then it is used", function()
assert_token_endpoint_call_contains("client_assertion=ey") -- check only beginning of the assertion as it changes each time
assert_token_endpoint_call_contains("client_assertion_type=urn%%3Aietf%%3Aparams%%3Aoauth%%3Aclient%-assertion%-type%%3Ajwt%-bearer")
end)
end)

describe("when 'private_key_jwt' auth method is configured but no key specified", function()
test_support.start_server({
oidc_opts = {
discovery = {
token_endpoint_auth_methods_supported = { "client_secret_basic", "client_secret_post", "private_key_jwt" },
},
token_endpoint_auth_method = "private_key_jwt",
}
})
teardown(test_support.stop_server)
test_support.login()
it("then it is not used", function()
assert.error_log_contains("token authorization header: Basic")
end)
end)

describe("if token endpoint is not resolvable", function()
test_support.start_server({
oidc_opts = {
Expand Down Expand Up @@ -213,3 +259,90 @@ describe("when a request_decorator has been specified when calling the token end
assert_token_endpoint_call_contains("foo=bar")
end)
end)

local function extract_jwt_from_error_log()
local log = test_support.load("/tmp/server/logs/error.log")
local encoded_jwt = log:match("request body for token endpoint call: .*client_assertion=([^\n&]+)")
local enc_hdr, enc_payload, enc_sign = string.match(encoded_jwt, '^(.+)%.(.+)%.(.*)$')
local base64_url_decode = function(s)
local mime = require "mime"
return mime.unb64(s:gsub('-','+'):gsub('_','/'))
end
local dkjson = require "dkjson"
return {
header = dkjson.decode(base64_url_decode(enc_hdr), 1, nil),
payload = dkjson.decode(base64_url_decode(enc_payload), 1, nil),
signature = enc_sign
}
end

describe("when the token endpoint is invoked using client_secret_jwt", function()
test_support.start_server({
oidc_opts = {
discovery = {
token_endpoint_auth_methods_supported = { "client_secret_jwt" },
}
}
})
teardown(test_support.stop_server)
test_support.login()
it("the request doesn't contain the client_secret as parameter", function()
assert.is_not.error_log_contains("request body for token endpoint call: .*client_secret=client_secret.*")
end)
it("the request doesn't contain a basic auth header", function()
assert.is_not.error_log_contains("token authorization header: Basic")
end)
it("the request contains the proper client_assertion_type parameter", function()
-- url.escape escapes the "-" while openidc doesn't so we must revert the encoding for comparison
local at = test_support.urlescape_for_regex("urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
:gsub("%%%%2d", "%%-")
assert.error_log_contains("request body for token endpoint call: .*client_assertion_type="..at..".*", true)
end)
it("the request contains a client_assertion parameter", function()
assert.error_log_contains("request body for token endpoint call: .*client_assertion=.*")
end)
describe("then the submitted JWT", function()
local jwt = extract_jwt_from_error_log()
it("has a proper HMAC header", function()
assert.are.equal("JWT", jwt.header.typ)
assert.are.equal("HS256", jwt.header.alg)
end)
it("is signed", function()
assert.truthy(jwt.signature)
end)
it("contains the client_id as iss claim", function()
assert.are.equal("client_id", jwt.payload.iss)
end)
it("contains the client_id as sub claim", function()
assert.are.equal("client_id", jwt.payload.sub)
end)
it("contains the token endpoint as aud claim", function()
assert.are.equal("http://127.0.0.1/token", jwt.payload.aud)
end)
it("contains a jti claim", function()
assert.truthy(jwt.payload.jti)
end)
it("contains a non-expired exp claim", function()
assert.truthy(jwt.payload.exp)
assert.is_true(jwt.payload.exp > os.time())
end)
end)
end)

describe("when 'client_secret_jwt' auth method is configured but no key specified", function()
test_support.start_server({
oidc_opts = {
discovery = {
token_endpoint_auth_methods_supported = { "client_secret_basic", "client_secret_post", "client_secret_jwt" },
},
token_endpoint_auth_method = "client_secret_jwt",
},
remove_oidc_config_keys = { "client_secret" }
})
teardown(test_support.stop_server)
test_support.login()
it("then it is not used", function()
assert.error_log_contains("token authorization header: Basic")
end)
end)

0 comments on commit 1ead99c

Please sign in to comment.