2 Licensed to the Apache Software Foundation (ASF) under one
3 or more contributor license agreements. See the NOTICE file
4 distributed with this work for additional information
5 regarding copyright ownership. The ASF licenses this file
6 to you under the Apache License, Version 2.0 (the
7 "License"); you may not use this file except in compliance
8 with the License. You may obtain a copy of the License at
10 http://www.apache.org/licenses/LICENSE-2.0
12 Unless required by applicable law or agreed to in writing,
13 software distributed under the License is distributed on an
14 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 KIND, either express or implied. See the License for the
16 specific language governing permissions and limitations
19 ***************************************************************************
20 Copyright (C) 2017-2019 ZmartZone IAM
21 Copyright (C) 2015-2017 Ping Identity Corporation
24 For further information please contact:
26 Ping Identity Corporation
27 1099 18th St Suite 2950
30 http://www.pingidentity.com
32 DISCLAIMER OF WARRANTIES:
34 THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT
35 ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING,
36 WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT,
37 MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY
38 WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE
39 USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET
40 YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE
41 WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
42 CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
43 EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF
44 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
45 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
46 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
48 @Author: Hans Zandbelt - hans.zandbelt@zmartzone.eu
51 local require = require
52 local cjson = require("cjson")
53 local cjson_s = require("cjson.safe")
54 local http = require("resty.http")
55 local r_session = require("resty.session")
61 local b64 = ngx.encode_base64
62 local unb64 = ngx.decode_base64
65 local DEBUG = ngx.DEBUG
69 local function token_auth_method_precondition(method, required_field)
71 if not opts[required_field] then
72 log(DEBUG, "Can't use " .. method .. " without opts." .. required_field)
79 local supported_token_auth_methods = {
80 client_secret_basic = true,
81 client_secret_post = true,
82 private_key_jwt = token_auth_method_precondition('private_key_jwt', 'client_rsa_private_key'),
83 client_secret_jwt = token_auth_method_precondition('client_secret_jwt', 'client_secret')
89 openidc.__index = openidc
91 local function store_in_session(opts, feature)
92 -- We don't have a whitelist of features to enable
93 if not opts.session_contents then
97 return opts.session_contents[feature]
100 -- set value in server-wide cache if available
101 local function openidc_cache_set(type, key, value, exp)
102 local dict = ngx.shared[type]
103 if dict and (exp > 0) then
104 local success, err, forcible = dict:set(key, value, exp)
105 log(DEBUG, "cache set: success=", success, " err=", err, " forcible=", forcible)
109 -- retrieve value from server-wide cache if available
110 local function openidc_cache_get(type, key)
111 local dict = ngx.shared[type]
114 value = dict:get(key)
115 if value then log(DEBUG, "cache hit: type=", type, " key=", key) end
120 -- invalidate values of server-wide cache
121 local function openidc_cache_invalidate(type)
122 local dict = ngx.shared[type]
124 log(DEBUG, "flushing cache for " .. type)
126 local nbr = dict.flush_expired(dict)
130 -- invalidate all server-wide caches
131 function openidc.invalidate_caches()
132 openidc_cache_invalidate("discovery")
133 openidc_cache_invalidate("jwks")
134 openidc_cache_invalidate("introspection")
135 openidc_cache_invalidate("jwt_verification")
138 -- validate the contents of and id_token
139 local function openidc_validate_id_token(opts, id_token, nonce)
142 if opts.discovery.issuer ~= id_token.iss then
143 log(ERROR, "issuer \"", id_token.iss, "\" in id_token is not equal to the issuer from the discovery document \"", opts.discovery.issuer, "\"")
148 if not id_token.sub then
149 log(ERROR, "no \"sub\" claim found in id_token")
154 if nonce and nonce ~= id_token.nonce then
155 log(ERROR, "nonce \"", id_token.nonce, "\" in id_token is not equal to the nonce that was sent in the request \"", nonce, "\"")
159 -- check issued-at timestamp
160 if not id_token.iat then
161 log(ERROR, "no \"iat\" claim found in id_token")
165 local slack = opts.iat_slack and opts.iat_slack or 120
166 if id_token.iat > (ngx.time() + slack) then
167 log(ERROR, "id_token not yet valid: id_token.iat=", id_token.iat, ", ngx.time()=", ngx.time(), ", slack=", slack)
171 -- check expiry timestamp
172 if not id_token.exp then
173 log(ERROR, "no \"exp\" claim found in id_token")
177 if (id_token.exp + slack) < ngx.time() then
178 log(ERROR, "token expired: id_token.exp=", id_token.exp, ", ngx.time()=", ngx.time())
182 -- check audience (array or string)
183 if not id_token.aud then
184 log(ERROR, "no \"aud\" claim found in id_token")
188 if (type(id_token.aud) == "table") then
189 for _, value in pairs(id_token.aud) do
190 if value == opts.client_id then
194 log(ERROR, "no match found token audience array: client_id=", opts.client_id)
196 elseif (type(id_token.aud) == "string") then
197 if id_token.aud ~= opts.client_id then
198 log(ERROR, "token audience does not match: id_token.aud=", id_token.aud, ", client_id=", opts.client_id)
205 local function get_first(table_or_string)
206 local res = table_or_string
207 if table_or_string and type(table_or_string) == 'table' then
208 res = table_or_string[1]
213 local function get_first_header(headers, header_name)
214 local header = headers[header_name]
215 return get_first(header)
218 local function get_first_header_and_strip_whitespace(headers, header_name)
219 local header = get_first_header(headers, header_name)
220 return header and header:gsub('%s', '')
223 local function get_forwarded_parameter(headers, param_name)
224 local forwarded = get_first_header(headers, 'Forwarded')
227 local function parse_parameter(pv)
228 local name, value = pv:match("^%s*([^=]+)%s*=%s*(.-)%s*$")
229 if name and value then
230 if value:sub(1, 1) == '"' then
231 value = value:sub(2, -2)
233 params[name:lower()] = value
237 -- this assumes there is no quoted comma inside the header's value
238 -- which should be fine as comma is not legal inside a node name,
239 -- a URI scheme or a host name. The only thing that might bite us
241 local first_part = forwarded
242 local first_comma = forwarded:find("%s*,%s*")
244 first_part = forwarded:sub(1, first_comma - 1)
246 first_part:gsub("[^;]+", parse_parameter)
248 return params[param_name:gsub("^%s*(.-)%s*$", "%1"):lower()]
251 local function get_scheme(headers)
252 return get_forwarded_parameter(headers, 'proto')
253 or get_first_header_and_strip_whitespace(headers, 'X-Forwarded-Proto')
257 local function get_host_name_from_x_header(headers)
258 local header = get_first_header_and_strip_whitespace(headers, 'X-Forwarded-Host')
259 return header and header:gsub('^([^,]+),?.*$', '%1')
262 local function get_host_name(headers)
263 return get_forwarded_parameter(headers, 'host')
264 or get_host_name_from_x_header(headers)
268 -- assemble the redirect_uri
269 local function openidc_get_redirect_uri(opts, session)
270 local path = opts.redirect_uri_path
271 if opts.redirect_uri then
272 if opts.redirect_uri:sub(1, 1) == '/' then
273 path = opts.redirect_uri
275 return opts.redirect_uri
278 local headers = ngx.req.get_headers()
279 local scheme = opts.redirect_uri_scheme or get_scheme(headers)
280 local host = get_host_name(headers)
282 -- possibly HTTP 1.0 and no Host header
283 if session then session:close() end
284 ngx.exit(ngx.HTTP_BAD_REQUEST)
286 return scheme .. "://" .. host .. path
289 -- perform base64url decoding
290 local function openidc_base64_url_decode(input)
291 local reminder = #input % 4
293 local padlen = 4 - reminder
294 input = input .. string.rep('=', padlen)
296 input = input:gsub('%-', '+'):gsub('_', '/')
300 -- perform base64url encoding
301 local function openidc_base64_url_encode(input)
302 local output = b64(input, true)
303 return output:gsub('%+', '-'):gsub('/', '_')
306 local function openidc_combine_uri(uri, params)
307 if params == nil or next(params) == nil then
311 if string.find(uri, "?", 1, true) then
314 return uri .. sep .. ngx.encode_args(params)
317 local function decorate_request(http_request_decorator, req)
318 return http_request_decorator and http_request_decorator(req) or req
321 local function openidc_s256(verifier)
322 local sha256 = (require 'resty.sha256'):new()
323 sha256:update(verifier)
324 return openidc_base64_url_encode(sha256:final())
327 -- send the browser of to the OP's authorization endpoint
328 local function openidc_authorize(opts, session, target_url, prompt)
329 local resty_random = require("resty.random")
330 local resty_string = require("resty.string")
333 -- generate state and nonce
334 local state = resty_string.to_hex(resty_random.bytes(16))
335 local nonce = (opts.use_nonce == nil or opts.use_nonce)
336 and resty_string.to_hex(resty_random.bytes(16))
337 local code_verifier = opts.use_pkce and openidc_base64_url_encode(resty_random.bytes(32))
339 -- assemble the parameters to the authentication request
341 client_id = opts.client_id,
342 response_type = "code",
343 scope = opts.scope and opts.scope or "openid email profile",
344 redirect_uri = openidc_get_redirect_uri(opts, session),
353 params.prompt = prompt
357 params.display = opts.display
360 if code_verifier then
361 params.code_challenge_method = 'S256'
362 params.code_challenge = openidc_s256(code_verifier)
365 -- merge any provided extra parameters
366 if opts.authorization_params then
367 for k, v in pairs(opts.authorization_params) do params[k] = v end
370 -- store state in the session
371 session.data.original_url = target_url
372 session.data.state = state
373 session.data.nonce = nonce
374 session.data.code_verifier = code_verifier
375 session.data.last_authenticated = ngx.time()
377 if opts.lifecycle and opts.lifecycle.on_created then
378 err = opts.lifecycle.on_created(session)
380 log(WARN, "failed in `on_created` handler: " .. err)
387 -- redirect to the /authorization endpoint
388 ngx.header["Cache-Control"] = "no-cache, no-store, max-age=0"
389 return ngx.redirect(openidc_combine_uri(opts.discovery.authorization_endpoint, params))
392 -- parse the JSON result from a call to the OP
393 local function openidc_parse_json_response(response, ignore_body_on_success)
394 local ignore_body_on_success = ignore_body_on_success or false
399 -- check the response from the OP
400 if response.status ~= 200 then
401 err = "response indicates failure, status=" .. response.status .. ", body=" .. response.body
403 if ignore_body_on_success then
407 -- decode the response and extract the JSON object
408 res = cjson_s.decode(response.body)
411 err = "JSON decoding failed"
418 local function openidc_configure_timeouts(httpc, timeout)
420 if type(timeout) == "table" then
421 local r, e = httpc:set_timeouts(timeout.connect or 0, timeout.send or 0, timeout.read or 0)
423 local r, e = httpc:set_timeout(timeout)
428 -- Set outgoing proxy options
429 local function openidc_configure_proxy(httpc, proxy_opts)
430 if httpc and proxy_opts and type(proxy_opts) == "table" then
431 log(DEBUG, "openidc_configure_proxy : use http proxy")
432 httpc:set_proxy_options(proxy_opts)
434 log(DEBUG, "openidc_configure_proxy : don't use http proxy")
438 -- make a call to the token endpoint
439 function openidc.call_token_endpoint(opts, endpoint, body, auth, endpoint_name, ignore_body_on_success)
440 local ignore_body_on_success = ignore_body_on_success or false
442 local ep_name = endpoint_name or 'token'
444 return nil, 'no endpoint URI for ' .. ep_name
448 ["Content-Type"] = "application/x-www-form-urlencoded"
452 if auth == "client_secret_basic" then
453 if opts.client_secret then
454 headers.Authorization = "Basic " .. b64(ngx.escape_uri(opts.client_id) .. ":" .. ngx.escape_uri(opts.client_secret))
456 -- client_secret must not be set if Windows Integrated Authentication (WIA) is used with
457 -- Active Directory Federation Services (AD FS) 4.0 (or newer) on Windows Server 2016 (or newer)
458 headers.Authorization = "Basic " .. b64(ngx.escape_uri(opts.client_id) .. ":")
460 log(DEBUG, "client_secret_basic: authorization header '" .. headers.Authorization .. "'")
462 elseif auth == "client_secret_post" then
463 body.client_id = opts.client_id
464 if opts.client_secret then
465 body.client_secret = opts.client_secret
467 log(DEBUG, "client_secret_post: client_id and client_secret being sent in POST body")
469 elseif auth == "private_key_jwt" or auth == "client_secret_jwt" then
470 local key = auth == "private_key_jwt" and opts.client_rsa_private_key or opts.client_secret
472 return nil, "Can't use " .. auth .. " without a key."
474 body.client_id = opts.client_id
475 body.client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
476 local now = ngx.time()
480 alg = auth == "private_key_jwt" and "RS256" or "HS256",
483 iss = opts.client_id,
484 sub = opts.client_id,
486 jti = ngx.var.request_id,
487 exp = now + (opts.client_jwt_assertion_expires_in and opts.client_jwt_assertion_expires_in or 60),
491 if auth == "private_key_jwt" then
492 assertion.header.kid = opts.client_rsa_private_key_id
495 local r_jwt = require("resty.jwt")
496 body.client_assertion = r_jwt:sign(key, assertion)
497 log(DEBUG, auth .. ": client_id, client_assertion_type and client_assertion being sent in POST body")
501 local pass_cookies = opts.pass_cookies
503 if ngx.req.get_headers()["Cookie"] then
505 for cookie_name in string.gmatch(pass_cookies, "%S+") do
506 local cookie_value = ngx.var["cookie_" .. cookie_name]
508 table.insert(t, cookie_name .. "=" .. cookie_value)
511 headers.Cookie = table.concat(t, "; ")
515 log(DEBUG, "request body for " .. ep_name .. " endpoint call: ", ngx.encode_args(body))
517 local httpc = http.new()
518 openidc_configure_timeouts(httpc, opts.timeout)
519 openidc_configure_proxy(httpc, opts.proxy_opts)
520 local res, err = httpc:request_uri(endpoint, decorate_request(opts.http_request_decorator, {
522 body = ngx.encode_args(body),
524 ssl_verify = (opts.ssl_verify ~= "no"),
525 keepalive = (opts.keepalive ~= "no")
528 err = "accessing " .. ep_name .. " endpoint (" .. endpoint .. ") failed: " .. err
533 log(DEBUG, ep_name .. " endpoint response: ", res.body)
535 return openidc_parse_json_response(res, ignore_body_on_success)
538 -- computes access_token expires_in value (in seconds)
539 local function openidc_access_token_expires_in(opts, expires_in)
540 return (expires_in or opts.access_token_expires_in or 3600) - 1 - (opts.access_token_expires_leeway or 0)
543 local function openidc_load_jwt_none_alg(enc_hdr, enc_payload)
544 local header = cjson_s.decode(openidc_base64_url_decode(enc_hdr))
545 local payload = cjson_s.decode(openidc_base64_url_decode(enc_payload))
546 if header and payload and header.alg == "none" then
548 raw_header = enc_hdr,
549 raw_payload = enc_payload,
558 -- get the Discovery metadata from the specified URL
559 local function openidc_discover(url, ssl_verify, keepalive, timeout, exptime, proxy_opts, http_request_decorator)
560 log(DEBUG, "openidc_discover: URL is: " .. url)
563 local v = openidc_cache_get("discovery", url)
566 log(DEBUG, "discovery data not in cache, making call to discovery endpoint")
567 -- make the call to the discovery endpoint
568 local httpc = http.new()
569 openidc_configure_timeouts(httpc, timeout)
570 openidc_configure_proxy(httpc, proxy_opts)
571 local res, error = httpc:request_uri(url, decorate_request(http_request_decorator, {
572 ssl_verify = (ssl_verify ~= "no"),
573 keepalive = (keepalive ~= "no")
576 err = "accessing discovery url (" .. url .. ") failed: " .. error
579 log(DEBUG, "response data: " .. res.body)
580 json, err = openidc_parse_json_response(res)
582 openidc_cache_set("discovery", url, cjson.encode(json), exptime or 24 * 60 * 60)
584 err = "could not decode JSON from Discovery data" .. (err and (": " .. err) or '')
590 json = cjson.decode(v)
596 -- turn a discovery url set in the opts dictionary into the discovered information
597 local function openidc_ensure_discovered_data(opts)
599 if type(opts.discovery) == "string" then
601 discovery, err = openidc_discover(opts.discovery, opts.ssl_verify, opts.keepalive, opts.timeout, opts.discovery_expires_in, opts.proxy_opts,
602 opts.http_request_decorator)
604 opts.discovery = discovery
610 -- make a call to the userinfo endpoint
611 function openidc.call_userinfo_endpoint(opts, access_token)
612 local err = openidc_ensure_discovered_data(opts)
616 if not (opts and opts.discovery and opts.discovery.userinfo_endpoint) then
617 log(DEBUG, "no userinfo endpoint supplied")
622 ["Authorization"] = "Bearer " .. access_token,
625 log(DEBUG, "authorization header '" .. headers.Authorization .. "'")
627 local httpc = http.new()
628 openidc_configure_timeouts(httpc, opts.timeout)
629 openidc_configure_proxy(httpc, opts.proxy_opts)
630 local res, err = httpc:request_uri(opts.discovery.userinfo_endpoint,
631 decorate_request(opts.http_request_decorator, {
633 ssl_verify = (opts.ssl_verify ~= "no"),
634 keepalive = (opts.keepalive ~= "no")
637 err = "accessing (" .. opts.discovery.userinfo_endpoint .. ") failed: " .. err
641 log(DEBUG, "userinfo response: ", res.body)
643 -- parse the response from the user info endpoint
644 return openidc_parse_json_response(res)
647 local function can_use_token_auth_method(method, opts)
648 local supported = supported_token_auth_methods[method]
649 return supported and (type(supported) ~= 'function' or supported(opts))
652 -- get the token endpoint authentication method
653 local function openidc_get_token_auth_method(opts)
655 if opts.token_endpoint_auth_method ~= nil and not can_use_token_auth_method(opts.token_endpoint_auth_method, opts) then
656 log(ERROR, "configured value for token_endpoint_auth_method (" .. opts.token_endpoint_auth_method .. ") is not supported, ignoring it")
657 opts.token_endpoint_auth_method = nil
661 if opts.discovery.token_endpoint_auth_methods_supported ~= nil then
662 -- if set check to make sure the discovery data includes the selected client auth method
663 if opts.token_endpoint_auth_method ~= nil then
664 for index, value in ipairs(opts.discovery.token_endpoint_auth_methods_supported) do
665 log(DEBUG, index .. " => " .. value)
666 if value == opts.token_endpoint_auth_method then
667 log(DEBUG, "configured value for token_endpoint_auth_method (" .. opts.token_endpoint_auth_method .. ") found in token_endpoint_auth_methods_supported in metadata")
668 result = opts.token_endpoint_auth_method
672 if result == nil then
673 log(ERROR, "configured value for token_endpoint_auth_method (" .. opts.token_endpoint_auth_method .. ") NOT found in token_endpoint_auth_methods_supported in metadata")
677 for index, value in ipairs(opts.discovery.token_endpoint_auth_methods_supported) do
678 log(DEBUG, index .. " => " .. value)
679 if can_use_token_auth_method(value, opts) then
681 log(DEBUG, "no configuration setting for option so select the first supported method specified by the OP: " .. result)
687 result = opts.token_endpoint_auth_method
690 -- set a sane default if auto-configuration failed
691 if result == nil then
692 result = "client_secret_basic"
695 log(DEBUG, "token_endpoint_auth_method result set to " .. result)
700 -- ensure that discovery and token auth configuration is available in opts
701 local function ensure_config(opts)
703 err = openidc_ensure_discovered_data(opts)
708 -- set the authentication method for the token endpoint
709 opts.token_endpoint_auth_method = openidc_get_token_auth_method(opts)
712 -- query for discovery endpoint data
713 function openidc.get_discovery_doc(opts)
714 local err = openidc_ensure_discovered_data(opts)
716 log(ERROR, "error getting endpoints definition using discovery endpoint")
719 return opts.discovery, err
722 local function openidc_jwks(url, force, ssl_verify, keepalive, timeout, exptime, proxy_opts, http_request_decorator)
723 log(DEBUG, "openidc_jwks: URL is: " .. url .. " (force=" .. force .. ") (decorator=" .. (http_request_decorator and type(http_request_decorator) or "nil"))
728 v = openidc_cache_get("jwks", url)
733 log(DEBUG, "cannot use cached JWKS data; making call to jwks endpoint")
734 -- make the call to the jwks endpoint
735 local httpc = http.new()
736 openidc_configure_timeouts(httpc, timeout)
737 openidc_configure_proxy(httpc, proxy_opts)
738 local res, error = httpc:request_uri(url, decorate_request(http_request_decorator, {
739 ssl_verify = (ssl_verify ~= "no"),
740 keepalive = (keepalive ~= "no")
743 err = "accessing jwks url (" .. url .. ") failed: " .. error
746 log(DEBUG, "response data: " .. res.body)
747 json, err = openidc_parse_json_response(res)
749 openidc_cache_set("jwks", url, cjson.encode(json), exptime or 24 * 60 * 60)
754 json = cjson.decode(v)
760 local function split_by_chunk(text, chunkSize)
762 for i = 1, #text, chunkSize do
763 s[#s + 1] = text:sub(i, i + chunkSize - 1)
768 local function get_jwk(keys, kid)
771 for _, value in pairs(keys) do
772 if value.kty == "RSA" and (not value.use or value.use == "sig") then
773 table.insert(rsa_keys, value)
778 if #rsa_keys == 1 then
779 log(DEBUG, "returning only RSA key of JWKS for keyid-less JWT")
780 return rsa_keys[1], nil
782 return nil, "JWT doesn't specify kid but the keystore contains multiple RSA keys"
785 for _, value in pairs(rsa_keys) do
786 if value.kid == kid then
791 return nil, "RSA key with id " .. kid .. " not found"
794 local wrap = ('.'):rep(64)
796 local envelope = "-----BEGIN %s-----\n%s\n-----END %s-----\n"
798 local function der2pem(data, typ)
799 typ = typ:upper() or "CERTIFICATE"
801 return string.format(envelope, typ, data:gsub(wrap, '%0\n', (#data - 1) / 64), typ)
805 local function encode_length(length)
806 if length < 0x80 then
807 return string.char(length)
808 elseif length < 0x100 then
809 return string.char(0x81, length)
810 elseif length < 0x10000 then
811 return string.char(0x82, math.floor(length / 0x100), length % 0x100)
813 error("Can't encode lengths over 65535")
817 local function encode_sequence(array, of)
818 local encoded_array = array
822 encoded_array[i] = of(array[i])
825 encoded_array = table.concat(encoded_array)
827 return string.char(0x30) .. encode_length(#encoded_array) .. encoded_array
830 local function encode_binary_integer(bytes)
831 if bytes:byte(1) > 127 then
832 -- We currenly only use this for unsigned integers,
833 -- however since the high bit is set here, it would look
834 -- like a negative signed int, so prefix with zeroes
835 bytes = "\0" .. bytes
837 return "\2" .. encode_length(#bytes) .. bytes
840 local function encode_sequence_of_integer(array)
841 return encode_sequence(array, encode_binary_integer)
844 local function encode_bit_string(array)
845 local s = "\0" .. array -- first octet holds the number of unused bits
846 return "\3" .. encode_length(#s) .. s
849 local function openidc_pem_from_x5c(x5c)
850 log(DEBUG, "Found x5c, getting PEM public key from x5c entry of json public key")
851 local chunks = split_by_chunk(b64(openidc_base64_url_decode(x5c[1])), 64)
852 local pem = "-----BEGIN CERTIFICATE-----\n" ..
853 table.concat(chunks, "\n") ..
854 "\n-----END CERTIFICATE-----"
855 log(DEBUG, "Generated PEM key from x5c:", pem)
859 local function openidc_pem_from_rsa_n_and_e(n, e)
860 log(DEBUG, "getting PEM public key from n and e parameters of json public key")
863 openidc_base64_url_decode(n), openidc_base64_url_decode(e)
865 local encoded_key = encode_sequence_of_integer(der_key)
866 local pem = der2pem(encode_sequence({
868 "\6\9\42\134\72\134\247\13\1\1\1" -- OID :rsaEncryption
869 .. "\5\0" -- ASN.1 NULL of length 0
871 encode_bit_string(encoded_key)
873 log(DEBUG, "Generated pem key from n and e: ", pem)
877 local function openidc_pem_from_jwk(opts, kid)
878 local err = openidc_ensure_discovered_data(opts)
883 if not opts.discovery.jwks_uri or not (type(opts.discovery.jwks_uri) == "string") or (opts.discovery.jwks_uri == "") then
884 return nil, "opts.discovery.jwks_uri is not present or not a string"
887 local cache_id = opts.discovery.jwks_uri .. '#' .. (kid or '')
888 local v = openidc_cache_get("jwks", cache_id)
897 jwks, err = openidc_jwks(opts.discovery.jwks_uri, force, opts.ssl_verify, opts.keepalive, opts.timeout, opts.jwk_expires_in, opts.proxy_opts,
898 opts.http_request_decorator)
903 jwk, err = get_jwk(jwks.keys, kid)
905 if jwk and not err then
915 if x5c and #(jwk.x5c) == 0 then
916 log(WARN, "Found invalid JWK with empty x5c array, ignoring x5c claim")
922 pem = openidc_pem_from_x5c(x5c)
923 elseif jwk.kty == "RSA" and jwk.n and jwk.e then
924 pem = openidc_pem_from_rsa_n_and_e(jwk.n, jwk.e)
926 return nil, "don't know how to create RSA key/cert for " .. cjson.encode(jwk)
929 openidc_cache_set("jwks", cache_id, pem, opts.jwk_expires_in or 24 * 60 * 60)
933 -- does lua-resty-jwt and/or we know how to handle the algorithm of the JWT?
934 local function is_algorithm_supported(jwt_header)
935 return jwt_header and jwt_header.alg and (jwt_header.alg == "none"
936 or string.sub(jwt_header.alg, 1, 2) == "RS"
937 or string.sub(jwt_header.alg, 1, 2) == "HS")
940 -- is the JWT signing algorithm an asymmetric one whose key might be
941 -- obtained from the discovery endpoint?
942 local function uses_asymmetric_algorithm(jwt_header)
943 return string.sub(jwt_header.alg, 1, 2) == "RS"
946 -- is the JWT signing algorithm one that has been expected?
947 local function is_algorithm_expected(jwt_header, expected_algs)
948 if expected_algs == nil or not jwt_header or not jwt_header.alg then
951 if type(expected_algs) == 'string' then
952 expected_algs = { expected_algs }
954 for _, alg in ipairs(expected_algs) do
955 if alg == jwt_header.alg then
962 -- parse a JWT and verify its signature (if present)
963 local function openidc_load_jwt_and_verify_crypto(opts, jwt_string, asymmetric_secret,
964 symmetric_secret, expected_algs, ...)
965 local r_jwt = require("resty.jwt")
966 local enc_hdr, enc_payload, enc_sign = string.match(jwt_string, '^(.+)%.(.+)%.(.*)$')
967 if enc_payload and (not enc_sign or enc_sign == "") then
968 local jwt = openidc_load_jwt_none_alg(enc_hdr, enc_payload)
970 if opts.accept_none_alg then
971 log(DEBUG, "accept JWT with alg \"none\" and no signature")
974 return jwt, "token uses \"none\" alg but accept_none_alg is not enabled"
976 end -- otherwise the JWT is invalid and load_jwt produces an error
979 local jwt_obj = r_jwt:load_jwt(jwt_string, nil)
980 if not jwt_obj.valid then
981 local reason = "invalid jwt"
982 if jwt_obj.reason then
983 reason = reason .. ": " .. jwt_obj.reason
988 if not is_algorithm_expected(jwt_obj.header, expected_algs) then
989 local alg = jwt_obj.header and jwt_obj.header.alg or "no algorithm at all"
990 return nil, "token is signed by unexpected algorithm \"" .. alg .. "\""
994 if is_algorithm_supported(jwt_obj.header) then
995 if uses_asymmetric_algorithm(jwt_obj.header) then
997 log(WARN, "using deprecated option `opts.secret` for asymmetric key; switch to `opts.public_key` instead")
999 secret = asymmetric_secret or opts.secret
1000 if not secret and opts.discovery then
1001 log(DEBUG, "using discovery to find key")
1003 secret, err = openidc_pem_from_jwk(opts, jwt_obj.header.kid)
1005 if secret == nil then
1012 log(WARN, "using deprecated option `opts.secret` for symmetric key; switch to `opts.symmetric_key` instead")
1014 secret = symmetric_secret or opts.secret
1018 if #{ ... } == 0 then
1019 -- an empty list of claim specs makes lua-resty-jwt add default
1020 -- validators for the exp and nbf claims if they are
1021 -- present. These validators need to know the configured slack
1023 local jwt_validators = require("resty.jwt-validators")
1024 jwt_validators.set_system_leeway(opts.iat_slack and opts.iat_slack or 120)
1027 jwt_obj = r_jwt:verify_jwt_obj(secret, jwt_obj, ...)
1029 log(DEBUG, "jwt: ", cjson.encode(jwt_obj), " ,valid: ", jwt_obj.valid, ", verified: ", jwt_obj.verified)
1031 if not jwt_obj.verified then
1032 local reason = "jwt signature verification failed"
1033 if jwt_obj.reason then
1034 reason = reason .. ": " .. jwt_obj.reason
1036 return jwt_obj, reason
1042 -- Load and validate id token from the id_token properties of the token endpoint response
1044 -- - opts the openidc module options
1045 -- - jwt_id_token the id_token from the id_token properties of the token endpoint response
1046 -- - session the current session
1047 -- Return the id_token, nil if valid
1048 -- Return nil, the error if invalid
1050 local function openidc_load_and_validate_jwt_id_token(opts, jwt_id_token, session)
1052 local jwt_obj, err = openidc_load_jwt_and_verify_crypto(opts, jwt_id_token, opts.public_key, opts.client_secret,
1053 opts.discovery.id_token_signing_alg_values_supported)
1055 local alg = (jwt_obj and jwt_obj.header and jwt_obj.header.alg) or ''
1056 local is_unsupported_signature_error = jwt_obj and not jwt_obj.verified and not is_algorithm_supported(jwt_obj.header)
1057 if is_unsupported_signature_error then
1058 if opts.accept_unsupported_alg == nil or opts.accept_unsupported_alg then
1059 log(WARN, "ignored id_token signature as algorithm '" .. alg .. "' is not supported")
1061 err = "token is signed using algorithm \"" .. alg .. "\" which is not supported by lua-resty-jwt"
1066 log(ERROR, "id_token '" .. alg .. "' signature verification failed")
1070 local id_token = jwt_obj.payload
1072 log(DEBUG, "id_token header: ", cjson.encode(jwt_obj.header))
1073 log(DEBUG, "id_token payload: ", cjson.encode(jwt_obj.payload))
1075 -- validate the id_token contents
1076 if openidc_validate_id_token(opts, id_token, session.data.nonce) == false then
1077 err = "id_token validation failed"
1085 -- handle a "code" authorization response from the OP
1086 local function openidc_authorization_response(opts, session)
1087 local args = ngx.req.get_uri_args()
1088 local err, log_err, client_err
1090 if not args.code or not args.state then
1091 err = "unhandled request to the redirect_uri: " .. ngx.var.request_uri
1093 return nil, err, session.data.original_url, session
1096 -- check that the state returned in the response against the session; prevents CSRF
1097 if args.state ~= session.data.state then
1098 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")
1099 client_err = "state from argument does not match state restored from session"
1101 return nil, client_err, session.data.original_url, session
1104 err = ensure_config(opts)
1106 return nil, err, session.data.original_url, session
1109 -- check the iss if returned from the OP
1110 if args.iss and args.iss ~= opts.discovery.issuer then
1111 log_err = "iss from argument: " .. args.iss .. " does not match expected issuer: " .. opts.discovery.issuer
1112 client_err = "iss from argument does not match expected issuer"
1114 return nil, client_err, session.data.original_url, session
1117 -- check the client_id if returned from the OP
1118 if args.client_id and args.client_id ~= opts.client_id then
1119 log_err = "client_id from argument: " .. args.client_id .. " does not match expected client_id: " .. opts.client_id
1120 client_err = "client_id from argument does not match expected client_id"
1122 return nil, client_err, session.data.original_url, session
1125 -- assemble the parameters to the token endpoint
1127 grant_type = "authorization_code",
1129 redirect_uri = openidc_get_redirect_uri(opts, session),
1130 state = session.data.state,
1131 code_verifier = session.data.code_verifier
1134 log(DEBUG, "Authentication with OP done -> Calling OP Token Endpoint to obtain tokens")
1136 local current_time = ngx.time()
1137 -- make the call to the token endpoint
1139 json, err = openidc.call_token_endpoint(opts, opts.discovery.token_endpoint, body, opts.token_endpoint_auth_method)
1141 return nil, err, session.data.original_url, session
1144 local id_token, err = openidc_load_and_validate_jwt_id_token(opts, json.id_token, session);
1146 return nil, err, session.data.original_url, session
1149 -- mark this sessions as authenticated
1150 session.data.authenticated = true
1151 -- clear state, nonce and code_verifier to protect against potential misuse
1152 session.data.nonce = nil
1153 session.data.state = nil
1154 session.data.code_verifier = nil
1155 if store_in_session(opts, 'id_token') then
1156 session.data.id_token = id_token
1159 if store_in_session(opts, 'user') then
1160 -- call the user info endpoint
1161 -- TODO: should this error be checked?
1163 user, err = openidc.call_userinfo_endpoint(opts, json.access_token)
1166 log(ERROR, "error calling userinfo endpoint: " .. err)
1168 if id_token.sub ~= user.sub then
1169 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") .. "\")"
1172 session.data.user = user
1177 if store_in_session(opts, 'enc_id_token') then
1178 session.data.enc_id_token = json.id_token
1181 if store_in_session(opts, 'access_token') then
1182 session.data.access_token = json.access_token
1183 session.data.access_token_expiration = current_time
1184 + openidc_access_token_expires_in(opts, json.expires_in)
1185 if json.refresh_token ~= nil then
1186 session.data.refresh_token = json.refresh_token
1190 if opts.lifecycle and opts.lifecycle.on_authenticated then
1191 err = opts.lifecycle.on_authenticated(session, id_token, json)
1193 log(WARN, "failed in `on_authenticated` handler: " .. err)
1194 return nil, err, session.data.original_url, session
1198 -- save the session with the obtained id_token
1201 -- redirect to the URL that was accessed originally
1202 log(DEBUG, "OIDC Authorization Code Flow completed -> Redirecting to original URL (" .. session.data.original_url .. ")")
1203 ngx.redirect(session.data.original_url)
1204 return nil, nil, session.data.original_url, session
1207 -- token revocation (RFC 7009)
1208 local function openidc_revoke_token(opts, token_type_hint, token)
1209 if not opts.discovery.revocation_endpoint then
1210 log(DEBUG, "no revocation endpoint supplied. unable to revoke " .. token_type_hint .. ".")
1214 local token_type_hint = token_type_hint or nil
1218 if token_type_hint then
1219 body['token_type_hint'] = token_type_hint
1221 local token_type_log = token_type_hint or 'token'
1223 -- ensure revocation endpoint auth method is properly discovered
1224 local err = ensure_config(opts)
1226 log(ERROR, "revocation of " .. token_type_log .. " unsuccessful: " .. err)
1230 -- call the revocation endpoint
1232 _, err = openidc.call_token_endpoint(opts, opts.discovery.revocation_endpoint, body, opts.token_endpoint_auth_method, "revocation", true)
1234 log(ERROR, "revocation of " .. token_type_log .. " unsuccessful: " .. err)
1237 log(DEBUG, "revocation of " .. token_type_log .. " successful")
1242 function openidc.revoke_token(opts, token_type_hint, token)
1243 local err = openidc_ensure_discovered_data(opts)
1245 log(ERROR, "revocation of " .. (token_type_hint or "token (no type specified)") .. " unsuccessful: " .. err)
1249 return openidc_revoke_token(opts, token_type_hint, token)
1252 function openidc.revoke_tokens(opts, session)
1253 local err = openidc_ensure_discovered_data(opts)
1255 log(ERROR, "revocation of tokens unsuccessful: " .. err)
1259 local access_token = session.data.access_token
1260 local refresh_token = session.data.refresh_token
1262 local access_token_revoke, refresh_token_revoke
1263 if refresh_token then
1264 access_token_revoke = openidc_revoke_token(opts, "refresh_token", refresh_token)
1266 if access_token then
1267 refresh_token_revoke = openidc_revoke_token(opts, "access_token", access_token)
1269 return access_token_revoke and refresh_token_revoke
1272 local openidc_transparent_pixel = "\137\080\078\071\013\010\026\010\000\000\000\013\073\072\068\082" ..
1273 "\000\000\000\001\000\000\000\001\008\004\000\000\000\181\028\012" ..
1274 "\002\000\000\000\011\073\068\065\084\120\156\099\250\207\000\000" ..
1275 "\002\007\001\002\154\028\049\113\000\000\000\000\073\069\078\068" ..
1279 local function openidc_logout(opts, session)
1280 local session_token = session.data.enc_id_token
1281 local access_token = session.data.access_token
1282 local refresh_token = session.data.refresh_token
1285 if opts.lifecycle and opts.lifecycle.on_logout then
1286 err = opts.lifecycle.on_logout(session)
1288 log(WARN, "failed in `on_logout` handler: " .. err)
1295 if opts.revoke_tokens_on_logout then
1296 log(DEBUG, "revoke_tokens_on_logout is enabled. " ..
1297 "trying to revoke access and refresh tokens...")
1298 if refresh_token then
1299 openidc_revoke_token(opts, "refresh_token", refresh_token)
1301 if access_token then
1302 openidc_revoke_token(opts, "access_token", access_token)
1306 local headers = ngx.req.get_headers()
1307 local header = get_first(headers['Accept'])
1308 if header and header:find("image/png") then
1309 ngx.header["Cache-Control"] = "no-cache, no-store"
1310 ngx.header["Pragma"] = "no-cache"
1311 ngx.header["P3P"] = "CAO PSA OUR"
1312 ngx.header["Expires"] = "0"
1313 ngx.header["X-Frame-Options"] = "DENY"
1314 ngx.header.content_type = "image/png"
1315 ngx.print(openidc_transparent_pixel)
1318 elseif opts.redirect_after_logout_uri or opts.discovery.end_session_endpoint then
1320 if opts.redirect_after_logout_uri then
1321 uri = opts.redirect_after_logout_uri
1323 uri = opts.discovery.end_session_endpoint
1326 if (opts.redirect_after_logout_with_id_token_hint or not opts.redirect_after_logout_uri) and session_token then
1327 params["id_token_hint"] = session_token
1329 if opts.post_logout_redirect_uri then
1330 params["post_logout_redirect_uri"] = opts.post_logout_redirect_uri
1332 return ngx.redirect(openidc_combine_uri(uri, params))
1333 elseif opts.discovery.ping_end_session_endpoint then
1335 if opts.post_logout_redirect_uri then
1336 params["TargetResource"] = opts.post_logout_redirect_uri
1338 return ngx.redirect(openidc_combine_uri(opts.discovery.ping_end_session_endpoint, params))
1341 ngx.header.content_type = "text/html"
1342 ngx.say("<html><body>Logged Out</body></html>")
1346 -- returns a valid access_token (eventually refreshing the token)
1347 local function openidc_access_token(opts, session, try_to_renew)
1351 if session.data.access_token == nil then
1354 local current_time = ngx.time()
1355 if current_time < session.data.access_token_expiration then
1356 return session.data.access_token, err
1358 if not try_to_renew then
1359 return nil, "token expired"
1361 if session.data.refresh_token == nil then
1362 return nil, "token expired and no refresh token available"
1365 log(DEBUG, "refreshing expired access_token: ", session.data.access_token, " with: ", session.data.refresh_token)
1367 -- retrieve token endpoint URL from discovery endpoint if necessary
1368 err = ensure_config(opts)
1373 -- assemble the parameters to the token endpoint
1375 grant_type = "refresh_token",
1376 refresh_token = session.data.refresh_token,
1377 scope = opts.scope and opts.scope or "openid email profile"
1381 json, err = openidc.call_token_endpoint(opts, opts.discovery.token_endpoint, body, opts.token_endpoint_auth_method)
1386 if json.id_token then
1387 id_token, err = openidc_load_and_validate_jwt_id_token(opts, json.id_token, session)
1389 log(ERROR, "invalid id token, discarding tokens returned while refreshing")
1393 log(DEBUG, "access_token refreshed: ", json.access_token, " updated refresh_token: ", json.refresh_token)
1395 session.data.access_token = json.access_token
1396 session.data.access_token_expiration = current_time + openidc_access_token_expires_in(opts, json.expires_in)
1397 if json.refresh_token then
1398 session.data.refresh_token = json.refresh_token
1401 if json.id_token and
1402 (store_in_session(opts, 'enc_id_token') or store_in_session(opts, 'id_token')) then
1403 log(DEBUG, "id_token refreshed: ", json.id_token)
1404 if store_in_session(opts, 'enc_id_token') then
1405 session.data.enc_id_token = json.id_token
1407 if store_in_session(opts, 'id_token') then
1408 session.data.id_token = id_token
1412 -- save the session with the new access_token and optionally the new refresh_token and id_token using a new sessionid
1414 regenerated, err = session:regenerate()
1416 log(ERROR, "failed to regenerate session: " .. err)
1419 if opts.lifecycle and opts.lifecycle.on_regenerated then
1420 err = opts.lifecycle.on_regenerated(session)
1422 log(WARN, "failed in `on_regenerated` handler: " .. err)
1427 return session.data.access_token, err
1430 local function openidc_get_path(uri)
1431 local without_query = uri:match("(.-)%?") or uri
1432 return without_query:match(".-//[^/]+(/.*)") or without_query
1435 local function openidc_get_redirect_uri_path(opts)
1436 return opts.redirect_uri and openidc_get_path(opts.redirect_uri) or opts.redirect_uri_path
1439 local function is_session(o)
1440 return o ~= nil and o.start and type(o.start) == "function"
1443 -- main routine for OpenID Connect user authentication
1444 function openidc.authenticate(opts, target_url, unauth_action, session_or_opts)
1446 if opts.redirect_uri_path then
1447 log(WARN, "using deprecated option `opts.redirect_uri_path`; switch to using an absolute URI and `opts.redirect_uri` instead")
1453 if is_session(session_or_opts) then
1454 session = session_or_opts
1457 session, session_error = r_session.start(session_or_opts)
1458 if session == nil then
1459 log(ERROR, "Error starting session: " .. session_error)
1460 return nil, session_error, target_url, session
1464 target_url = target_url or ngx.var.request_uri
1468 -- see if this is a request to the redirect_uri i.e. an authorization response
1469 local path = openidc_get_path(target_url)
1470 if path == openidc_get_redirect_uri_path(opts) then
1471 log(DEBUG, "Redirect URI path (" .. path .. ") is currently navigated -> Processing authorization response coming from OP")
1473 if not session.present then
1474 err = "request to the redirect_uri path but there's no session state found"
1476 return nil, err, target_url, session
1479 return openidc_authorization_response(opts, session)
1482 -- see if this is a request to logout
1483 if path == (opts.logout_path or "/logout") then
1484 log(DEBUG, "Logout path (" .. path .. ") is currently navigated -> Processing local session removal before redirecting to next step of logout process")
1486 err = ensure_config(opts)
1488 return nil, err, session.data.original_url, session
1491 openidc_logout(opts, session)
1492 return nil, nil, target_url, session
1495 local token_expired = false
1496 local try_to_renew = opts.renew_access_token_on_expiry == nil or opts.renew_access_token_on_expiry
1497 if session.present and session.data.authenticated
1498 and store_in_session(opts, 'access_token') then
1500 -- refresh access_token if necessary
1501 access_token, err = openidc_access_token(opts, session, try_to_renew)
1503 log(ERROR, "lost access token:" .. err)
1506 if not access_token then
1507 token_expired = true
1512 "session.present=", session.present,
1513 ", session.data.id_token=", session.data.id_token ~= nil,
1514 ", session.data.authenticated=", session.data.authenticated,
1515 ", opts.force_reauthorize=", opts.force_reauthorize,
1516 ", opts.renew_access_token_on_expiry=", opts.renew_access_token_on_expiry,
1517 ", try_to_renew=", try_to_renew,
1518 ", token_expired=", token_expired)
1520 -- if we are not authenticated then redirect to the OP for authentication
1521 -- the presence of the id_token is check for backwards compatibility
1522 if not session.present
1523 or not (session.data.id_token or session.data.authenticated)
1524 or opts.force_reauthorize
1525 or (try_to_renew and token_expired) then
1526 if unauth_action == "pass" then
1527 if token_expired then
1528 session.data.authenticated = false
1529 return nil, 'token refresh failed', target_url, session
1531 return nil, err, target_url, session
1533 if unauth_action == 'deny' then
1534 return nil, 'unauthorized request', target_url, session
1537 err = ensure_config(opts)
1539 return nil, err, session.data.original_url, session
1542 log(DEBUG, "Authentication is required - Redirecting to OP Authorization endpoint")
1543 openidc_authorize(opts, session, target_url, opts.prompt)
1544 return nil, nil, target_url, session
1547 -- silently reauthenticate if necessary (mainly used for session refresh/getting updated id_token data)
1548 if opts.refresh_session_interval ~= nil then
1549 if session.data.last_authenticated == nil or (session.data.last_authenticated + opts.refresh_session_interval) < ngx.time() then
1550 err = ensure_config(opts)
1552 return nil, err, session.data.original_url, session
1555 log(DEBUG, "Silent authentication is required - Redirecting to OP Authorization endpoint")
1556 openidc_authorize(opts, session, target_url, "none")
1557 return nil, nil, target_url, session
1561 if store_in_session(opts, 'id_token') then
1562 -- log id_token contents
1563 log(DEBUG, "id_token=", cjson.encode(session.data.id_token))
1566 -- return the id_token to the caller Lua script for access control purposes
1569 id_token = session.data.id_token,
1570 access_token = access_token,
1571 user = session.data.user
1578 -- get a valid access_token (eventually refreshing the token), or nil if there's no valid access_token
1579 function openidc.access_token(opts, session_opts)
1581 local session = r_session.start(session_opts)
1582 local token, err = openidc_access_token(opts, session, true)
1588 -- get an OAuth 2.0 bearer access token from the HTTP request cookies
1589 local function openidc_get_bearer_access_token_from_cookie(opts)
1593 log(DEBUG, "getting bearer access token from Cookie")
1595 local accept_token_as = opts.auth_accept_token_as or "header"
1596 if accept_token_as:find("cookie") ~= 1 then
1597 return nil, "openidc_get_bearer_access_token_from_cookie called but auth_accept_token_as wants "
1598 .. opts.auth_accept_token_as
1600 local divider = accept_token_as:find(':')
1601 local cookie_name = divider and accept_token_as:sub(divider + 1) or "PA.global"
1603 log(DEBUG, "bearer access token from cookie named: " .. cookie_name)
1605 local cookies = ngx.req.get_headers()["Cookie"]
1607 err = "no Cookie header found"
1612 local cookie_value = ngx.var["cookie_" .. cookie_name]
1613 if not cookie_value then
1614 err = "no Cookie " .. cookie_name .. " found"
1618 return cookie_value, err
1622 -- get an OAuth 2.0 bearer access token from the HTTP request
1623 local function openidc_get_bearer_access_token(opts)
1627 local accept_token_as = opts.auth_accept_token_as or "header"
1629 if accept_token_as:find("cookie") == 1 then
1630 return openidc_get_bearer_access_token_from_cookie(opts)
1633 -- get the access token from the Authorization header
1634 local headers = ngx.req.get_headers()
1635 local header_name = opts.auth_accept_token_as_header_name or "Authorization"
1636 local header = get_first(headers[header_name])
1638 if header == nil or header:find(" ") == nil then
1639 err = "no Authorization header found"
1644 local divider = header:find(' ')
1645 if string.lower(header:sub(0, divider - 1)) ~= string.lower("Bearer") then
1646 err = "no Bearer authorization header value found"
1651 local access_token = header:sub(divider + 1)
1652 if access_token == nil then
1653 err = "no Bearer access token value found"
1658 return access_token, err
1661 local function get_introspection_endpoint(opts)
1662 local introspection_endpoint = opts.introspection_endpoint
1663 if not introspection_endpoint then
1664 local err = openidc_ensure_discovered_data(opts)
1666 return nil, "opts.introspection_endpoint not said and " .. err
1668 local endpoint = opts.discovery and opts.discovery.introspection_endpoint
1673 return introspection_endpoint
1676 local function get_introspection_cache_prefix(opts)
1677 return (opts.cache_segment and opts.cache_segment.gsub(',', '_') or 'DEFAULT') .. ','
1678 .. (get_introspection_endpoint(opts) or 'nil-endpoint') .. ','
1679 .. (opts.client_id or 'no-client_id') .. ','
1680 .. (opts.client_secret and 'secret' or 'no-client_secret') .. ':'
1683 local function get_cached_introspection(opts, access_token)
1684 local introspection_cache_ignore = opts.introspection_cache_ignore or false
1685 if not introspection_cache_ignore then
1686 return openidc_cache_get("introspection",
1687 get_introspection_cache_prefix(opts) .. access_token)
1691 local function set_cached_introspection(opts, access_token, encoded_json, ttl)
1692 local introspection_cache_ignore = opts.introspection_cache_ignore or false
1693 if not introspection_cache_ignore then
1694 openidc_cache_set("introspection",
1695 get_introspection_cache_prefix(opts) .. access_token,
1700 -- main routine for OAuth 2.0 token introspection
1701 function openidc.introspect(opts)
1703 -- get the access token from the request
1704 local access_token, err = openidc_get_bearer_access_token(opts)
1705 if access_token == nil then
1709 -- see if we've previously cached the introspection result for this access token
1711 local v = get_cached_introspection(opts, access_token)
1714 json = cjson.decode(v)
1718 -- assemble the parameters to the introspection (token) endpoint
1719 local token_param_name = opts.introspection_token_param_name and opts.introspection_token_param_name or "token"
1723 body[token_param_name] = access_token
1725 if opts.client_id then
1726 body.client_id = opts.client_id
1728 if opts.client_secret then
1729 body.client_secret = opts.client_secret
1732 -- merge any provided extra parameters
1733 if opts.introspection_params then
1734 for key, val in pairs(opts.introspection_params) do body[key] = val end
1737 -- call the introspection endpoint
1738 local introspection_endpoint
1739 introspection_endpoint, err = get_introspection_endpoint(opts)
1743 json, err = openidc.call_token_endpoint(opts, introspection_endpoint, body, opts.introspection_endpoint_auth_method, "introspection")
1750 if not json.active then
1751 err = "invalid token"
1755 -- cache the results
1756 local introspection_cache_ignore = opts.introspection_cache_ignore or false
1757 local expiry_claim = opts.introspection_expiry_claim or "exp"
1759 if not introspection_cache_ignore and json[expiry_claim] then
1760 local introspection_interval = opts.introspection_interval or 0
1761 local ttl = json[expiry_claim]
1762 if expiry_claim == "exp" then --https://tools.ietf.org/html/rfc7662#section-2.2
1763 ttl = ttl - ngx.time()
1765 if introspection_interval > 0 then
1766 if ttl > introspection_interval then
1767 ttl = introspection_interval
1770 log(DEBUG, "cache token ttl: " .. ttl)
1771 set_cached_introspection(opts, access_token, cjson.encode(json), ttl)
1778 local function get_jwt_verification_cache_prefix(opts)
1779 local signing_alg_values_expected = (opts.accept_none_alg and 'none' or 'no-none')
1780 local expected_algs = opts.token_signing_alg_values_expected or {}
1781 if type(expected_algs) == 'string' then
1782 expected_algs = { expected_algs }
1784 for _, alg in ipairs(expected_algs) do
1785 signing_alg_values_expected = signing_alg_values_expected .. ',' .. alg
1787 return (opts.cache_segment and opts.cache_segment.gsub(',', '_') or 'DEFAULT') .. ','
1788 .. (opts.public_key or 'no-pubkey') .. ','
1789 .. (opts.symmetric_key or 'no-symkey') .. ','
1790 .. signing_alg_values_expected .. ':'
1793 local function get_cached_jwt_verification(opts, access_token)
1794 local jwt_verification_cache_ignore = opts.jwt_verification_cache_ignore or false
1795 if not jwt_verification_cache_ignore then
1796 return openidc_cache_get("jwt_verification",
1797 get_jwt_verification_cache_prefix(opts) .. access_token)
1801 local function set_cached_jwt_verification(opts, access_token, encoded_json, ttl)
1802 local jwt_verification_cache_ignore = opts.jwt_verification_cache_ignore or false
1803 if not jwt_verification_cache_ignore then
1804 openidc_cache_set("jwt_verification",
1805 get_jwt_verification_cache_prefix(opts) .. access_token,
1810 -- main routine for OAuth 2.0 JWT token validation
1811 -- optional args are claim specs, see jwt-validators in resty.jwt
1812 function openidc.jwt_verify(access_token, opts, ...)
1815 local v = get_cached_jwt_verification(opts, access_token)
1817 local slack = opts.iat_slack and opts.iat_slack or 120
1820 jwt_obj, err = openidc_load_jwt_and_verify_crypto(opts, access_token, opts.public_key, opts.symmetric_key,
1821 opts.token_signing_alg_values_expected, ...)
1823 json = jwt_obj.payload
1824 local encoded_json = cjson.encode(json)
1825 log(DEBUG, "jwt: ", encoded_json)
1827 set_cached_jwt_verification(opts, access_token, encoded_json,
1828 json.exp and json.exp - ngx.time() or 120)
1832 -- decode from the cache
1833 json = cjson.decode(v)
1836 -- check the token expiry
1838 if json.exp and json.exp + slack < ngx.time() then
1839 log(ERROR, "token expired: json.exp=", json.exp, ", ngx.time()=", ngx.time())
1847 function openidc.bearer_jwt_verify(opts, ...)
1850 -- get the access token from the request
1851 local access_token, err = openidc_get_bearer_access_token(opts)
1852 if access_token == nil then
1856 log(DEBUG, "access_token: ", access_token)
1858 json, err = openidc.jwt_verify(access_token, opts, ...)
1859 return json, err, access_token
1862 -- Passing nil to any of the arguments resets the configuration to default
1863 function openidc.set_logging(new_log, new_levels)
1864 log = new_log and new_log or ngx.log
1865 DEBUG = new_levels.DEBUG and new_levels.DEBUG or ngx.DEBUG
1866 ERROR = new_levels.ERROR and new_levels.ERROR or ngx.ERR
1867 WARN = new_levels.WARN and new_levels.WARN or ngx.WARN