aboutsummaryrefslogtreecommitdiffstats
path: root/server/resty/openidc.lua
diff options
context:
space:
mode:
Diffstat (limited to 'server/resty/openidc.lua')
-rw-r--r--server/resty/openidc.lua1870
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