zmartzone/lua-resty-openidc

Name: lua-resty-openidc

Owner: ZmartZone IAM

Description: Lua implementation to make NGINX operate as an OpenID Connect RP or OAuth 2.0 RS using the Lua extension scripting features (http://wiki.nginx.org/HttpLuaModule) which are for instance part of OpenResty (http://openresty.org/)

Created: 2015-07-14 16:37:02.0

Updated: 2018-05-24 20:08:34.0

Pushed: 2018-05-24 16:09:15.0

Homepage:

Size: 381

Language: Lua

GitHub Committers

UserMost Recent Commit# Commits

Other Committers

UserEmailMost Recent Commit# Commits

README

Build Status OpenID Certification

lua-resty-openidc

lua-resty-openidc is a library for NGINX implementing the OpenID Connect Relying Party (RP) and/or the OAuth 2.0 Resource Server (RS) functionality.

When used as an OpenID Connect Relying Party it authenticates users against an OpenID Connect Provider using OpenID Connect Discovery and the Basic Client Profile (i.e. the Authorization Code flow). When used as an OAuth 2.0 Resource Server it can validate OAuth 2.0 Bearer Access Tokens against an Authorization Server or, in case a JSON Web Token is used for an Access Token, verification can happen against a pre-configured secret/key .

It maintains sessions for authenticated users by leveraging lua-resty-session thus offering a configurable choice between storing the session state in a client-side browser cookie or use in of the server-side storage mechanisms shared-memory|memcache|redis.

It supports server-wide caching of resolved Discovery documents and validated Access Tokens.

It can be used as a reverse proxy terminating OAuth/OpenID Connect in front of an origin server so that the origin server/services can be protected with the relevant standards without implementing those on the server itself.

Dependencies

lua-resty-openidc depends on the following packages:

The dependencies above come automatically with OpenResty. You will need to install two extra pure-Lua dependencies that implement session management and HTTP client functions:

Typically - when running as an OpenID Connect RP or an OAuth 2.0 server that consumes JWT access tokens - you'll also need to install the following dependency:

The lua-resty-jwt dependency above is not required when running as an OAuth 2.0 Resource Server (only) using remote introspection for access token validation.

Installation

If you're using opm execute the following:

 opm install zmartzone/lua-resty-openidc

If you're using luarocks execute the following:

 luarocks install lua-resty-openidc

Otherwise copy openidc.lua somewhere in your lua_package_path under a directory named resty. If you are using OpenResty, the default location would be /usr/local/openresty/lualib/resty.

Sample Configuration for Google+ Signin

Sample nginx.conf configuration for authenticating users against Google+ Signin, protecting a reverse-proxied path.

ts {
rker_connections 128;


 {

a_package_path '~/lua/?.lua;;';

solver 8.8.8.8;

a_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
a_ssl_verify_depth 5;

cache for discovery metadata documents
a_shared_dict discovery 1m;
cache for JWKs
a_shared_dict jwks 1m;

NB: if you have "lua_code_cache off;", use:
set $session_secret xxxxxxxxxxxxxxxxxxx;
see: https://github.com/bungle/lua-resty-session#notes-about-turning-lua-code-cache-off

rver {
listen 8080;

location / {

  access_by_lua_block {

      local opts = {
         -- the full redirect URI must be protected by this script and becomes:
         -- ngx.var.scheme.."://"..ngx.var.http_host..opts.redirect_uri_path
         -- unless the scheme is overridden using opts.redirect_uri_scheme or an X-Forwarded-Proto header in the incoming request
         redirect_uri_path = "/redirect_uri",
         discovery = "https://accounts.google.com/.well-known/openid-configuration",
         client_id = "<client_id>",
         client_secret = "<client_secret>"
         --authorization_params = { hd="zmartzone.eu" },
         --scope = "openid email profile",
         -- Refresh the users id_token after 900 seconds without requiring re-authentication
         --refresh_session_interval = 900,
         --iat_slack = 600,
         --redirect_uri_scheme = "https",
         --logout_path = "/logout",
         --redirect_after_logout_uri = "/",
         --redirect_after_logout_with_id_token_hint = true,
         --token_endpoint_auth_method = ["client_secret_basic"|"client_secret_post"],
         --ssl_verify = "no"

         --accept_none_alg = false
         -- if your OpenID Connect Provider doesn't sign its id tokens
         -- (uses the "none" signature algorithm) then set this to true.

         --accept_unsupported_alg = true
         -- if you want to reject tokens signed using an algorithm
         -- not supported by lua-resty-jwt set this to false. If
         -- you leave it unset or set it to true, the token signature will not be
         -- verified when an unsupported algorithm is used.

         --renew_access_token_on_expiry = true
         -- whether this plugin shall try to silently renew the access token once it is expired if a refresh token is available.
         -- if it fails to renew the token, the user will be redirected to the authorization endpoint.
         --access_token_expires_in = 3600
         -- Default lifetime in seconds of the access_token if no expires_in attribute is present in the token endpoint response. 

         --access_token_expires_leeway = 0
         --  Expiration leeway for access_token renewal. If this is set, renewal will happen access_token_expires_leeway seconds before the token expiration. This avoids errors in case the access_token just expires when arriving to the OAuth Resource Server.

         --force_reauthorize = false
         -- When force_reauthorize is set to true the authorization flow will be executed even if a token has been cached already
         --session_contents = {id_token=true}
         -- Whitelist of session content to enable. This can be used to reduce the session size.
         -- When not set everything will be included in the session.
         -- Available are: 
         -- id_token, enc_id_token, user, access_token (includes refresh token)

         -- You can specify timeouts for connect/send/read as a single number (setting all timeouts) or as a table. Values are in milliseconds
         -- timeout = 1000
         -- timeout = { connect = 500, send = 1000, read = 1000 }

         -- Optional : use outgoing proxy to the OpenID Connect provider endpoints with the proxy_opts table : 
         -- this requires lua-resty-http >= 0.12
         -- proxy_opts = {
         --    http_proxy  = "http://<proxy_host>:<proxy_port>/",
         --    https_proxy = "http://<proxy_host>:<proxy_port>/"
         -- }

      }

      -- call authenticate for OpenID Connect user authentication
      local res, err = require("resty.openidc").authenticate(opts)

      if err then
        ngx.status = 500
        ngx.say(err)
        ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
      end

      -- at this point res is a Lua table with 3 keys:
      --   id_token    : a Lua table with the claims from the id_token (required)
      --   access_token: the access token (optional)
      --   user        : a Lua table with the claims returned from the user info endpoint (optional)

      --if res.id_token.hd ~= "zmartzone.eu" then
      --  ngx.exit(ngx.HTTP_FORBIDDEN)
      --end

      --if res.user.email ~= "hans.zandbelt@zmartzone.eu" then
      --  ngx.exit(ngx.HTTP_FORBIDDEN)
      --end

      -- set headers with user info: this will overwrite any existing headers
      -- but also scrub(!) them in case no value is provided in the token
      ngx.req.set_header("X-USER", res.id_token.sub)
  }

  proxy_pass http://localhost:80;
}


Check authentication only
heck session, but do not redirect to auth if not already logged in
l res, err = require("resty.openidc").authenticate(opts, nil, "pass")
Sample Configuration for OAuth 2.0 JWT Token Validation

Sample nginx.conf configuration for verifying Bearer JWT Access Tokens against a pre-configured secret/key. Once successfully verified, the NGINX server may function as a reverse proxy to an internal origin server.

ts {
rker_connections 128;


 {

a_package_path '~/lua/?.lua;;';

solver 8.8.8.8;

cache for JWT verification results
a_shared_dict introspection 10m;

rver {
listen 8080;

location /api {

  access_by_lua '

      local opts = {

        -- 1. example of a shared secret for HS??? signature verification
        --secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",

        -- 2. another example of a public cert for RS??? signature verification
        secret = [[-----BEGIN CERTIFICATE-----
0DCCAbigAwIBAgIGAVSbMZs1MA0GCSqGSIb3DQEBCwUAMCkxCzAJBgNVBAYTAlVTMQwwCgYD
EwNibGExDDAKBgNVBAMTA2JsYTAeFw0xNjA1MTAxNTAzMjBaFw0yNjA1MDgxNTAzMjBaMCkx
BgNVBAYTAlVTMQwwCgYDVQQKEwNibGExDDAKBgNVBAMTA2JsYTCCASIwDQYJKoZIhvcNAQEB
ggEPADCCAQoCggEBAIcLtHjX2GFxYv1033dvfohyCU6nsuR1qoDXfHTG3Mf/Yj4BfLHtMjJr
gHItH3B6qZPnfErfsN0LP4uZ10/74CrWVqT5dy6ecXMqYtz/KNJ8rG0vY8vltc417AU4fie8
v/Z6wHWUCf3NHRV8GfFgfuvywgUpHo8ujpUPFr+zrPr8butrzJPq1h3+r0f5P45tfWOdpjCT
K6urUG0k3WkwdDYapL3wRCAw597nYfgKzzXuh9N0ZL3Uj+eJ6BgCzUZDLXABpMBZfk6hmmzp
4nTf1AaAs/EOwVE0YgZBJiBrueMcteAIxKrKjEHgThU2Zs9gN9cSFicCAwEAATANBgkqhkiG
AQsFAAOCAQEAQLU1A58TrSwrEccCIy0wxiGdCwQbaNMohzirc41zRMCXleJXbtsn1vv85J6A
eH5f/JbDqRRRArGMdLooGbqjWG/lwZT456Q6DXqF2plkBvh37kp/GjthGyR8ODJn5ekZwxuB
ruRhqYOIJjiYZSgK/P0zUw1cjLwUJ9ig/O6ozYmof83974fygA/wK3SgFNEoFlTkTpOvZhVW
CVA/CRBfJNKnz5PWBBxd/3XSEuP/fcWqKGTy7zZso4MTB0NKgWO4duGTgMyZbM4onJPyA0CY
Csj0o5Q+oEhPUAVBIF07m4rd0OvAVPOCQ2NJhQSL1oWASbf+fg==
-END CERTIFICATE-----]],

        -- 3. alternatively one can point to a so-called Discovery document that
        -- contains "jwks_uri" entry; the jwks endpoint must provide either an "x5c" entry
        -- or both the "n" modulus and "e" exponent entries for RSA signature verification
        -- discovery = "https://accounts.google.com/.well-known/openid-configuration",

         -- the signature algorithm that you expect has been used;
         -- can be a single string or a table.
         -- You should set this for security reasons in order to
         -- avoid accepting a token claiming to be signed by HMAC
         -- using a public RSA key.
         --token_signing_alg_values_expected = { "RS256" }

         -- if you want to accept unsigned tokens (using the
         -- "none" signature algorithm) then set this to true.
         --accept_none_alg = false

         -- if you want to reject tokens signed using an algorithm
         -- not supported by lua-resty-jwt set this to false. If
         -- you leave it unset, the token signature will not be
         -- verified at all.
         --accept_unsupported_alg = true

      }

      -- 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.

ts {
rker_connections 128;


 {

a_package_path '~/lua/?.lua;;';

solver 8.8.8.8;

a_ssl_trusted_certificate /opt/local/etc/openssl/cert.pem;
a_ssl_verify_depth 5;

cache for validation results
a_shared_dict introspection 10m;

rver {
listen 8080;

location /api {

  access_by_lua '

      local opts = {
         introspection_endpoint="https://localhost:9031/as/introspect.oauth2",
         client_id="rs_client",
         client_secret="2Federate",
         ssl_verify = "no",

         -- Defaults to "exp" - Controls the TTL of the introspection cache
         -- https://tools.ietf.org/html/rfc7662#section-2.2
         -- introspection_expiry_claim = "exp"
      }

      -- call introspect for OAuth 2.0 Bearer Access Token validation
      local res, err = require("resty.openidc").introspect(opts)

      if err then
        ngx.status = 403
        ngx.say(err)
        ngx.exit(ngx.HTTP_FORBIDDEN)
      end

      -- at this point res is a Lua table that represents the JSON
      -- object returned from the introspection/validation endpoint

      --if res.scope ~= "edit" then
      --  ngx.exit(ngx.HTTP_FORBIDDEN)
      --end

      --if res.client_id ~= "ro_client" then
      --  ngx.exit(ngx.HTTP_FORBIDDEN)
      --end
  ';
}


Sample Configuration for passing bearer OAuth 2.0 access tokens as cookie

Sample nginx.conf configuration for validating Bearer Access Tokens passed as cookie against a ORY/Hydra Authorization Server.

ts {
rker_connections 128;


 {

a_package_path '~/lua/?.lua;;';

solver 8.8.8.8;

a_ssl_trusted_certificate /opt/local/etc/openssl/cert.pem;
a_ssl_verify_depth 5;

cache for validation results
a_shared_dict introspection 10m;

rver {
listen 8080;

location /api {

  access_by_lua '

      local opts = {
         introspection_endpoint="https://localhost:9031/oauth2/introspect",
         client_id="admin",
         client_secret="demo-password",
         ssl_verify = "no",

         -- Defines the interval in seconds after which a cached and introspected access token needs
         -- to be refreshed by introspecting (and validating) it again against the Authorization Server.
         -- When not defined the value is 0, which means it only expires after the `exp` (or alternative,
         -- see introspection_expiry_claim) hint as returned by the Authorization Server
         -- introspection_interval = 60, 

         -- Defines the way in which bearer OAuth 2.0 access tokens can be passed to this Resource Server. 
         -- "cookie" as a cookie header called "PA.global" or using the name specified after ":"
         -- "header" "Authorization: bearer" header
         -- When not defined the default "Authorization: bearer" header is used
         -- auth_accept_token_as = "cookie:PA",

         -- Authentication method for the OAuth 2.0 Authorization Server introspection endpoint,
         -- Used to authenticate the client to the introspection endpoint with a client_id/client_secret
         -- Defaults to "client_secret_post"
         -- introspection_endpoint_auth_method = "client_secret_basic",

         -- Specify the names of cookies separated by whitespace to pickup from the browser and send along on backchannel
         -- calls to the OP and AS endpoints. 
         -- When not defined, no such cookies are sent.
         -- pass_cookies = "JSESSION"

         -- Defaults to "exp" - Controls the TTL of the introspection cache
         -- https://tools.ietf.org/html/rfc7662#section-2.2
         -- introspection_expiry_claim = "exp"
      }

      -- call introspect for OAuth 2.0 Bearer Access Token validation
      local res, err = require("resty.openidc").introspect(opts)

      if err then
        ngx.status = 403
        ngx.say(err)
        ngx.exit(ngx.HTTP_FORBIDDEN)
      end

      -- at this point res is a Lua table that represents the JSON
      -- object returned from the introspection/validation endpoint

      --if res.scope ~= "edit" then
      --  ngx.exit(ngx.HTTP_FORBIDDEN)
      --end

      --if res.client_id ~= "ro_client" then
      --  ngx.exit(ngx.HTTP_FORBIDDEN)
      --end
  ';
}


Running Tests

We've created a dockerized setup for the test in order to simplify the installation of dependencies.

In order to run the tests perform

cker build -f tests/Dockerfile . -t lua-resty-openidc/test
cker run -it --rm lua-resty-openidc/test:latest

if you want to create luacov coverage while testing use

cker run -it --rm -e coverage=t lua-resty-openidc/test:latest

as the second command

Support

See the Wiki pages with Frequently Asked Questions at: https://github.com/zmartzone/lua-resty-openidc/wiki For commercial support and consultancy you can contact: info@zmartzone.eu

Any questions/issues should go to issues tracker or the primary author hans.zandbelt@zmartzone.eu

Disclaimer

This software is open sourced by ZmartZone IAM. For commercial support you can contact ZmartZone IAM as described above.


This work is supported by the National Institutes of Health's National Center for Advancing Translational Sciences, Grant Number U24TR002306. This work is solely the responsibility of the creators and does not necessarily represent the official views of the National Institutes of Health.