diff options
Diffstat (limited to 'server/resty/openidc.lua')
-rw-r--r-- | server/resty/openidc.lua | 1870 |
1 files changed, 0 insertions, 1870 deletions
diff --git a/server/resty/openidc.lua b/server/resty/openidc.lua deleted file mode 100644 index 246414e..0000000 --- a/server/resty/openidc.lua +++ /dev/null @@ -1,1870 +0,0 @@ ---[[ -Licensed to the Apache Software Foundation (ASF) under one -or more contributor license agreements. See the NOTICE file -distributed with this work for additional information -regarding copyright ownership. The ASF licenses this file -to you under the Apache License, Version 2.0 (the -"License"); you may not use this file except in compliance -with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an -"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, either express or implied. See the License for the -specific language governing permissions and limitations -under the License. - -*************************************************************************** -Copyright (C) 2017-2019 ZmartZone IAM -Copyright (C) 2015-2017 Ping Identity Corporation -All rights reserved. - -For further information please contact: - - Ping Identity Corporation - 1099 18th St Suite 2950 - Denver, CO 80202 - 303.468.2900 - http://www.pingidentity.com - -DISCLAIMER OF WARRANTIES: - -THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT -ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING, -WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT, -MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY -WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE -USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET -YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE -WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -@Author: Hans Zandbelt - hans.zandbelt@zmartzone.eu ---]] - -local require = require -local cjson = require("cjson") -local cjson_s = require("cjson.safe") -local http = require("resty.http") -local r_session = require("resty.session") -local string = string -local ipairs = ipairs -local pairs = pairs -local type = type -local ngx = ngx -local b64 = ngx.encode_base64 -local unb64 = ngx.decode_base64 - -local log = ngx.log -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, - 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 = { - _VERSION = "1.7.5" -} -openidc.__index = openidc - -local function store_in_session(opts, feature) - -- We don't have a whitelist of features to enable - if not opts.session_contents then - return true - end - - return opts.session_contents[feature] -end - --- set value in server-wide cache if available -local function openidc_cache_set(type, key, value, exp) - local dict = ngx.shared[type] - if dict and (exp > 0) then - local success, err, forcible = dict:set(key, value, exp) - log(DEBUG, "cache set: success=", success, " err=", err, " forcible=", forcible) - end -end - --- retrieve value from server-wide cache if available -local function openidc_cache_get(type, key) - local dict = ngx.shared[type] - local value - if dict then - value = dict:get(key) - if value then log(DEBUG, "cache hit: type=", type, " key=", key) end - end - return value -end - --- invalidate values of server-wide cache -local function openidc_cache_invalidate(type) - local dict = ngx.shared[type] - if dict then - log(DEBUG, "flushing cache for " .. type) - dict.flush_all(dict) - local nbr = dict.flush_expired(dict) - end -end - --- invalidate all server-wide caches -function openidc.invalidate_caches() - openidc_cache_invalidate("discovery") - openidc_cache_invalidate("jwks") - openidc_cache_invalidate("introspection") - openidc_cache_invalidate("jwt_verification") -end - --- validate the contents of and id_token -local function openidc_validate_id_token(opts, id_token, nonce) - - -- check issuer - if opts.discovery.issuer ~= id_token.iss then - log(ERROR, "issuer \"", id_token.iss, "\" in id_token is not equal to the issuer from the discovery document \"", opts.discovery.issuer, "\"") - return false - end - - -- check sub - if not id_token.sub then - log(ERROR, "no \"sub\" claim found in id_token") - return false - end - - -- check nonce - if nonce and nonce ~= id_token.nonce then - log(ERROR, "nonce \"", id_token.nonce, "\" in id_token is not equal to the nonce that was sent in the request \"", nonce, "\"") - return false - end - - -- check issued-at timestamp - if not id_token.iat then - log(ERROR, "no \"iat\" claim found in id_token") - return false - end - - local slack = opts.iat_slack and opts.iat_slack or 120 - if id_token.iat > (ngx.time() + slack) then - log(ERROR, "id_token not yet valid: id_token.iat=", id_token.iat, ", ngx.time()=", ngx.time(), ", slack=", slack) - return false - end - - -- check expiry timestamp - if not id_token.exp then - log(ERROR, "no \"exp\" claim found in id_token") - return false - end - - if (id_token.exp + slack) < ngx.time() then - log(ERROR, "token expired: id_token.exp=", id_token.exp, ", ngx.time()=", ngx.time()) - return false - end - - -- check audience (array or string) - if not id_token.aud then - log(ERROR, "no \"aud\" claim found in id_token") - return false - end - - if (type(id_token.aud) == "table") then - for _, value in pairs(id_token.aud) do - if value == opts.client_id then - return true - end - end - log(ERROR, "no match found token audience array: client_id=", opts.client_id) - return false - elseif (type(id_token.aud) == "string") then - if id_token.aud ~= opts.client_id then - log(ERROR, "token audience does not match: id_token.aud=", id_token.aud, ", client_id=", opts.client_id) - return false - end - end - return true -end - -local function get_first(table_or_string) - local res = table_or_string - if table_or_string and type(table_or_string) == 'table' then - res = table_or_string[1] - end - return res -end - -local function get_first_header(headers, header_name) - local header = headers[header_name] - return get_first(header) -end - -local function get_first_header_and_strip_whitespace(headers, header_name) - local header = get_first_header(headers, header_name) - return header and header:gsub('%s', '') -end - -local function get_forwarded_parameter(headers, param_name) - local forwarded = get_first_header(headers, 'Forwarded') - local params = {} - if forwarded then - local function parse_parameter(pv) - local name, value = pv:match("^%s*([^=]+)%s*=%s*(.-)%s*$") - if name and value then - if value:sub(1, 1) == '"' then - value = value:sub(2, -2) - end - params[name:lower()] = value - end - end - - -- this assumes there is no quoted comma inside the header's value - -- which should be fine as comma is not legal inside a node name, - -- a URI scheme or a host name. The only thing that might bite us - -- are extensions. - local first_part = forwarded - local first_comma = forwarded:find("%s*,%s*") - if first_comma then - first_part = forwarded:sub(1, first_comma - 1) - end - first_part:gsub("[^;]+", parse_parameter) - end - return params[param_name:gsub("^%s*(.-)%s*$", "%1"):lower()] -end - -local function get_scheme(headers) - return get_forwarded_parameter(headers, 'proto') - or get_first_header_and_strip_whitespace(headers, 'X-Forwarded-Proto') - or ngx.var.scheme -end - -local function get_host_name_from_x_header(headers) - local header = get_first_header_and_strip_whitespace(headers, 'X-Forwarded-Host') - return header and header:gsub('^([^,]+),?.*$', '%1') -end - -local function get_host_name(headers) - return get_forwarded_parameter(headers, 'host') - or get_host_name_from_x_header(headers) - or ngx.var.http_host -end - --- assemble the redirect_uri -local function openidc_get_redirect_uri(opts, session) - local path = opts.redirect_uri_path - if opts.redirect_uri then - if opts.redirect_uri:sub(1, 1) == '/' then - path = opts.redirect_uri - else - return opts.redirect_uri - end - end - local headers = ngx.req.get_headers() - local scheme = opts.redirect_uri_scheme or get_scheme(headers) - local host = get_host_name(headers) - if not host then - -- possibly HTTP 1.0 and no Host header - if session then session:close() end - ngx.exit(ngx.HTTP_BAD_REQUEST) - end - return scheme .. "://" .. host .. path -end - --- perform base64url decoding -local function openidc_base64_url_decode(input) - local reminder = #input % 4 - if reminder > 0 then - local padlen = 4 - reminder - input = input .. string.rep('=', padlen) - end - input = input:gsub('%-', '+'):gsub('_', '/') - return unb64(input) -end - --- perform base64url encoding -local function openidc_base64_url_encode(input) - local output = b64(input, true) - return output:gsub('%+', '-'):gsub('/', '_') -end - -local function openidc_combine_uri(uri, params) - if params == nil or next(params) == nil then - return uri - end - local sep = "?" - if string.find(uri, "?", 1, true) then - sep = "&" - end - return uri .. sep .. ngx.encode_args(params) -end - -local function decorate_request(http_request_decorator, req) - return http_request_decorator and http_request_decorator(req) or req -end - -local function openidc_s256(verifier) - local sha256 = (require 'resty.sha256'):new() - sha256:update(verifier) - return openidc_base64_url_encode(sha256:final()) -end - --- send the browser of to the OP's authorization endpoint -local function openidc_authorize(opts, session, target_url, prompt) - local resty_random = require("resty.random") - local resty_string = require("resty.string") - local err - - -- generate state and nonce - local state = resty_string.to_hex(resty_random.bytes(16)) - local nonce = (opts.use_nonce == nil or opts.use_nonce) - and resty_string.to_hex(resty_random.bytes(16)) - local code_verifier = opts.use_pkce and openidc_base64_url_encode(resty_random.bytes(32)) - - -- assemble the parameters to the authentication request - local params = { - client_id = opts.client_id, - response_type = "code", - scope = opts.scope and opts.scope or "openid email profile", - redirect_uri = openidc_get_redirect_uri(opts, session), - state = state, - } - - if nonce then - params.nonce = nonce - end - - if prompt then - params.prompt = prompt - end - - if opts.display then - params.display = opts.display - end - - if code_verifier then - params.code_challenge_method = 'S256' - params.code_challenge = openidc_s256(code_verifier) - end - - -- merge any provided extra parameters - if opts.authorization_params then - for k, v in pairs(opts.authorization_params) do params[k] = v end - end - - -- store state in the session - session.data.original_url = target_url - session.data.state = state - session.data.nonce = nonce - session.data.code_verifier = code_verifier - session.data.last_authenticated = ngx.time() - - if opts.lifecycle and opts.lifecycle.on_created then - err = opts.lifecycle.on_created(session) - if err then - log(WARN, "failed in `on_created` handler: " .. err) - return err - end - end - - session:save() - - -- redirect to the /authorization endpoint - ngx.header["Cache-Control"] = "no-cache, no-store, max-age=0" - return ngx.redirect(openidc_combine_uri(opts.discovery.authorization_endpoint, params)) -end - --- parse the JSON result from a call to the OP -local function openidc_parse_json_response(response, ignore_body_on_success) - local ignore_body_on_success = ignore_body_on_success or false - - local err - local res - - -- check the response from the OP - if response.status ~= 200 then - err = "response indicates failure, status=" .. response.status .. ", body=" .. response.body - else - if ignore_body_on_success then - return nil, nil - end - - -- decode the response and extract the JSON object - res = cjson_s.decode(response.body) - - if not res then - err = "JSON decoding failed" - end - end - - return res, err -end - -local function openidc_configure_timeouts(httpc, timeout) - if timeout then - if type(timeout) == "table" then - local r, e = httpc:set_timeouts(timeout.connect or 0, timeout.send or 0, timeout.read or 0) - else - local r, e = httpc:set_timeout(timeout) - end - end -end - --- Set outgoing proxy options -local function openidc_configure_proxy(httpc, proxy_opts) - if httpc and proxy_opts and type(proxy_opts) == "table" then - log(DEBUG, "openidc_configure_proxy : use http proxy") - httpc:set_proxy_options(proxy_opts) - else - log(DEBUG, "openidc_configure_proxy : don't use http proxy") - end -end - --- make a call to the token endpoint -function openidc.call_token_endpoint(opts, endpoint, body, auth, endpoint_name, ignore_body_on_success) - local ignore_body_on_success = ignore_body_on_success or false - - local ep_name = endpoint_name or 'token' - if not endpoint then - return nil, 'no endpoint URI for ' .. ep_name - end - - local headers = { - ["Content-Type"] = "application/x-www-form-urlencoded" - } - - if auth then - if auth == "client_secret_basic" then - if opts.client_secret then - headers.Authorization = "Basic " .. b64(ngx.escape_uri(opts.client_id) .. ":" .. ngx.escape_uri(opts.client_secret)) - else - -- client_secret must not be set if Windows Integrated Authentication (WIA) is used with - -- Active Directory Federation Services (AD FS) 4.0 (or newer) on Windows Server 2016 (or newer) - headers.Authorization = "Basic " .. b64(ngx.escape_uri(opts.client_id) .. ":") - end - log(DEBUG, "client_secret_basic: authorization header '" .. headers.Authorization .. "'") - - 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 - - local pass_cookies = opts.pass_cookies - if pass_cookies then - if ngx.req.get_headers()["Cookie"] then - local t = {} - for cookie_name in string.gmatch(pass_cookies, "%S+") do - local cookie_value = ngx.var["cookie_" .. cookie_name] - if cookie_value then - table.insert(t, cookie_name .. "=" .. cookie_value) - end - end - headers.Cookie = table.concat(t, "; ") - end - end - - log(DEBUG, "request body for " .. ep_name .. " endpoint call: ", ngx.encode_args(body)) - - local httpc = http.new() - openidc_configure_timeouts(httpc, opts.timeout) - openidc_configure_proxy(httpc, opts.proxy_opts) - local res, err = httpc:request_uri(endpoint, decorate_request(opts.http_request_decorator, { - method = "POST", - body = ngx.encode_args(body), - headers = headers, - ssl_verify = (opts.ssl_verify ~= "no"), - keepalive = (opts.keepalive ~= "no") - })) - if not res then - err = "accessing " .. ep_name .. " endpoint (" .. endpoint .. ") failed: " .. err - log(ERROR, err) - return nil, err - end - - log(DEBUG, ep_name .. " endpoint response: ", res.body) - - return openidc_parse_json_response(res, ignore_body_on_success) -end - --- computes access_token expires_in value (in seconds) -local function openidc_access_token_expires_in(opts, expires_in) - return (expires_in or opts.access_token_expires_in or 3600) - 1 - (opts.access_token_expires_leeway or 0) -end - -local function openidc_load_jwt_none_alg(enc_hdr, enc_payload) - local header = cjson_s.decode(openidc_base64_url_decode(enc_hdr)) - local payload = cjson_s.decode(openidc_base64_url_decode(enc_payload)) - if header and payload and header.alg == "none" then - return { - raw_header = enc_hdr, - raw_payload = enc_payload, - header = header, - payload = payload, - signature = '' - } - end - return nil -end - --- get the Discovery metadata from the specified URL -local function openidc_discover(url, ssl_verify, keepalive, timeout, exptime, proxy_opts, http_request_decorator) - log(DEBUG, "openidc_discover: URL is: " .. url) - - local json, err - local v = openidc_cache_get("discovery", url) - if not v then - - log(DEBUG, "discovery data not in cache, making call to discovery endpoint") - -- make the call to the discovery endpoint - local httpc = http.new() - openidc_configure_timeouts(httpc, timeout) - openidc_configure_proxy(httpc, proxy_opts) - local res, error = httpc:request_uri(url, decorate_request(http_request_decorator, { - ssl_verify = (ssl_verify ~= "no"), - keepalive = (keepalive ~= "no") - })) - if not res then - err = "accessing discovery url (" .. url .. ") failed: " .. error - log(ERROR, err) - else - log(DEBUG, "response data: " .. res.body) - json, err = openidc_parse_json_response(res) - if json then - openidc_cache_set("discovery", url, cjson.encode(json), exptime or 24 * 60 * 60) - else - err = "could not decode JSON from Discovery data" .. (err and (": " .. err) or '') - log(ERROR, err) - end - end - - else - json = cjson.decode(v) - end - - return json, err -end - --- turn a discovery url set in the opts dictionary into the discovered information -local function openidc_ensure_discovered_data(opts) - local err - if type(opts.discovery) == "string" then - local discovery - discovery, err = openidc_discover(opts.discovery, opts.ssl_verify, opts.keepalive, opts.timeout, opts.discovery_expires_in, opts.proxy_opts, - opts.http_request_decorator) - if not err then - opts.discovery = discovery - end - end - return err -end - --- make a call to the userinfo endpoint -function openidc.call_userinfo_endpoint(opts, access_token) - local err = openidc_ensure_discovered_data(opts) - if err then - return nil, err - end - if not (opts and opts.discovery and opts.discovery.userinfo_endpoint) then - log(DEBUG, "no userinfo endpoint supplied") - return nil, nil - end - - local headers = { - ["Authorization"] = "Bearer " .. access_token, - } - - log(DEBUG, "authorization header '" .. headers.Authorization .. "'") - - local httpc = http.new() - openidc_configure_timeouts(httpc, opts.timeout) - openidc_configure_proxy(httpc, opts.proxy_opts) - local res, err = httpc:request_uri(opts.discovery.userinfo_endpoint, - decorate_request(opts.http_request_decorator, { - headers = headers, - ssl_verify = (opts.ssl_verify ~= "no"), - keepalive = (opts.keepalive ~= "no") - })) - if not res then - err = "accessing (" .. opts.discovery.userinfo_endpoint .. ") failed: " .. err - return nil, err - end - - log(DEBUG, "userinfo response: ", res.body) - - -- parse the response from the user info endpoint - return openidc_parse_json_response(res) -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 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 - - local result - if opts.discovery.token_endpoint_auth_methods_supported ~= nil then - -- if set check to make sure the discovery data includes the selected client auth method - if opts.token_endpoint_auth_method ~= nil then - for index, value in ipairs(opts.discovery.token_endpoint_auth_methods_supported) do - log(DEBUG, index .. " => " .. value) - if value == opts.token_endpoint_auth_method then - log(DEBUG, "configured value for token_endpoint_auth_method (" .. opts.token_endpoint_auth_method .. ") found in token_endpoint_auth_methods_supported in metadata") - result = opts.token_endpoint_auth_method - break - end - end - if result == nil then - log(ERROR, "configured value for token_endpoint_auth_method (" .. opts.token_endpoint_auth_method .. ") NOT found in token_endpoint_auth_methods_supported in metadata") - return nil - end - else - for index, value in ipairs(opts.discovery.token_endpoint_auth_methods_supported) do - log(DEBUG, index .. " => " .. value) - 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 - end - end - end - else - result = opts.token_endpoint_auth_method - end - - -- set a sane default if auto-configuration failed - if result == nil then - result = "client_secret_basic" - end - - log(DEBUG, "token_endpoint_auth_method result set to " .. result) - - return result -end - --- ensure that discovery and token auth configuration is available in opts -local function ensure_config(opts) - local err - err = openidc_ensure_discovered_data(opts) - if err then - return err - end - - -- set the authentication method for the token endpoint - opts.token_endpoint_auth_method = openidc_get_token_auth_method(opts) -end - --- query for discovery endpoint data -function openidc.get_discovery_doc(opts) - local err = openidc_ensure_discovered_data(opts) - if err then - log(ERROR, "error getting endpoints definition using discovery endpoint") - end - - return opts.discovery, err -end - -local function openidc_jwks(url, force, ssl_verify, keepalive, timeout, exptime, proxy_opts, http_request_decorator) - log(DEBUG, "openidc_jwks: URL is: " .. url .. " (force=" .. force .. ") (decorator=" .. (http_request_decorator and type(http_request_decorator) or "nil")) - - local json, err, v - - if force == 0 then - v = openidc_cache_get("jwks", url) - end - - if not v then - - log(DEBUG, "cannot use cached JWKS data; making call to jwks endpoint") - -- make the call to the jwks endpoint - local httpc = http.new() - openidc_configure_timeouts(httpc, timeout) - openidc_configure_proxy(httpc, proxy_opts) - local res, error = httpc:request_uri(url, decorate_request(http_request_decorator, { - ssl_verify = (ssl_verify ~= "no"), - keepalive = (keepalive ~= "no") - })) - if not res then - err = "accessing jwks url (" .. url .. ") failed: " .. error - log(ERROR, err) - else - log(DEBUG, "response data: " .. res.body) - json, err = openidc_parse_json_response(res) - if json then - openidc_cache_set("jwks", url, cjson.encode(json), exptime or 24 * 60 * 60) - end - end - - else - json = cjson.decode(v) - end - - return json, err -end - -local function split_by_chunk(text, chunkSize) - local s = {} - for i = 1, #text, chunkSize do - s[#s + 1] = text:sub(i, i + chunkSize - 1) - end - return s -end - -local function get_jwk(keys, kid) - - local rsa_keys = {} - for _, value in pairs(keys) do - if value.kty == "RSA" and (not value.use or value.use == "sig") then - table.insert(rsa_keys, value) - end - end - - if kid == nil then - if #rsa_keys == 1 then - log(DEBUG, "returning only RSA key of JWKS for keyid-less JWT") - return rsa_keys[1], nil - else - return nil, "JWT doesn't specify kid but the keystore contains multiple RSA keys" - end - end - for _, value in pairs(rsa_keys) do - if value.kid == kid then - return value, nil - end - end - - return nil, "RSA key with id " .. kid .. " not found" -end - -local wrap = ('.'):rep(64) - -local envelope = "-----BEGIN %s-----\n%s\n-----END %s-----\n" - -local function der2pem(data, typ) - typ = typ:upper() or "CERTIFICATE" - data = b64(data) - return string.format(envelope, typ, data:gsub(wrap, '%0\n', (#data - 1) / 64), typ) -end - - -local function encode_length(length) - if length < 0x80 then - return string.char(length) - elseif length < 0x100 then - return string.char(0x81, length) - elseif length < 0x10000 then - return string.char(0x82, math.floor(length / 0x100), length % 0x100) - end - error("Can't encode lengths over 65535") -end - - -local function encode_sequence(array, of) - local encoded_array = array - if of then - encoded_array = {} - for i = 1, #array do - encoded_array[i] = of(array[i]) - end - end - encoded_array = table.concat(encoded_array) - - return string.char(0x30) .. encode_length(#encoded_array) .. encoded_array -end - -local function encode_binary_integer(bytes) - if bytes:byte(1) > 127 then - -- We currenly only use this for unsigned integers, - -- however since the high bit is set here, it would look - -- like a negative signed int, so prefix with zeroes - bytes = "\0" .. bytes - end - return "\2" .. encode_length(#bytes) .. bytes -end - -local function encode_sequence_of_integer(array) - return encode_sequence(array, encode_binary_integer) -end - -local function encode_bit_string(array) - local s = "\0" .. array -- first octet holds the number of unused bits - return "\3" .. encode_length(#s) .. s -end - -local function openidc_pem_from_x5c(x5c) - log(DEBUG, "Found x5c, getting PEM public key from x5c entry of json public key") - local chunks = split_by_chunk(b64(openidc_base64_url_decode(x5c[1])), 64) - local pem = "-----BEGIN CERTIFICATE-----\n" .. - table.concat(chunks, "\n") .. - "\n-----END CERTIFICATE-----" - log(DEBUG, "Generated PEM key from x5c:", pem) - return pem -end - -local function openidc_pem_from_rsa_n_and_e(n, e) - log(DEBUG, "getting PEM public key from n and e parameters of json public key") - - local der_key = { - openidc_base64_url_decode(n), openidc_base64_url_decode(e) - } - local encoded_key = encode_sequence_of_integer(der_key) - local pem = der2pem(encode_sequence({ - encode_sequence({ - "\6\9\42\134\72\134\247\13\1\1\1" -- OID :rsaEncryption - .. "\5\0" -- ASN.1 NULL of length 0 - }), - encode_bit_string(encoded_key) - }), "PUBLIC KEY") - log(DEBUG, "Generated pem key from n and e: ", pem) - return pem -end - -local function openidc_pem_from_jwk(opts, kid) - local err = openidc_ensure_discovered_data(opts) - if err then - return nil, err - end - - if not opts.discovery.jwks_uri or not (type(opts.discovery.jwks_uri) == "string") or (opts.discovery.jwks_uri == "") then - return nil, "opts.discovery.jwks_uri is not present or not a string" - end - - local cache_id = opts.discovery.jwks_uri .. '#' .. (kid or '') - local v = openidc_cache_get("jwks", cache_id) - - if v then - return v - end - - local jwk, jwks - - for force = 0, 1 do - jwks, err = openidc_jwks(opts.discovery.jwks_uri, force, opts.ssl_verify, opts.keepalive, opts.timeout, opts.jwk_expires_in, opts.proxy_opts, - opts.http_request_decorator) - if err then - return nil, err - end - - jwk, err = get_jwk(jwks.keys, kid) - - if jwk and not err then - break - end - end - - if err then - return nil, err - end - - local x5c = jwk.x5c - if x5c and #(jwk.x5c) == 0 then - log(WARN, "Found invalid JWK with empty x5c array, ignoring x5c claim") - x5c = nil - end - - local pem - if x5c then - pem = openidc_pem_from_x5c(x5c) - elseif jwk.kty == "RSA" and jwk.n and jwk.e then - pem = openidc_pem_from_rsa_n_and_e(jwk.n, jwk.e) - else - return nil, "don't know how to create RSA key/cert for " .. cjson.encode(jwk) - end - - openidc_cache_set("jwks", cache_id, pem, opts.jwk_expires_in or 24 * 60 * 60) - return pem -end - --- does lua-resty-jwt and/or we know how to handle the algorithm of the JWT? -local function is_algorithm_supported(jwt_header) - return jwt_header and jwt_header.alg and (jwt_header.alg == "none" - or string.sub(jwt_header.alg, 1, 2) == "RS" - or string.sub(jwt_header.alg, 1, 2) == "HS") -end - --- is the JWT signing algorithm an asymmetric one whose key might be --- obtained from the discovery endpoint? -local function uses_asymmetric_algorithm(jwt_header) - return string.sub(jwt_header.alg, 1, 2) == "RS" -end - --- is the JWT signing algorithm one that has been expected? -local function is_algorithm_expected(jwt_header, expected_algs) - if expected_algs == nil or not jwt_header or not jwt_header.alg then - return true - end - if type(expected_algs) == 'string' then - expected_algs = { expected_algs } - end - for _, alg in ipairs(expected_algs) do - if alg == jwt_header.alg then - return true - end - end - return false -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 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) - if jwt then - if opts.accept_none_alg then - log(DEBUG, "accept JWT with alg \"none\" and no signature") - return jwt - else - return jwt, "token uses \"none\" alg but accept_none_alg is not enabled" - end - end -- otherwise the JWT is invalid and load_jwt produces an error - end - - 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 - reason = reason .. ": " .. jwt_obj.reason - end - return nil, reason - end - - if not is_algorithm_expected(jwt_obj.header, expected_algs) then - local alg = jwt_obj.header and jwt_obj.header.alg or "no algorithm at all" - return nil, "token is signed by unexpected algorithm \"" .. alg .. "\"" - end - - local secret - if is_algorithm_supported(jwt_obj.header) then - if uses_asymmetric_algorithm(jwt_obj.header) then - if opts.secret then - log(WARN, "using deprecated option `opts.secret` for asymmetric key; switch to `opts.public_key` instead") - end - secret = asymmetric_secret or opts.secret - if not secret and opts.discovery then - log(DEBUG, "using discovery to find key") - local err - secret, err = openidc_pem_from_jwk(opts, jwt_obj.header.kid) - - if secret == nil then - log(ERROR, err) - return nil, err - end - end - else - if opts.secret then - log(WARN, "using deprecated option `opts.secret` for symmetric key; switch to `opts.symmetric_key` instead") - end - secret = symmetric_secret or opts.secret - end - end - - if #{ ... } == 0 then - -- an empty list of claim specs makes lua-resty-jwt add default - -- validators for the exp and nbf claims if they are - -- present. These validators need to know the configured slack - -- value - local jwt_validators = require("resty.jwt-validators") - jwt_validators.set_system_leeway(opts.iat_slack and opts.iat_slack or 120) - end - - 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 - if not jwt_obj.verified then - local reason = "jwt signature verification failed" - if jwt_obj.reason then - reason = reason .. ": " .. jwt_obj.reason - end - return jwt_obj, reason - end - return jwt_obj -end - --- --- Load and validate id token from the id_token properties of the token endpoint response --- Parameters : --- - opts the openidc module options --- - jwt_id_token the id_token from the id_token properties of the token endpoint response --- - session the current session --- Return the id_token, nil if valid --- Return nil, the error if invalid --- -local function openidc_load_and_validate_jwt_id_token(opts, jwt_id_token, session) - - local jwt_obj, err = openidc_load_jwt_and_verify_crypto(opts, jwt_id_token, opts.public_key, opts.client_secret, - opts.discovery.id_token_signing_alg_values_supported) - if err then - local alg = (jwt_obj and jwt_obj.header and jwt_obj.header.alg) or '' - local is_unsupported_signature_error = jwt_obj and not jwt_obj.verified and not is_algorithm_supported(jwt_obj.header) - if is_unsupported_signature_error then - if opts.accept_unsupported_alg == nil or opts.accept_unsupported_alg then - log(WARN, "ignored id_token signature as algorithm '" .. alg .. "' is not supported") - else - err = "token is signed using algorithm \"" .. alg .. "\" which is not supported by lua-resty-jwt" - log(ERROR, err) - return nil, err - end - else - log(ERROR, "id_token '" .. alg .. "' signature verification failed") - return nil, err - end - end - local id_token = jwt_obj.payload - - log(DEBUG, "id_token header: ", cjson.encode(jwt_obj.header)) - log(DEBUG, "id_token payload: ", cjson.encode(jwt_obj.payload)) - - -- validate the id_token contents - if openidc_validate_id_token(opts, id_token, session.data.nonce) == false then - err = "id_token validation failed" - log(ERROR, err) - return nil, err - end - - return id_token -end - --- handle a "code" authorization response from the OP -local function openidc_authorization_response(opts, session) - local args = ngx.req.get_uri_args() - local err, log_err, client_err - - if not args.code or not args.state then - err = "unhandled request to the redirect_uri: " .. ngx.var.request_uri - log(ERROR, err) - return nil, err, session.data.original_url, session - end - - -- check that the state returned in the response against the session; prevents CSRF - if args.state ~= session.data.state then - log_err = "state from argument: " .. (args.state and args.state or "nil") .. " does not match state restored from session: " .. (session.data.state and session.data.state or "nil") - client_err = "state from argument does not match state restored from session" - log(ERROR, log_err) - return nil, client_err, session.data.original_url, session - end - - err = ensure_config(opts) - if err then - return nil, err, session.data.original_url, session - end - - -- check the iss if returned from the OP - if args.iss and args.iss ~= opts.discovery.issuer then - log_err = "iss from argument: " .. args.iss .. " does not match expected issuer: " .. opts.discovery.issuer - client_err = "iss from argument does not match expected issuer" - log(ERROR, log_err) - return nil, client_err, session.data.original_url, session - end - - -- check the client_id if returned from the OP - if args.client_id and args.client_id ~= opts.client_id then - log_err = "client_id from argument: " .. args.client_id .. " does not match expected client_id: " .. opts.client_id - client_err = "client_id from argument does not match expected client_id" - log(ERROR, log_err) - return nil, client_err, session.data.original_url, session - end - - -- assemble the parameters to the token endpoint - local body = { - grant_type = "authorization_code", - code = args.code, - redirect_uri = openidc_get_redirect_uri(opts, session), - state = session.data.state, - code_verifier = session.data.code_verifier - } - - log(DEBUG, "Authentication with OP done -> Calling OP Token Endpoint to obtain tokens") - - local current_time = ngx.time() - -- make the call to the token endpoint - local json - json, err = openidc.call_token_endpoint(opts, opts.discovery.token_endpoint, body, opts.token_endpoint_auth_method) - if err then - return nil, err, session.data.original_url, session - end - - local id_token, err = openidc_load_and_validate_jwt_id_token(opts, json.id_token, session); - if err then - return nil, err, session.data.original_url, session - end - - -- mark this sessions as authenticated - session.data.authenticated = true - -- clear state, nonce and code_verifier to protect against potential misuse - session.data.nonce = nil - session.data.state = nil - session.data.code_verifier = nil - if store_in_session(opts, 'id_token') then - session.data.id_token = id_token - end - - if store_in_session(opts, 'user') then - -- call the user info endpoint - -- TODO: should this error be checked? - local user - user, err = openidc.call_userinfo_endpoint(opts, json.access_token) - - if err then - log(ERROR, "error calling userinfo endpoint: " .. err) - elseif user then - if id_token.sub ~= user.sub then - err = "\"sub\" claim in id_token (\"" .. (id_token.sub or "null") .. "\") is not equal to the \"sub\" claim returned from the userinfo endpoint (\"" .. (user.sub or "null") .. "\")" - log(ERROR, err) - else - session.data.user = user - end - end - end - - if store_in_session(opts, 'enc_id_token') then - session.data.enc_id_token = json.id_token - end - - if store_in_session(opts, 'access_token') then - session.data.access_token = json.access_token - session.data.access_token_expiration = current_time - + openidc_access_token_expires_in(opts, json.expires_in) - if json.refresh_token ~= nil then - session.data.refresh_token = json.refresh_token - end - end - - if opts.lifecycle and opts.lifecycle.on_authenticated then - err = opts.lifecycle.on_authenticated(session, id_token, json) - if err then - log(WARN, "failed in `on_authenticated` handler: " .. err) - return nil, err, session.data.original_url, session - end - end - - -- save the session with the obtained id_token - session:save() - - -- redirect to the URL that was accessed originally - log(DEBUG, "OIDC Authorization Code Flow completed -> Redirecting to original URL (" .. session.data.original_url .. ")") - ngx.redirect(session.data.original_url) - return nil, nil, session.data.original_url, session -end - --- token revocation (RFC 7009) -local function openidc_revoke_token(opts, token_type_hint, token) - if not opts.discovery.revocation_endpoint then - log(DEBUG, "no revocation endpoint supplied. unable to revoke " .. token_type_hint .. ".") - return nil - end - - local token_type_hint = token_type_hint or nil - local body = { - token = token - } - if token_type_hint then - body['token_type_hint'] = token_type_hint - end - local token_type_log = token_type_hint or 'token' - - -- ensure revocation endpoint auth method is properly discovered - local err = ensure_config(opts) - if err then - log(ERROR, "revocation of " .. token_type_log .. " unsuccessful: " .. err) - return false - end - - -- call the revocation endpoint - local _ - _, err = openidc.call_token_endpoint(opts, opts.discovery.revocation_endpoint, body, opts.token_endpoint_auth_method, "revocation", true) - if err then - log(ERROR, "revocation of " .. token_type_log .. " unsuccessful: " .. err) - return false - else - log(DEBUG, "revocation of " .. token_type_log .. " successful") - return true - end -end - -function openidc.revoke_token(opts, token_type_hint, token) - local err = openidc_ensure_discovered_data(opts) - if err then - log(ERROR, "revocation of " .. (token_type_hint or "token (no type specified)") .. " unsuccessful: " .. err) - return false - end - - return openidc_revoke_token(opts, token_type_hint, token) -end - -function openidc.revoke_tokens(opts, session) - local err = openidc_ensure_discovered_data(opts) - if err then - log(ERROR, "revocation of tokens unsuccessful: " .. err) - return false - end - - local access_token = session.data.access_token - local refresh_token = session.data.refresh_token - - local access_token_revoke, refresh_token_revoke - if refresh_token then - access_token_revoke = openidc_revoke_token(opts, "refresh_token", refresh_token) - end - if access_token then - refresh_token_revoke = openidc_revoke_token(opts, "access_token", access_token) - end - return access_token_revoke and refresh_token_revoke -end - -local openidc_transparent_pixel = "\137\080\078\071\013\010\026\010\000\000\000\013\073\072\068\082" .. - "\000\000\000\001\000\000\000\001\008\004\000\000\000\181\028\012" .. - "\002\000\000\000\011\073\068\065\084\120\156\099\250\207\000\000" .. - "\002\007\001\002\154\028\049\113\000\000\000\000\073\069\078\068" .. - "\174\066\096\130" - --- handle logout -local function openidc_logout(opts, session) - local session_token = session.data.enc_id_token - local access_token = session.data.access_token - local refresh_token = session.data.refresh_token - local err - - if opts.lifecycle and opts.lifecycle.on_logout then - err = opts.lifecycle.on_logout(session) - if err then - log(WARN, "failed in `on_logout` handler: " .. err) - return err - end - end - - session:destroy() - - if opts.revoke_tokens_on_logout then - log(DEBUG, "revoke_tokens_on_logout is enabled. " .. - "trying to revoke access and refresh tokens...") - if refresh_token then - openidc_revoke_token(opts, "refresh_token", refresh_token) - end - if access_token then - openidc_revoke_token(opts, "access_token", access_token) - end - end - - local headers = ngx.req.get_headers() - local header = get_first(headers['Accept']) - if header and header:find("image/png") then - ngx.header["Cache-Control"] = "no-cache, no-store" - ngx.header["Pragma"] = "no-cache" - ngx.header["P3P"] = "CAO PSA OUR" - ngx.header["Expires"] = "0" - ngx.header["X-Frame-Options"] = "DENY" - ngx.header.content_type = "image/png" - ngx.print(openidc_transparent_pixel) - ngx.exit(ngx.OK) - return - elseif opts.redirect_after_logout_uri or opts.discovery.end_session_endpoint then - local uri - if opts.redirect_after_logout_uri then - uri = opts.redirect_after_logout_uri - else - uri = opts.discovery.end_session_endpoint - end - local params = {} - if (opts.redirect_after_logout_with_id_token_hint or not opts.redirect_after_logout_uri) and session_token then - params["id_token_hint"] = session_token - end - if opts.post_logout_redirect_uri then - params["post_logout_redirect_uri"] = opts.post_logout_redirect_uri - end - return ngx.redirect(openidc_combine_uri(uri, params)) - elseif opts.discovery.ping_end_session_endpoint then - local params = {} - if opts.post_logout_redirect_uri then - params["TargetResource"] = opts.post_logout_redirect_uri - end - return ngx.redirect(openidc_combine_uri(opts.discovery.ping_end_session_endpoint, params)) - end - - ngx.header.content_type = "text/html" - ngx.say("<html><body>Logged Out</body></html>") - ngx.exit(ngx.OK) -end - --- returns a valid access_token (eventually refreshing the token) -local function openidc_access_token(opts, session, try_to_renew) - - local err - - if session.data.access_token == nil then - return nil, err - end - local current_time = ngx.time() - if current_time < session.data.access_token_expiration then - return session.data.access_token, err - end - if not try_to_renew then - return nil, "token expired" - end - if session.data.refresh_token == nil then - return nil, "token expired and no refresh token available" - end - - log(DEBUG, "refreshing expired access_token: ", session.data.access_token, " with: ", session.data.refresh_token) - - -- retrieve token endpoint URL from discovery endpoint if necessary - err = ensure_config(opts) - if err then - return nil, err - end - - -- assemble the parameters to the token endpoint - local body = { - grant_type = "refresh_token", - refresh_token = session.data.refresh_token, - scope = opts.scope and opts.scope or "openid email profile" - } - - local json - json, err = openidc.call_token_endpoint(opts, opts.discovery.token_endpoint, body, opts.token_endpoint_auth_method) - if err then - return nil, err - end - local id_token - if json.id_token then - id_token, err = openidc_load_and_validate_jwt_id_token(opts, json.id_token, session) - if err then - log(ERROR, "invalid id token, discarding tokens returned while refreshing") - return nil, err - end - end - log(DEBUG, "access_token refreshed: ", json.access_token, " updated refresh_token: ", json.refresh_token) - - session.data.access_token = json.access_token - session.data.access_token_expiration = current_time + openidc_access_token_expires_in(opts, json.expires_in) - if json.refresh_token then - session.data.refresh_token = json.refresh_token - end - - if json.id_token and - (store_in_session(opts, 'enc_id_token') or store_in_session(opts, 'id_token')) then - log(DEBUG, "id_token refreshed: ", json.id_token) - if store_in_session(opts, 'enc_id_token') then - session.data.enc_id_token = json.id_token - end - if store_in_session(opts, 'id_token') then - session.data.id_token = id_token - end - end - - -- save the session with the new access_token and optionally the new refresh_token and id_token using a new sessionid - local regenerated - regenerated, err = session:regenerate() - if err then - log(ERROR, "failed to regenerate session: " .. err) - return nil, err - end - if opts.lifecycle and opts.lifecycle.on_regenerated then - err = opts.lifecycle.on_regenerated(session) - if err then - log(WARN, "failed in `on_regenerated` handler: " .. err) - return nil, err - end - end - - return session.data.access_token, err -end - -local function openidc_get_path(uri) - local without_query = uri:match("(.-)%?") or uri - return without_query:match(".-//[^/]+(/.*)") or without_query -end - -local function openidc_get_redirect_uri_path(opts) - return opts.redirect_uri and openidc_get_path(opts.redirect_uri) or opts.redirect_uri_path -end - -local function is_session(o) - return o ~= nil and o.start and type(o.start) == "function" -end - --- main routine for OpenID Connect user authentication -function openidc.authenticate(opts, target_url, unauth_action, session_or_opts) - - if opts.redirect_uri_path then - log(WARN, "using deprecated option `opts.redirect_uri_path`; switch to using an absolute URI and `opts.redirect_uri` instead") - end - - local err - - local session - if is_session(session_or_opts) then - session = session_or_opts - else - local session_error - session, session_error = r_session.start(session_or_opts) - if session == nil then - log(ERROR, "Error starting session: " .. session_error) - return nil, session_error, target_url, session - end - end - - target_url = target_url or ngx.var.request_uri - - local access_token - - -- see if this is a request to the redirect_uri i.e. an authorization response - local path = openidc_get_path(target_url) - if path == openidc_get_redirect_uri_path(opts) then - log(DEBUG, "Redirect URI path (" .. path .. ") is currently navigated -> Processing authorization response coming from OP") - - if not session.present then - err = "request to the redirect_uri path but there's no session state found" - log(ERROR, err) - return nil, err, target_url, session - end - - return openidc_authorization_response(opts, session) - end - - -- see if this is a request to logout - if path == (opts.logout_path or "/logout") then - log(DEBUG, "Logout path (" .. path .. ") is currently navigated -> Processing local session removal before redirecting to next step of logout process") - - err = ensure_config(opts) - if err then - return nil, err, session.data.original_url, session - end - - openidc_logout(opts, session) - return nil, nil, target_url, session - end - - local token_expired = false - local try_to_renew = opts.renew_access_token_on_expiry == nil or opts.renew_access_token_on_expiry - if session.present and session.data.authenticated - and store_in_session(opts, 'access_token') then - - -- refresh access_token if necessary - access_token, err = openidc_access_token(opts, session, try_to_renew) - if err then - log(ERROR, "lost access token:" .. err) - err = nil - end - if not access_token then - token_expired = true - end - end - - log(DEBUG, - "session.present=", session.present, - ", session.data.id_token=", session.data.id_token ~= nil, - ", session.data.authenticated=", session.data.authenticated, - ", opts.force_reauthorize=", opts.force_reauthorize, - ", opts.renew_access_token_on_expiry=", opts.renew_access_token_on_expiry, - ", try_to_renew=", try_to_renew, - ", token_expired=", token_expired) - - -- if we are not authenticated then redirect to the OP for authentication - -- the presence of the id_token is check for backwards compatibility - if not session.present - or not (session.data.id_token or session.data.authenticated) - or opts.force_reauthorize - or (try_to_renew and token_expired) then - if unauth_action == "pass" then - if token_expired then - session.data.authenticated = false - return nil, 'token refresh failed', target_url, session - end - return nil, err, target_url, session - end - if unauth_action == 'deny' then - return nil, 'unauthorized request', target_url, session - end - - err = ensure_config(opts) - if err then - return nil, err, session.data.original_url, session - end - - log(DEBUG, "Authentication is required - Redirecting to OP Authorization endpoint") - openidc_authorize(opts, session, target_url, opts.prompt) - return nil, nil, target_url, session - end - - -- silently reauthenticate if necessary (mainly used for session refresh/getting updated id_token data) - if opts.refresh_session_interval ~= nil then - if session.data.last_authenticated == nil or (session.data.last_authenticated + opts.refresh_session_interval) < ngx.time() then - err = ensure_config(opts) - if err then - return nil, err, session.data.original_url, session - end - - log(DEBUG, "Silent authentication is required - Redirecting to OP Authorization endpoint") - openidc_authorize(opts, session, target_url, "none") - return nil, nil, target_url, session - end - end - - if store_in_session(opts, 'id_token') then - -- log id_token contents - log(DEBUG, "id_token=", cjson.encode(session.data.id_token)) - end - - -- return the id_token to the caller Lua script for access control purposes - return - { - id_token = session.data.id_token, - access_token = access_token, - user = session.data.user - }, - err, - target_url, - session -end - --- get a valid access_token (eventually refreshing the token), or nil if there's no valid access_token -function openidc.access_token(opts, session_opts) - - local session = r_session.start(session_opts) - local token, err = openidc_access_token(opts, session, true) - session:close() - return token, err -end - - --- get an OAuth 2.0 bearer access token from the HTTP request cookies -local function openidc_get_bearer_access_token_from_cookie(opts) - - local err - - log(DEBUG, "getting bearer access token from Cookie") - - local accept_token_as = opts.auth_accept_token_as or "header" - if accept_token_as:find("cookie") ~= 1 then - 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(':') - local cookie_name = divider and accept_token_as:sub(divider + 1) or "PA.global" - - log(DEBUG, "bearer access token from cookie named: " .. cookie_name) - - local cookies = ngx.req.get_headers()["Cookie"] - if not cookies then - err = "no Cookie header found" - log(ERROR, err) - return nil, err - end - - local cookie_value = ngx.var["cookie_" .. cookie_name] - if not cookie_value then - err = "no Cookie " .. cookie_name .. " found" - log(ERROR, err) - end - - return cookie_value, err -end - - --- get an OAuth 2.0 bearer access token from the HTTP request -local function openidc_get_bearer_access_token(opts) - - local err - - local accept_token_as = opts.auth_accept_token_as or "header" - - if accept_token_as:find("cookie") == 1 then - return openidc_get_bearer_access_token_from_cookie(opts) - end - - -- get the access token from the Authorization header - local headers = ngx.req.get_headers() - local header_name = opts.auth_accept_token_as_header_name or "Authorization" - local header = get_first(headers[header_name]) - - if header == nil or header:find(" ") == nil then - err = "no Authorization header found" - log(ERROR, err) - return nil, err - end - - local divider = header:find(' ') - if string.lower(header:sub(0, divider - 1)) ~= string.lower("Bearer") then - err = "no Bearer authorization header value found" - log(ERROR, err) - return nil, err - end - - local access_token = header:sub(divider + 1) - if access_token == nil then - err = "no Bearer access token value found" - log(ERROR, err) - return nil, err - end - - return access_token, err -end - -local function get_introspection_endpoint(opts) - local introspection_endpoint = opts.introspection_endpoint - if not introspection_endpoint then - local err = openidc_ensure_discovered_data(opts) - if err then - return nil, "opts.introspection_endpoint not said and " .. err - end - local endpoint = opts.discovery and opts.discovery.introspection_endpoint - if endpoint then - return endpoint - end - end - return introspection_endpoint -end - -local function get_introspection_cache_prefix(opts) - return (opts.cache_segment and opts.cache_segment.gsub(',', '_') or 'DEFAULT') .. ',' - .. (get_introspection_endpoint(opts) or 'nil-endpoint') .. ',' - .. (opts.client_id or 'no-client_id') .. ',' - .. (opts.client_secret and 'secret' or 'no-client_secret') .. ':' -end - -local function get_cached_introspection(opts, access_token) - local introspection_cache_ignore = opts.introspection_cache_ignore or false - if not introspection_cache_ignore then - return openidc_cache_get("introspection", - get_introspection_cache_prefix(opts) .. access_token) - end -end - -local function set_cached_introspection(opts, access_token, encoded_json, ttl) - local introspection_cache_ignore = opts.introspection_cache_ignore or false - if not introspection_cache_ignore then - openidc_cache_set("introspection", - get_introspection_cache_prefix(opts) .. access_token, - encoded_json, ttl) - end -end - --- main routine for OAuth 2.0 token introspection -function openidc.introspect(opts) - - -- get the access token from the request - local access_token, err = openidc_get_bearer_access_token(opts) - if access_token == nil then - return nil, err - end - - -- see if we've previously cached the introspection result for this access token - local json - local v = get_cached_introspection(opts, access_token) - - if v then - json = cjson.decode(v) - return json, err - end - - -- assemble the parameters to the introspection (token) endpoint - local token_param_name = opts.introspection_token_param_name and opts.introspection_token_param_name or "token" - - local body = {} - - body[token_param_name] = access_token - - if opts.client_id then - body.client_id = opts.client_id - end - if opts.client_secret then - body.client_secret = opts.client_secret - end - - -- merge any provided extra parameters - if opts.introspection_params then - for key, val in pairs(opts.introspection_params) do body[key] = val end - end - - -- call the introspection endpoint - local introspection_endpoint - introspection_endpoint, err = get_introspection_endpoint(opts) - if err then - return nil, err - end - json, err = openidc.call_token_endpoint(opts, introspection_endpoint, body, opts.introspection_endpoint_auth_method, "introspection") - - - if not json then - return json, err - end - - if not json.active then - err = "invalid token" - return json, err - end - - -- cache the results - local introspection_cache_ignore = opts.introspection_cache_ignore or false - local expiry_claim = opts.introspection_expiry_claim or "exp" - - if not introspection_cache_ignore and json[expiry_claim] then - local introspection_interval = opts.introspection_interval or 0 - local ttl = json[expiry_claim] - if expiry_claim == "exp" then --https://tools.ietf.org/html/rfc7662#section-2.2 - ttl = ttl - ngx.time() - end - if introspection_interval > 0 then - if ttl > introspection_interval then - ttl = introspection_interval - end - end - log(DEBUG, "cache token ttl: " .. ttl) - set_cached_introspection(opts, access_token, cjson.encode(json), ttl) - end - - return json, err - -end - -local function get_jwt_verification_cache_prefix(opts) - local signing_alg_values_expected = (opts.accept_none_alg and 'none' or 'no-none') - local expected_algs = opts.token_signing_alg_values_expected or {} - if type(expected_algs) == 'string' then - expected_algs = { expected_algs } - end - for _, alg in ipairs(expected_algs) do - signing_alg_values_expected = signing_alg_values_expected .. ',' .. alg - end - return (opts.cache_segment and opts.cache_segment.gsub(',', '_') or 'DEFAULT') .. ',' - .. (opts.public_key or 'no-pubkey') .. ',' - .. (opts.symmetric_key or 'no-symkey') .. ',' - .. signing_alg_values_expected .. ':' -end - -local function get_cached_jwt_verification(opts, access_token) - local jwt_verification_cache_ignore = opts.jwt_verification_cache_ignore or false - if not jwt_verification_cache_ignore then - return openidc_cache_get("jwt_verification", - get_jwt_verification_cache_prefix(opts) .. access_token) - end -end - -local function set_cached_jwt_verification(opts, access_token, encoded_json, ttl) - local jwt_verification_cache_ignore = opts.jwt_verification_cache_ignore or false - if not jwt_verification_cache_ignore then - openidc_cache_set("jwt_verification", - get_jwt_verification_cache_prefix(opts) .. access_token, - encoded_json, ttl) - end -end - --- main routine for OAuth 2.0 JWT token validation --- optional args are claim specs, see jwt-validators in resty.jwt -function openidc.jwt_verify(access_token, opts, ...) - local err - local json - local v = get_cached_jwt_verification(opts, access_token) - - local slack = opts.iat_slack and opts.iat_slack or 120 - if not v then - local jwt_obj - jwt_obj, err = openidc_load_jwt_and_verify_crypto(opts, access_token, opts.public_key, opts.symmetric_key, - opts.token_signing_alg_values_expected, ...) - if not err then - json = jwt_obj.payload - local encoded_json = cjson.encode(json) - log(DEBUG, "jwt: ", encoded_json) - - set_cached_jwt_verification(opts, access_token, encoded_json, - json.exp and json.exp - ngx.time() or 120) - end - - else - -- decode from the cache - json = cjson.decode(v) - end - - -- check the token expiry - if json then - if json.exp and json.exp + slack < ngx.time() then - log(ERROR, "token expired: json.exp=", json.exp, ", ngx.time()=", ngx.time()) - err = "JWT expired" - end - end - - return json, err -end - -function openidc.bearer_jwt_verify(opts, ...) - local json - - -- get the access token from the request - local access_token, err = openidc_get_bearer_access_token(opts) - if access_token == nil then - return nil, err - end - - log(DEBUG, "access_token: ", access_token) - - json, err = openidc.jwt_verify(access_token, opts, ...) - return json, err, access_token -end - --- Passing nil to any of the arguments resets the configuration to default -function openidc.set_logging(new_log, new_levels) - log = new_log and new_log or ngx.log - DEBUG = new_levels.DEBUG and new_levels.DEBUG or ngx.DEBUG - ERROR = new_levels.ERROR and new_levels.ERROR or ngx.ERR - WARN = new_levels.WARN and new_levels.WARN or ngx.WARN -end - -return openidc |