Merge "Change role prefix from onap_ to portal_ [ui]"
[portal-ng/ui.git] / server / resty / openidc.lua
1 --[[
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
9
10   http://www.apache.org/licenses/LICENSE-2.0
11
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
17 under the License.
18
19 ***************************************************************************
20 Copyright (C) 2017-2019 ZmartZone IAM
21 Copyright (C) 2015-2017 Ping Identity Corporation
22 All rights reserved.
23
24 For further information please contact:
25
26      Ping Identity Corporation
27      1099 18th St Suite 2950
28      Denver, CO 80202
29      303.468.2900
30      http://www.pingidentity.com
31
32 DISCLAIMER OF WARRANTIES:
33
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.
47
48 @Author: Hans Zandbelt - hans.zandbelt@zmartzone.eu
49 --]]
50
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")
56 local string = string
57 local ipairs = ipairs
58 local pairs = pairs
59 local type = type
60 local ngx = ngx
61 local b64 = ngx.encode_base64
62 local unb64 = ngx.decode_base64
63
64 local log = ngx.log
65 local DEBUG = ngx.DEBUG
66 local ERROR = ngx.ERR
67 local WARN = ngx.WARN
68
69 local function token_auth_method_precondition(method, required_field)
70   return function(opts)
71     if not opts[required_field] then
72       log(DEBUG, "Can't use " .. method .. " without opts." .. required_field)
73       return false
74     end
75     return true
76   end
77 end
78
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')
84 }
85
86 local openidc = {
87   _VERSION = "1.7.5"
88 }
89 openidc.__index = openidc
90
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
94     return true
95   end
96
97   return opts.session_contents[feature]
98 end
99
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)
106   end
107 end
108
109 -- retrieve value from server-wide cache if available
110 local function openidc_cache_get(type, key)
111   local dict = ngx.shared[type]
112   local value
113   if dict then
114     value = dict:get(key)
115     if value then log(DEBUG, "cache hit: type=", type, " key=", key) end
116   end
117   return value
118 end
119
120 -- invalidate values of server-wide cache
121 local function openidc_cache_invalidate(type)
122   local dict = ngx.shared[type]
123   if dict then
124     log(DEBUG, "flushing cache for " .. type)
125     dict.flush_all(dict)
126     local nbr = dict.flush_expired(dict)
127   end
128 end
129
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")
136 end
137
138 -- validate the contents of and id_token
139 local function openidc_validate_id_token(opts, id_token, nonce)
140
141   -- check issuer
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, "\"")
144     return false
145   end
146
147   -- check sub
148   if not id_token.sub then
149     log(ERROR, "no \"sub\" claim found in id_token")
150     return false
151   end
152
153   -- check nonce
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, "\"")
156     return false
157   end
158
159   -- check issued-at timestamp
160   if not id_token.iat then
161     log(ERROR, "no \"iat\" claim found in id_token")
162     return false
163   end
164
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)
168     return false
169   end
170
171   -- check expiry timestamp
172   if not id_token.exp then
173     log(ERROR, "no \"exp\" claim found in id_token")
174     return false
175   end
176
177   if (id_token.exp + slack) < ngx.time() then
178     log(ERROR, "token expired: id_token.exp=", id_token.exp, ", ngx.time()=", ngx.time())
179     return false
180   end
181
182   -- check audience (array or string)
183   if not id_token.aud then
184     log(ERROR, "no \"aud\" claim found in id_token")
185     return false
186   end
187
188   if (type(id_token.aud) == "table") then
189     for _, value in pairs(id_token.aud) do
190       if value == opts.client_id then
191         return true
192       end
193     end
194     log(ERROR, "no match found token audience array: client_id=", opts.client_id)
195     return false
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)
199       return false
200     end
201   end
202   return true
203 end
204
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]
209   end
210   return res
211 end
212
213 local function get_first_header(headers, header_name)
214   local header = headers[header_name]
215   return get_first(header)
216 end
217
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', '')
221 end
222
223 local function get_forwarded_parameter(headers, param_name)
224   local forwarded = get_first_header(headers, 'Forwarded')
225   local params = {}
226   if forwarded then
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)
232         end
233         params[name:lower()] = value
234       end
235     end
236
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
240     -- are extensions.
241     local first_part = forwarded
242     local first_comma = forwarded:find("%s*,%s*")
243     if first_comma then
244       first_part = forwarded:sub(1, first_comma - 1)
245     end
246     first_part:gsub("[^;]+", parse_parameter)
247   end
248   return params[param_name:gsub("^%s*(.-)%s*$", "%1"):lower()]
249 end
250
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')
254       or ngx.var.scheme
255 end
256
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')
260 end
261
262 local function get_host_name(headers)
263   return get_forwarded_parameter(headers, 'host')
264       or get_host_name_from_x_header(headers)
265       or ngx.var.http_host
266 end
267
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
274     else
275       return opts.redirect_uri
276     end
277   end
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)
281   if not host then
282     -- possibly HTTP 1.0 and no Host header
283     if session then session:close() end
284     ngx.exit(ngx.HTTP_BAD_REQUEST)
285   end
286   return scheme .. "://" .. host .. path
287 end
288
289 -- perform base64url decoding
290 local function openidc_base64_url_decode(input)
291   local reminder = #input % 4
292   if reminder > 0 then
293     local padlen = 4 - reminder
294     input = input .. string.rep('=', padlen)
295   end
296   input = input:gsub('%-', '+'):gsub('_', '/')
297   return unb64(input)
298 end
299
300 -- perform base64url encoding
301 local function openidc_base64_url_encode(input)
302   local output = b64(input, true)
303   return output:gsub('%+', '-'):gsub('/', '_')
304 end
305
306 local function openidc_combine_uri(uri, params)
307   if params == nil or next(params) == nil then
308     return uri
309   end
310   local sep = "?"
311   if string.find(uri, "?", 1, true) then
312     sep = "&"
313   end
314   return uri .. sep .. ngx.encode_args(params)
315 end
316
317 local function decorate_request(http_request_decorator, req)
318   return http_request_decorator and http_request_decorator(req) or req
319 end
320
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())
325 end
326
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")
331   local err
332
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))
338
339   -- assemble the parameters to the authentication request
340   local params = {
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),
345     state = state,
346   }
347
348   if nonce then
349     params.nonce = nonce
350   end
351
352   if prompt then
353     params.prompt = prompt
354   end
355
356   if opts.display then
357     params.display = opts.display
358   end
359
360   if code_verifier then
361     params.code_challenge_method = 'S256'
362     params.code_challenge = openidc_s256(code_verifier)
363   end
364
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
368   end
369
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()
376
377   if opts.lifecycle and opts.lifecycle.on_created then
378     err = opts.lifecycle.on_created(session)
379     if err then
380       log(WARN, "failed in `on_created` handler: " .. err)
381       return err
382     end
383   end
384
385   session:save()
386
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))
390 end
391
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
395
396   local err
397   local res
398
399   -- check the response from the OP
400   if response.status ~= 200 then
401     err = "response indicates failure, status=" .. response.status .. ", body=" .. response.body
402   else
403     if ignore_body_on_success then
404       return nil, nil
405     end
406
407     -- decode the response and extract the JSON object
408     res = cjson_s.decode(response.body)
409
410     if not res then
411       err = "JSON decoding failed"
412     end
413   end
414
415   return res, err
416 end
417
418 local function openidc_configure_timeouts(httpc, timeout)
419   if timeout then
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)
422     else
423       local r, e = httpc:set_timeout(timeout)
424     end
425   end
426 end
427
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)
433   else
434     log(DEBUG, "openidc_configure_proxy : don't use http proxy")
435   end
436 end
437
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
441
442   local ep_name = endpoint_name or 'token'
443   if not endpoint then
444     return nil, 'no endpoint URI for ' .. ep_name
445   end
446
447   local headers = {
448     ["Content-Type"] = "application/x-www-form-urlencoded"
449   }
450
451   if auth then
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))
455       else
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) .. ":")
459       end
460       log(DEBUG, "client_secret_basic: authorization header '" .. headers.Authorization .. "'")
461
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
466       end
467       log(DEBUG, "client_secret_post: client_id and client_secret being sent in POST body")
468
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
471       if not key then
472         return nil, "Can't use " .. auth .. " without a key."
473       end
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()
477       local assertion = {
478         header = {
479           typ = "JWT",
480           alg = auth == "private_key_jwt" and "RS256" or "HS256",
481         },
482         payload = {
483           iss = opts.client_id,
484           sub = opts.client_id,
485           aud = endpoint,
486           jti = ngx.var.request_id,
487           exp = now + (opts.client_jwt_assertion_expires_in and opts.client_jwt_assertion_expires_in or 60),
488           iat = now
489         }
490       }
491       if auth == "private_key_jwt" then
492         assertion.header.kid = opts.client_rsa_private_key_id
493       end
494
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")
498     end
499   end
500
501   local pass_cookies = opts.pass_cookies
502   if pass_cookies then
503     if ngx.req.get_headers()["Cookie"] then
504       local t = {}
505       for cookie_name in string.gmatch(pass_cookies, "%S+") do
506         local cookie_value = ngx.var["cookie_" .. cookie_name]
507         if cookie_value then
508           table.insert(t, cookie_name .. "=" .. cookie_value)
509         end
510       end
511       headers.Cookie = table.concat(t, "; ")
512     end
513   end
514
515   log(DEBUG, "request body for " .. ep_name .. " endpoint call: ", ngx.encode_args(body))
516
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, {
521     method = "POST",
522     body = ngx.encode_args(body),
523     headers = headers,
524     ssl_verify = (opts.ssl_verify ~= "no"),
525     keepalive = (opts.keepalive ~= "no")
526   }))
527   if not res then
528     err = "accessing " .. ep_name .. " endpoint (" .. endpoint .. ") failed: " .. err
529     log(ERROR, err)
530     return nil, err
531   end
532
533   log(DEBUG, ep_name .. " endpoint response: ", res.body)
534
535   return openidc_parse_json_response(res, ignore_body_on_success)
536 end
537
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)
541 end
542
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
547     return {
548       raw_header = enc_hdr,
549       raw_payload = enc_payload,
550       header = header,
551       payload = payload,
552       signature = ''
553     }
554   end
555   return nil
556 end
557
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)
561
562   local json, err
563   local v = openidc_cache_get("discovery", url)
564   if not v then
565
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")
574     }))
575     if not res then
576       err = "accessing discovery url (" .. url .. ") failed: " .. error
577       log(ERROR, err)
578     else
579       log(DEBUG, "response data: " .. res.body)
580       json, err = openidc_parse_json_response(res)
581       if json then
582         openidc_cache_set("discovery", url, cjson.encode(json), exptime or 24 * 60 * 60)
583       else
584         err = "could not decode JSON from Discovery data" .. (err and (": " .. err) or '')
585         log(ERROR, err)
586       end
587     end
588
589   else
590     json = cjson.decode(v)
591   end
592
593   return json, err
594 end
595
596 -- turn a discovery url set in the opts dictionary into the discovered information
597 local function openidc_ensure_discovered_data(opts)
598   local err
599   if type(opts.discovery) == "string" then
600     local discovery
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)
603     if not err then
604       opts.discovery = discovery
605     end
606   end
607   return err
608 end
609
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)
613   if err then
614     return nil, err
615   end
616   if not (opts and opts.discovery and opts.discovery.userinfo_endpoint) then
617     log(DEBUG, "no userinfo endpoint supplied")
618     return nil, nil
619   end
620
621   local headers = {
622     ["Authorization"] = "Bearer " .. access_token,
623   }
624
625   log(DEBUG, "authorization header '" .. headers.Authorization .. "'")
626
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, {
632     headers = headers,
633     ssl_verify = (opts.ssl_verify ~= "no"),
634     keepalive = (opts.keepalive ~= "no")
635   }))
636   if not res then
637     err = "accessing (" .. opts.discovery.userinfo_endpoint .. ") failed: " .. err
638     return nil, err
639   end
640
641   log(DEBUG, "userinfo response: ", res.body)
642
643   -- parse the response from the user info endpoint
644   return openidc_parse_json_response(res)
645 end
646
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))
650 end
651
652 -- get the token endpoint authentication method
653 local function openidc_get_token_auth_method(opts)
654
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
658   end
659
660   local result
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
669           break
670         end
671       end
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")
674         return nil
675       end
676     else
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
680           result = value
681           log(DEBUG, "no configuration setting for option so select the first supported method specified by the OP: " .. result)
682           break
683         end
684       end
685     end
686   else
687     result = opts.token_endpoint_auth_method
688   end
689
690   -- set a sane default if auto-configuration failed
691   if result == nil then
692     result = "client_secret_basic"
693   end
694
695   log(DEBUG, "token_endpoint_auth_method result set to " .. result)
696
697   return result
698 end
699
700 -- ensure that discovery and token auth configuration is available in opts
701 local function ensure_config(opts)
702   local err
703   err = openidc_ensure_discovered_data(opts)
704   if err then
705     return err
706   end
707
708   -- set the authentication method for the token endpoint
709   opts.token_endpoint_auth_method = openidc_get_token_auth_method(opts)
710 end
711
712 -- query for discovery endpoint data
713 function openidc.get_discovery_doc(opts)
714   local err = openidc_ensure_discovered_data(opts)
715   if err then
716     log(ERROR, "error getting endpoints definition using discovery endpoint")
717   end
718
719   return opts.discovery, err
720 end
721
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"))
724
725   local json, err, v
726
727   if force == 0 then
728     v = openidc_cache_get("jwks", url)
729   end
730
731   if not v then
732
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")
741     }))
742     if not res then
743       err = "accessing jwks url (" .. url .. ") failed: " .. error
744       log(ERROR, err)
745     else
746       log(DEBUG, "response data: " .. res.body)
747       json, err = openidc_parse_json_response(res)
748       if json then
749         openidc_cache_set("jwks", url, cjson.encode(json), exptime or 24 * 60 * 60)
750       end
751     end
752
753   else
754     json = cjson.decode(v)
755   end
756
757   return json, err
758 end
759
760 local function split_by_chunk(text, chunkSize)
761   local s = {}
762   for i = 1, #text, chunkSize do
763     s[#s + 1] = text:sub(i, i + chunkSize - 1)
764   end
765   return s
766 end
767
768 local function get_jwk(keys, kid)
769
770   local rsa_keys = {}
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)
774     end
775   end
776
777   if kid == nil then
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
781     else
782       return nil, "JWT doesn't specify kid but the keystore contains multiple RSA keys"
783     end
784   end
785   for _, value in pairs(rsa_keys) do
786     if value.kid == kid then
787       return value, nil
788     end
789   end
790
791   return nil, "RSA key with id " .. kid .. " not found"
792 end
793
794 local wrap = ('.'):rep(64)
795
796 local envelope = "-----BEGIN %s-----\n%s\n-----END %s-----\n"
797
798 local function der2pem(data, typ)
799   typ = typ:upper() or "CERTIFICATE"
800   data = b64(data)
801   return string.format(envelope, typ, data:gsub(wrap, '%0\n', (#data - 1) / 64), typ)
802 end
803
804
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)
812   end
813   error("Can't encode lengths over 65535")
814 end
815
816
817 local function encode_sequence(array, of)
818   local encoded_array = array
819   if of then
820     encoded_array = {}
821     for i = 1, #array do
822       encoded_array[i] = of(array[i])
823     end
824   end
825   encoded_array = table.concat(encoded_array)
826
827   return string.char(0x30) .. encode_length(#encoded_array) .. encoded_array
828 end
829
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
836   end
837   return "\2" .. encode_length(#bytes) .. bytes
838 end
839
840 local function encode_sequence_of_integer(array)
841   return encode_sequence(array, encode_binary_integer)
842 end
843
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
847 end
848
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)
856   return pem
857 end
858
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")
861
862   local der_key = {
863     openidc_base64_url_decode(n), openidc_base64_url_decode(e)
864   }
865   local encoded_key = encode_sequence_of_integer(der_key)
866   local pem = der2pem(encode_sequence({
867     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
870     }),
871     encode_bit_string(encoded_key)
872   }), "PUBLIC KEY")
873   log(DEBUG, "Generated pem key from n and e: ", pem)
874   return pem
875 end
876
877 local function openidc_pem_from_jwk(opts, kid)
878   local err = openidc_ensure_discovered_data(opts)
879   if err then
880     return nil, err
881   end
882
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"
885   end
886
887   local cache_id = opts.discovery.jwks_uri .. '#' .. (kid or '')
888   local v = openidc_cache_get("jwks", cache_id)
889
890   if v then
891     return v
892   end
893
894   local jwk, jwks
895
896   for force = 0, 1 do
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)
899     if err then
900       return nil, err
901     end
902
903     jwk, err = get_jwk(jwks.keys, kid)
904
905     if jwk and not err then
906       break
907     end
908   end
909
910   if err then
911     return nil, err
912   end
913
914   local x5c = jwk.x5c
915   if x5c and #(jwk.x5c) == 0 then
916     log(WARN, "Found invalid JWK with empty x5c array, ignoring x5c claim")
917     x5c = nil
918   end
919
920   local pem
921   if x5c then
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)
925   else
926     return nil, "don't know how to create RSA key/cert for " .. cjson.encode(jwk)
927   end
928
929   openidc_cache_set("jwks", cache_id, pem, opts.jwk_expires_in or 24 * 60 * 60)
930   return pem
931 end
932
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")
938 end
939
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"
944 end
945
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
949     return true
950   end
951   if type(expected_algs) == 'string' then
952     expected_algs = { expected_algs }
953   end
954   for _, alg in ipairs(expected_algs) do
955     if alg == jwt_header.alg then
956       return true
957     end
958   end
959   return false
960 end
961
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)
969     if jwt then
970       if opts.accept_none_alg then
971         log(DEBUG, "accept JWT with alg \"none\" and no signature")
972         return jwt
973       else
974         return jwt, "token uses \"none\" alg but accept_none_alg is not enabled"
975       end
976     end -- otherwise the JWT is invalid and load_jwt produces an error
977   end
978
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
984     end
985     return nil, reason
986   end
987
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 .. "\""
991   end
992
993   local secret
994   if is_algorithm_supported(jwt_obj.header) then
995     if uses_asymmetric_algorithm(jwt_obj.header) then
996       if opts.secret then
997         log(WARN, "using deprecated option `opts.secret` for asymmetric key; switch to `opts.public_key` instead")
998       end
999       secret = asymmetric_secret or opts.secret
1000       if not secret and opts.discovery then
1001         log(DEBUG, "using discovery to find key")
1002         local err
1003         secret, err = openidc_pem_from_jwk(opts, jwt_obj.header.kid)
1004
1005         if secret == nil then
1006           log(ERROR, err)
1007           return nil, err
1008         end
1009       end
1010     else
1011       if opts.secret then
1012         log(WARN, "using deprecated option `opts.secret` for symmetric key; switch to `opts.symmetric_key` instead")
1013       end
1014       secret = symmetric_secret or opts.secret
1015     end
1016   end
1017
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
1022     -- value
1023     local jwt_validators = require("resty.jwt-validators")
1024     jwt_validators.set_system_leeway(opts.iat_slack and opts.iat_slack or 120)
1025   end
1026
1027   jwt_obj = r_jwt:verify_jwt_obj(secret, jwt_obj, ...)
1028   if jwt_obj then
1029     log(DEBUG, "jwt: ", cjson.encode(jwt_obj), " ,valid: ", jwt_obj.valid, ", verified: ", jwt_obj.verified)
1030   end
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
1035     end
1036     return jwt_obj, reason
1037   end
1038   return jwt_obj
1039 end
1040
1041 --
1042 -- Load and validate id token from the id_token properties of the token endpoint response
1043 -- Parameters :
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
1049 --
1050 local function openidc_load_and_validate_jwt_id_token(opts, jwt_id_token, session)
1051
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)
1054   if err then
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")
1060       else
1061         err = "token is signed using algorithm \"" .. alg .. "\" which is not supported by lua-resty-jwt"
1062         log(ERROR, err)
1063         return nil, err
1064       end
1065     else
1066       log(ERROR, "id_token '" .. alg .. "' signature verification failed")
1067       return nil, err
1068     end
1069   end
1070   local id_token = jwt_obj.payload
1071
1072   log(DEBUG, "id_token header: ", cjson.encode(jwt_obj.header))
1073   log(DEBUG, "id_token payload: ", cjson.encode(jwt_obj.payload))
1074
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"
1078     log(ERROR, err)
1079     return nil, err
1080   end
1081
1082   return id_token
1083 end
1084
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
1089
1090   if not args.code or not args.state then
1091     err = "unhandled request to the redirect_uri: " .. ngx.var.request_uri
1092     log(ERROR, err)
1093     return nil, err, session.data.original_url, session
1094   end
1095
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"
1100     log(ERROR, log_err)
1101     return nil, client_err, session.data.original_url, session
1102   end
1103
1104   err = ensure_config(opts)
1105   if err then
1106     return nil, err, session.data.original_url, session
1107   end
1108
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"
1113     log(ERROR, log_err)
1114     return nil, client_err, session.data.original_url, session
1115   end
1116
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"
1121     log(ERROR, log_err)
1122     return nil, client_err, session.data.original_url, session
1123   end
1124
1125   -- assemble the parameters to the token endpoint
1126   local body = {
1127     grant_type = "authorization_code",
1128     code = args.code,
1129     redirect_uri = openidc_get_redirect_uri(opts, session),
1130     state = session.data.state,
1131     code_verifier = session.data.code_verifier
1132   }
1133
1134   log(DEBUG, "Authentication with OP done -> Calling OP Token Endpoint to obtain tokens")
1135
1136   local current_time = ngx.time()
1137   -- make the call to the token endpoint
1138   local json
1139   json, err = openidc.call_token_endpoint(opts, opts.discovery.token_endpoint, body, opts.token_endpoint_auth_method)
1140   if err then
1141     return nil, err, session.data.original_url, session
1142   end
1143
1144   local id_token, err = openidc_load_and_validate_jwt_id_token(opts, json.id_token, session);
1145   if err then
1146     return nil, err, session.data.original_url, session
1147   end
1148
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
1157   end
1158
1159   if store_in_session(opts, 'user') then
1160     -- call the user info endpoint
1161     -- TODO: should this error be checked?
1162     local user
1163     user, err = openidc.call_userinfo_endpoint(opts, json.access_token)
1164
1165     if err then
1166       log(ERROR, "error calling userinfo endpoint: " .. err)
1167     elseif user then
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") .. "\")"
1170         log(ERROR, err)
1171       else
1172         session.data.user = user
1173       end
1174     end
1175   end
1176
1177   if store_in_session(opts, 'enc_id_token') then
1178     session.data.enc_id_token = json.id_token
1179   end
1180
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
1187     end
1188   end
1189
1190   if opts.lifecycle and opts.lifecycle.on_authenticated then
1191     err = opts.lifecycle.on_authenticated(session, id_token, json)
1192     if err then
1193       log(WARN, "failed in `on_authenticated` handler: " .. err)
1194       return nil, err, session.data.original_url, session
1195     end
1196   end
1197
1198   -- save the session with the obtained id_token
1199   session:save()
1200
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
1205 end
1206
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 .. ".")
1211     return nil
1212   end
1213
1214   local token_type_hint = token_type_hint or nil
1215   local body = {
1216     token = token
1217   }
1218   if token_type_hint then
1219     body['token_type_hint'] = token_type_hint
1220   end
1221   local token_type_log = token_type_hint or 'token'
1222
1223   -- ensure revocation endpoint auth method is properly discovered
1224   local err = ensure_config(opts)
1225   if err then
1226     log(ERROR, "revocation of " .. token_type_log .. " unsuccessful: " .. err)
1227     return false
1228   end
1229
1230   -- call the revocation endpoint
1231   local _
1232   _, err = openidc.call_token_endpoint(opts, opts.discovery.revocation_endpoint, body, opts.token_endpoint_auth_method, "revocation", true)
1233   if err then
1234     log(ERROR, "revocation of " .. token_type_log .. " unsuccessful: " .. err)
1235     return false
1236   else
1237     log(DEBUG, "revocation of " .. token_type_log .. " successful")
1238     return true
1239   end
1240 end
1241
1242 function openidc.revoke_token(opts, token_type_hint, token)
1243   local err = openidc_ensure_discovered_data(opts)
1244   if err then
1245     log(ERROR, "revocation of " .. (token_type_hint or "token (no type specified)") .. " unsuccessful: " .. err)
1246     return false
1247   end
1248
1249   return openidc_revoke_token(opts, token_type_hint, token)
1250 end
1251
1252 function openidc.revoke_tokens(opts, session)
1253   local err = openidc_ensure_discovered_data(opts)
1254   if err then
1255     log(ERROR, "revocation of tokens unsuccessful: " .. err)
1256     return false
1257   end
1258
1259   local access_token = session.data.access_token
1260   local refresh_token = session.data.refresh_token
1261
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)
1265   end
1266   if access_token then
1267     refresh_token_revoke = openidc_revoke_token(opts, "access_token", access_token)
1268   end
1269   return access_token_revoke and refresh_token_revoke
1270 end
1271
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" ..
1276     "\174\066\096\130"
1277
1278 -- handle logout
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
1283   local err
1284
1285   if opts.lifecycle and opts.lifecycle.on_logout then
1286     err = opts.lifecycle.on_logout(session)
1287     if err then
1288       log(WARN, "failed in `on_logout` handler: " .. err)
1289       return err
1290     end
1291   end
1292
1293   session:destroy()
1294
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)
1300     end
1301     if access_token then
1302       openidc_revoke_token(opts, "access_token", access_token)
1303     end
1304   end
1305
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)
1316     ngx.exit(ngx.OK)
1317     return
1318   elseif opts.redirect_after_logout_uri or opts.discovery.end_session_endpoint then
1319     local uri
1320     if opts.redirect_after_logout_uri then
1321       uri = opts.redirect_after_logout_uri
1322     else
1323       uri = opts.discovery.end_session_endpoint
1324     end
1325     local params = {}
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
1328     end
1329     if opts.post_logout_redirect_uri then
1330       params["post_logout_redirect_uri"] = opts.post_logout_redirect_uri
1331     end
1332     return ngx.redirect(openidc_combine_uri(uri, params))
1333   elseif opts.discovery.ping_end_session_endpoint then
1334     local params = {}
1335     if opts.post_logout_redirect_uri then
1336       params["TargetResource"] = opts.post_logout_redirect_uri
1337     end
1338     return ngx.redirect(openidc_combine_uri(opts.discovery.ping_end_session_endpoint, params))
1339   end
1340
1341   ngx.header.content_type = "text/html"
1342   ngx.say("<html><body>Logged Out</body></html>")
1343   ngx.exit(ngx.OK)
1344 end
1345
1346 -- returns a valid access_token (eventually refreshing the token)
1347 local function openidc_access_token(opts, session, try_to_renew)
1348
1349   local err
1350
1351   if session.data.access_token == nil then
1352     return nil, err
1353   end
1354   local current_time = ngx.time()
1355   if current_time < session.data.access_token_expiration then
1356     return session.data.access_token, err
1357   end
1358   if not try_to_renew then
1359     return nil, "token expired"
1360   end
1361   if session.data.refresh_token == nil then
1362     return nil, "token expired and no refresh token available"
1363   end
1364
1365   log(DEBUG, "refreshing expired access_token: ", session.data.access_token, " with: ", session.data.refresh_token)
1366
1367   -- retrieve token endpoint URL from discovery endpoint if necessary
1368   err = ensure_config(opts)
1369   if err then
1370     return nil, err
1371   end
1372
1373   -- assemble the parameters to the token endpoint
1374   local body = {
1375     grant_type = "refresh_token",
1376     refresh_token = session.data.refresh_token,
1377     scope = opts.scope and opts.scope or "openid email profile"
1378   }
1379
1380   local json
1381   json, err = openidc.call_token_endpoint(opts, opts.discovery.token_endpoint, body, opts.token_endpoint_auth_method)
1382   if err then
1383     return nil, err
1384   end
1385   local id_token
1386   if json.id_token then
1387     id_token, err = openidc_load_and_validate_jwt_id_token(opts, json.id_token, session)
1388     if err then
1389       log(ERROR, "invalid id token, discarding tokens returned while refreshing")
1390       return nil, err
1391     end
1392   end
1393   log(DEBUG, "access_token refreshed: ", json.access_token, " updated refresh_token: ", json.refresh_token)
1394
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
1399   end
1400
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
1406     end
1407     if store_in_session(opts, 'id_token') then
1408       session.data.id_token = id_token
1409     end
1410   end
1411
1412   -- save the session with the new access_token and optionally the new refresh_token and id_token using a new sessionid
1413   local regenerated
1414   regenerated, err = session:regenerate()
1415   if err then
1416     log(ERROR, "failed to regenerate session: " .. err)
1417     return nil, err
1418   end
1419   if opts.lifecycle and opts.lifecycle.on_regenerated then
1420     err = opts.lifecycle.on_regenerated(session)
1421     if err then
1422       log(WARN, "failed in `on_regenerated` handler: " .. err)
1423       return nil, err
1424     end
1425   end
1426
1427   return session.data.access_token, err
1428 end
1429
1430 local function openidc_get_path(uri)
1431   local without_query = uri:match("(.-)%?") or uri
1432   return without_query:match(".-//[^/]+(/.*)") or without_query
1433 end
1434
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
1437 end
1438
1439 local function is_session(o)
1440   return o ~= nil and o.start and type(o.start) == "function"
1441 end
1442
1443 -- main routine for OpenID Connect user authentication
1444 function openidc.authenticate(opts, target_url, unauth_action, session_or_opts)
1445
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")
1448   end
1449
1450   local err
1451
1452   local session
1453   if is_session(session_or_opts) then
1454     session = session_or_opts
1455   else
1456     local session_error
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
1461     end
1462   end
1463
1464   target_url = target_url or ngx.var.request_uri
1465
1466   local access_token
1467
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")
1472
1473     if not session.present then
1474       err = "request to the redirect_uri path but there's no session state found"
1475       log(ERROR, err)
1476       return nil, err, target_url, session
1477     end
1478
1479     return openidc_authorization_response(opts, session)
1480   end
1481
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")
1485
1486     err = ensure_config(opts)
1487     if err then
1488       return nil, err, session.data.original_url, session
1489     end
1490
1491     openidc_logout(opts, session)
1492     return nil, nil, target_url, session
1493   end
1494
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
1499
1500     -- refresh access_token if necessary
1501     access_token, err = openidc_access_token(opts, session, try_to_renew)
1502     if err then
1503       log(ERROR, "lost access token:" .. err)
1504       err = nil
1505     end
1506     if not access_token then
1507       token_expired = true
1508     end
1509   end
1510
1511   log(DEBUG,
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)
1519
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
1530       end
1531       return nil, err, target_url, session
1532     end
1533     if unauth_action == 'deny' then
1534       return nil, 'unauthorized request', target_url, session
1535     end
1536
1537     err = ensure_config(opts)
1538     if err then
1539       return nil, err, session.data.original_url, session
1540     end
1541
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
1545   end
1546
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)
1551       if err then
1552         return nil, err, session.data.original_url, session
1553       end
1554
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
1558     end
1559   end
1560
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))
1564   end
1565
1566   -- return the id_token to the caller Lua script for access control purposes
1567   return
1568   {
1569     id_token = session.data.id_token,
1570     access_token = access_token,
1571     user = session.data.user
1572   },
1573   err,
1574   target_url,
1575   session
1576 end
1577
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)
1580
1581   local session = r_session.start(session_opts)
1582   local token, err = openidc_access_token(opts, session, true)
1583   session:close()
1584   return token, err
1585 end
1586
1587
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)
1590
1591   local err
1592
1593   log(DEBUG, "getting bearer access token from Cookie")
1594
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
1599   end
1600   local divider = accept_token_as:find(':')
1601   local cookie_name = divider and accept_token_as:sub(divider + 1) or "PA.global"
1602
1603   log(DEBUG, "bearer access token from cookie named: " .. cookie_name)
1604
1605   local cookies = ngx.req.get_headers()["Cookie"]
1606   if not cookies then
1607     err = "no Cookie header found"
1608     log(ERROR, err)
1609     return nil, err
1610   end
1611
1612   local cookie_value = ngx.var["cookie_" .. cookie_name]
1613   if not cookie_value then
1614     err = "no Cookie " .. cookie_name .. " found"
1615     log(ERROR, err)
1616   end
1617
1618   return cookie_value, err
1619 end
1620
1621
1622 -- get an OAuth 2.0 bearer access token from the HTTP request
1623 local function openidc_get_bearer_access_token(opts)
1624
1625   local err
1626
1627   local accept_token_as = opts.auth_accept_token_as or "header"
1628
1629   if accept_token_as:find("cookie") == 1 then
1630     return openidc_get_bearer_access_token_from_cookie(opts)
1631   end
1632
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])
1637
1638   if header == nil or header:find(" ") == nil then
1639     err = "no Authorization header found"
1640     log(ERROR, err)
1641     return nil, err
1642   end
1643
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"
1647     log(ERROR, err)
1648     return nil, err
1649   end
1650
1651   local access_token = header:sub(divider + 1)
1652   if access_token == nil then
1653     err = "no Bearer access token value found"
1654     log(ERROR, err)
1655     return nil, err
1656   end
1657
1658   return access_token, err
1659 end
1660
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)
1665     if err then
1666       return nil, "opts.introspection_endpoint not said and " .. err
1667     end
1668     local endpoint = opts.discovery and opts.discovery.introspection_endpoint
1669     if endpoint then
1670       return endpoint
1671     end
1672   end
1673   return introspection_endpoint
1674 end
1675
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') .. ':'
1681 end
1682
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)
1688   end
1689 end
1690
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,
1696                       encoded_json, ttl)
1697   end
1698 end
1699
1700 -- main routine for OAuth 2.0 token introspection
1701 function openidc.introspect(opts)
1702
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
1706     return nil, err
1707   end
1708
1709   -- see if we've previously cached the introspection result for this access token
1710   local json
1711   local v = get_cached_introspection(opts, access_token)
1712
1713   if v then
1714     json = cjson.decode(v)
1715     return json, err
1716   end
1717
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"
1720
1721   local body = {}
1722
1723   body[token_param_name] = access_token
1724
1725   if opts.client_id then
1726     body.client_id = opts.client_id
1727   end
1728   if opts.client_secret then
1729     body.client_secret = opts.client_secret
1730   end
1731
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
1735   end
1736
1737   -- call the introspection endpoint
1738   local introspection_endpoint
1739   introspection_endpoint, err = get_introspection_endpoint(opts)
1740   if err then
1741     return nil, err
1742   end
1743   json, err = openidc.call_token_endpoint(opts, introspection_endpoint, body, opts.introspection_endpoint_auth_method, "introspection")
1744
1745
1746   if not json then
1747     return json, err
1748   end
1749
1750   if not json.active then
1751     err = "invalid token"
1752     return json, err
1753   end
1754
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"
1758
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()
1764     end
1765     if introspection_interval > 0 then
1766       if ttl > introspection_interval then
1767         ttl = introspection_interval
1768       end
1769     end
1770     log(DEBUG, "cache token ttl: " .. ttl)
1771     set_cached_introspection(opts, access_token, cjson.encode(json), ttl)
1772   end
1773
1774   return json, err
1775
1776 end
1777
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 }
1783   end
1784   for _, alg in ipairs(expected_algs) do
1785     signing_alg_values_expected = signing_alg_values_expected .. ',' .. alg
1786   end
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 .. ':'
1791 end
1792
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)
1798   end
1799 end
1800
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,
1806                       encoded_json, ttl)
1807   end
1808 end
1809
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, ...)
1813   local err
1814   local json
1815   local v = get_cached_jwt_verification(opts, access_token)
1816
1817   local slack = opts.iat_slack and opts.iat_slack or 120
1818   if not v then
1819     local jwt_obj
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, ...)
1822     if not err then
1823       json = jwt_obj.payload
1824       local encoded_json = cjson.encode(json)
1825       log(DEBUG, "jwt: ", encoded_json)
1826
1827       set_cached_jwt_verification(opts, access_token, encoded_json,
1828                                   json.exp and json.exp - ngx.time() or 120)
1829     end
1830
1831   else
1832     -- decode from the cache
1833     json = cjson.decode(v)
1834   end
1835
1836   -- check the token expiry
1837   if json then
1838     if json.exp and json.exp + slack < ngx.time() then
1839       log(ERROR, "token expired: json.exp=", json.exp, ", ngx.time()=", ngx.time())
1840       err = "JWT expired"
1841     end
1842   end
1843
1844   return json, err
1845 end
1846
1847 function openidc.bearer_jwt_verify(opts, ...)
1848   local json
1849
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
1853     return nil, err
1854   end
1855
1856   log(DEBUG, "access_token: ", access_token)
1857
1858   json, err = openidc.jwt_verify(access_token, opts, ...)
1859   return json, err, access_token
1860 end
1861
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
1868 end
1869
1870 return openidc