Skip to content

Adding support of private_key_jwt for authentication to OP #217

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

Merged
merged 3 commits into from
Nov 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"],
-- o If token_endpoint_auth_method is set to "client_secret_basic" or "client_secret_post", 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" 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
26 changes: 20 additions & 6 deletions lib/resty/openidc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ local WARN = ngx.WARN

local supported_token_auth_methods = {
client_secret_basic = true,
client_secret_post = true
client_secret_post = true,
private_key_jwt = true
}

local openidc = {
Expand Down Expand Up @@ -405,13 +406,26 @@ 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" then
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 = "RS256", kid = opts.client_rsa_private_key_id}
local assertion_payload = {sub = opts.client_id, iss = 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}

local r_jwt = require("resty.jwt")
body.client_assertion=r_jwt:sign(opts.client_rsa_private_key, { header = assertion_header, payload = assertion_payload })
log(DEBUG, "private_key_jwt: client_id, client_assertion_type and client_assertion being sent in POST body")
end
end

Expand Down Expand Up @@ -864,7 +878,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 @@ -878,7 +892,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 @@ -926,7 +940,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
18 changes: 18 additions & 0 deletions tests/spec/token_request_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,24 @@ 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("if token endpoint is not resolvable", function()
test_support.start_server({
oidc_opts = {
Expand Down