From 1fc1960ba03317e7c0a671340cf5edf4b27e18ce Mon Sep 17 00:00:00 2001 From: Steffen Hanikel Date: Tue, 18 Oct 2016 17:54:53 +0200 Subject: [PATCH 1/4] Allow verification of jwt tokens via the discovery endpoint --- lib/resty/openidc.lua | 89 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/lib/resty/openidc.lua b/lib/resty/openidc.lua index 6ebd67b..cdf328c 100644 --- a/lib/resty/openidc.lua +++ b/lib/resty/openidc.lua @@ -383,6 +383,76 @@ local function openidc_discover(url, ssl_verify) return json, err end +local function openidc_jwks(url, ssl_verify) + ngx.log(ngx.DEBUG, "In openidc_jwks - URL is "..url) + + local json, err + local v = openidc_cache_get("jwks", url) + if not v then + + ngx.log(ngx.DEBUG, "JWKS data not in cache. Making call to jwks endpoint") + -- make the call to the jwks endpoint + local httpc = http.new() + local res, error = httpc:request_uri(url, { + ssl_verify = (ssl_verify ~= "no") + }) + if not res then + err = "accessing jwks url ("..url..") failed: "..error + ngx.log(ngx.ERR, err) + else + ngx.log(ngx.DEBUG, "Response data: "..res.body) + json, err = openidc_parse_json_response(res) + if json then + openidc_cache_set("jwks", url, cjson.encode(json), 24 * 60 * 60) + end + end + + else + json = cjson.decode(v) + end + + return json, err +end + +local function split_by_chunk(text, chunkSize) + local s = {} + for i=1, #text, chunkSize do + s[#s+1] = text:sub(i,i+chunkSize - 1) + end + return s +end + +local function get_jwk (keys, kid) + for _, value in pairs(keys) do + if value.kid == kid then + return value + end + end + + return nil +end + +local function pem_from_jwk (opts, kid) + local cache_id = opts.discovery.jwks_uri .. '#' .. kid + local v = openidc_cache_get("jwks", cache_id) + + if v then + return v + end + + local jwks, err = openidc_jwks(opts.discovery.jwks_uri, opts.ssl_verify) + if err then + return nil, err + end + + local x5c = get_jwk(jwks.keys, kid).x5c + -- TODO check x5c length + local chunks = split_by_chunk(ngx.encode_base64(openidc_base64_url_decode(x5c[1])), 64) + local pem = "-----BEGIN CERTIFICATE-----\n" .. table.concat(chunks, "\n") .. "\n-----END CERTIFICATE-----" + openidc_cache_set("jwks", cache_id, pem, 24 * 60 * 60) + return pem +end + local openidc_transparent_pixel = "\137\080\078\071\013\010\026\010\000\000\000\013\073\072\068\082" .. "\000\000\000\001\000\000\000\001\008\004\000\000\000\181\028\012" .. "\002\000\000\000\011\073\068\065\084\120\156\099\250\207\000\000" .. @@ -606,6 +676,25 @@ function openidc.bearer_jwt_verify(opts) -- do the verification first time local jwt = require "resty.jwt" + + -- No secret given try getting it from the jwks endpoint + if not opts.secret and opts.discovery then + ngx.log(ngx.DEBUG, "bearer_jwt_verify using discovery.") + opts.discovery, err = openidc_discover(opts.discovery, opts.ssl_verify) + if err then + return nil, err + end + + -- We decode the token twice, could be saved + local jwt_obj = jwt:load_jwt(access_token, nil) + + opts.secret, err = pem_from_jwk(opts, jwt_obj.header.kid) + + if opts.secret == nil then + return nil, err + end + end + json = jwt:verify(opts.secret, access_token) ngx.log(ngx.DEBUG, "jwt: ", cjson.encode(json)) From b6eebf4d577309277eb4321d19b3aca07a1d8350 Mon Sep 17 00:00:00 2001 From: Steffen Hanikel Date: Tue, 18 Oct 2016 18:05:15 +0200 Subject: [PATCH 2/4] Added jwt validation via discovery to Readme --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/README.md b/README.md index 182aba7..a184a7c 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,66 @@ lAc5Csj0o5Q+oEhPUAVBIF07m4rd0OvAVPOCQ2NJhQSL1oWASbf+fg== } ``` +## Sample Configuration for OAuth 2.0 JWT Token Validation + +Sample `nginx.conf` configuration for verifying Bearer JWT Access Tokens against a OpenID Connect Discovery endpoint. +Once successfully verified, the NGINX server may function as a reverse proxy to an internal origin server. + +``` +events { + worker_connections 128; +} + +http { + + lua_package_path '~/lua/?.lua;;'; + + resolver 8.8.8.8; + + # cache for JWT verification results + lua_shared_dict introspection 10m; + # cache for jwks metadata documents + lua_shared_dict discovery 1m; + + server { + listen 8080; + + location /api { + + access_by_lua ' + + local opts = { + -- The jwks endpoint must provide a x5c entry + -- discovery = "https://accounts.google.com/.well-known/openid-configuration", + } + + -- call bearer_jwt_verify for OAuth 2.0 JWT validation + local res, err = require("resty.openidc").bearer_jwt_verify(opts) + + if err or not res then + ngx.status = 403 + ngx.say(err and err or "no access_token provided") + ngx.exit(ngx.HTTP_FORBIDDEN) + end + + -- at this point res is a Lua table that represents the JSON + -- payload in the JWT token + + --if res.scope ~= "edit" then + -- ngx.exit(ngx.HTTP_FORBIDDEN) + --end + + --if res.client_id ~= "ro_client" then + -- ngx.exit(ngx.HTTP_FORBIDDEN) + --end + '; + + proxy_pass http://localhost:80; + } + } +} +``` + ## Sample Configuration for PingFederate OAuth 2.0 Sample `nginx.conf` configuration for validating Bearer Access Tokens against a PingFederate OAuth 2.0 Authorization Server. From a8dc71ef0b1d3b24cd8ca393b20dab6aca4aa892 Mon Sep 17 00:00:00 2001 From: Steffen Hanikel Date: Wed, 8 Feb 2017 17:56:42 +0100 Subject: [PATCH 3/4] Allow verification of tokens directly from the code --- lib/resty/openidc.lua | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/resty/openidc.lua b/lib/resty/openidc.lua index cdf328c..7c4c364 100644 --- a/lib/resty/openidc.lua +++ b/lib/resty/openidc.lua @@ -657,19 +657,10 @@ function openidc.introspect(opts) end -- main routine for OAuth 2.0 JWT token validation -function openidc.bearer_jwt_verify(opts) - +function openidc.jwt_verify(access_token, opts) local err local json - -- get the access token from the request - local access_token, err = openidc_get_bearer_access_token(opts) - if access_token == nil then - return nil, err - end - - ngx.log(ngx.DEBUG, "access_token: ", access_token) - -- see if we've previously cached the validation result for this access token local v = openidc_cache_get("introspection", access_token) if not v then @@ -723,4 +714,19 @@ function openidc.bearer_jwt_verify(opts) return json, err end +function openidc.bearer_jwt_verify(opts) + local err + local json + + -- get the access token from the request + local access_token, err = openidc_get_bearer_access_token(opts) + if access_token == nil then + return nil, err + end + + ngx.log(ngx.DEBUG, "access_token: ", access_token) + + return openidc.jwt_verify(access_token, opts) +end + return openidc From 012d025817f769bef0473f89a5b720a0853f8912 Mon Sep 17 00:00:00 2001 From: Steffen Hanikel Date: Thu, 9 Feb 2017 12:48:10 +0100 Subject: [PATCH 4/4] Add better error handling for invalid jwt tokens --- lib/resty/openidc.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/resty/openidc.lua b/lib/resty/openidc.lua index 7c4c364..d90eeb4 100644 --- a/lib/resty/openidc.lua +++ b/lib/resty/openidc.lua @@ -679,6 +679,10 @@ function openidc.jwt_verify(access_token, opts) -- We decode the token twice, could be saved local jwt_obj = jwt:load_jwt(access_token, nil) + if not jwt_obj.valid then + return nil, "invalid jwt" + end + opts.secret, err = pem_from_jwk(opts, jwt_obj.header.kid) if opts.secret == nil then