Self-hosted service
zmartzone/lua-resty-openidc avatar
zmartzone/lua-resty-openidc

lua-resty-openidc: putting OpenID Connect in front of an NGINX origin

OpenID Connect Relying Party and OAuth 2.0 Resource Server implementation in Lua for NGINX / OpenResty

1,072 stars257 forksLuaApache-2.0

At a glance

What is it?
lua-resty-openidc is a Lua library that lets NGINX or OpenResty terminate OpenID Connect and OAuth 2.0 either as a relying party or as a resource server, so the origin application never has to implement either standard. The trade-off is that token validation, session storage and client authentication all become NGINX configuration that your operations team has to own.
Who is it for?
Adopt lua-resty-openidc when you have several NGINX-fronted services that all need the same identity provider and you would rather patch one proxy layer than four application stacks. Do not adopt it for a single application that already speaks OpenID Connect, or when your team cannot maintain NGINX configuration, TLS trust stores and shared dictionary sizing.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 14 days ago.
What is it written in?
Mainly Lua, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One library covering two distinct roles at the proxy layer

lua-resty-openidc is a Lua library that implements the OpenID Connect Relying Party and OAuth 2.0 Resource Server roles inside NGINX. The two roles share code but solve different problems, and the choice between them determines what you configure. As a relying party, the library authenticates users against an OpenID Connect Provider using OpenID Connect Discovery and the Basic Client Profile, in other words the Authorization Code flow. As a resource server, it validates OAuth 2.0 Bearer Access Tokens against an Authorization Server, and when the access token is a JSON Web Token, verification can happen against a pre-configured secret or key instead. The target audience is an operator with an origin server behind NGINX that needs standards-based protection without writing OpenID Connect code in the origin itself. The README is explicit about this purpose: the library can act as a reverse proxy terminating OAuth and OpenID Connect so that origin services are protected with the relevant standards without implementing them on the server.

Discovery metadata and validated tokens are cached in shared dictionaries

Two caches sit at the core of the design, and both are ordinary NGINX shared dictionaries rather than a bespoke cache layer. The first holds resolved Discovery documents, which is the metadata that tells the library where the provider's token, introspection and logout endpoints live. The second holds the provider's JWKs, the public keys used to verify token signatures. Because the cache is server-wide, every worker process shares the same copy, which matters for the per-request cost of validating a JWT against a remote key set. The sample configuration declares them as two separate dictionaries with a fixed size each. That sizing is the operational detail people miss: a shared dictionary is allocated once and holds a bounded number of entries, so a value of 1m is a hard ceiling on how many distinct discovery documents and key sets can be resident, and once full, new entries are not cached until older ones expire. The Discovery document itself is fetched from a well-known URL, which in the sample points at Google's OpenID configuration endpoint. Because the resolution is cached, the proxy does not hit the provider on every request, but it also means a provider-side change to endpoints is only picked up when the cached entry is replaced.

Session state is a deliberate choice between a cookie and a server-side store

Sessions are not stored by lua-resty-openidc itself. The library relies on lua-resty-session, which gives the operator a configurable choice between a client-side browser cookie and server-side storage. The README names three server-side mechanisms: shared memory, memcache and redis. This choice is a real security boundary rather than a formatting preference. A cookie-based session keeps the session state in the browser and means the proxy signs or encrypts it, while a server-side session keeps the state out of the browser and makes revocation possible, since the server can drop the entry. Shared memory is tied to a single NGINX instance and disappears on restart, memcache is a fast network cache with no durability guarantees, and redis adds a network dependency to the request path but gives a session store that survives restarts. The library also supports caching of validated access tokens server-wide, which means a resource-server-only deployment can avoid touching the session store at all if it validates tokens through the introspection endpoint.

Installing the library with luarocks or by copying openidc.lua

Installation has two documented routes. The first is the standard Lua rocks tooling.

bash
luarocks install lua-resty-openidc

The second route is manual: copy openidc.lua somewhere in your lua_package_path, under a directory named resty. If you run OpenResty, the README points at the default location /usr/local/openresty/lualib/resty. The manual route matters for container images that are rebuilt from a pinned base rather than installed per host. Older versions could also be installed with opm, but the README states that this is no longer supported, so any internal runbook still mentioning opm is out of date. The dependency list is split in two. NGINX with ngx_devel_kit, Lua or LuaJIT, lua-nginx-module, lua-cjson and lua-resty-string all ship with OpenResty. Two extra pure-Lua packages must be installed on top: lua-resty-http, which provides the HTTP client functions, and lua-resty-session for session management. A third, lua-resty-jwt, is typically needed when you run as an OpenID Connect relying party or consume JWT access tokens, and the README is careful to note that it is not required for a resource-server-only deployment that validates access tokens through remote introspection.

Reading the sample configuration: trust store, caches and client authentication modes

The sample nginx.conf in the README is written for Google+ Signin and protects a reverse-proxied path. The first part sets the Lua path, a DNS resolver, the certificate bundle used for outbound TLS verification and the two shared dictionaries.

nginx
http {
  lua_package_path '~/lua/?.lua;;';

  resolver 8.8.8.8;

  lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
  lua_ssl_verify_depth 5;

  # cache for discovery metadata documents
  lua_shared_dict discovery 1m;
  # cache for JWKs
  lua_shared_dict jwks 1m;

A separate lua_shared_dict is reserved for JWKs, the provider's public keys, so key rotation and token validation do not compete for the same space as discovery metadata. The resolver entry matters because the library has to resolve the provider host at runtime rather than at startup, which is the usual NGINX pattern for dynamic upstreams. The trust store and verification depth are what make outbound calls to the provider fail closed instead of accepting any certificate chain. Inside access_by_lua_block the options object carries the redirect URI, the discovery endpoint and the client credentials, with the discovery value set to the provider's well-known configuration URL. The redirect URI must be the full external address of the protected callback, since the library reconstructs it from the scheme and host of the incoming request unless opts.redirect_uri_scheme or an X-Forwarded-Proto header overrides it. That reconstruction is the classic source of broken logins behind a TLS-terminating load balancer, so it deserves a deliberate decision rather than a copied default.

Four ways to authenticate at the token endpoint, only one of them default

Client authentication at the provider's token endpoint is configurable, and the README lists four modes in the token_endpoint_auth_method option: client_secret_basic, client_secret_post, private_key_jwt and client_secret_jwt. With the first, the second and the fourth, authentication uses client_id and client_secret, and the README warns that for providers that are not compliant with RFC 6749 section 2.3.1 the client_id and client_secret must be invariant when URL encoded. The private_key_jwt mode is different in kind: it uses client_id, client_rsa_private_key and an optional client_rsa_private_key_id to compute a signed client assertion JWT, where the key id identifies which public key the provider should use to verify the signature. The signing algorithm defaults to RS256 for private_key_jwt and HS256 for client_secret_jwt, and the sample shows overriding it with client_jwt_assertion_alg = "PS256". The README also states the constraint that matters: when the provider advertises token_endpoint_auth_signing_alg_values_supported in its discovery document, the configured value must be one the provider supports. A separate audience option, client_jwt_assertion_audience, defaults to the URL of the endpoint being called and exists for the case where the token endpoint is reached through an internal URL while the provider expects the external one as the audience. Getting these wrong does not produce a clear error, it produces a rejected token request at the worst possible moment.

Where the reverse proxy approach is the wrong fit

The library has clear limits that come from being an NGINX module rather than an application library. First, it protects paths, not fine-grained business permissions: the origin application still has to decide what an authenticated user may do, so the proxy only answers who the caller is. Second, everything the library does is NGINX configuration, which means an operator who is not comfortable with access_by_lua_block, Lua string quoting and TLS trust configuration owns the security posture of every service behind the proxy. Third, the dependency chain is specific. lua-resty-http, lua-resty-session and, in most deployments, lua-resty-jwt must all be present and compatible, and the README does not document version constraints for them. Fourth, the reverse proxy pattern is not a substitute for implementing the standard in the origin. If only one application needs OpenID Connect, adding a proxy layer, a certificate bundle and a resolver to that deployment costs more than the code it saves. The alternative approach is the one the README names in passing: implement the relevant standards in the origin server itself, which keeps the identity logic with the application that depends on it and removes the proxy hop from the request path.

Frequent releases under Apache-2.0, with the disclaimer file shipped alongside

The project is under active release pressure. The last push was on 2026-10-09 and version 1.9.2 was published the same day, following 1.9.1 on 2026-09-05 and 1.9.0 on 2026-07-01. Three releases in a quarter is a visible maintenance cadence, and the repository carries the artefacts that usually go with it: a ChangeLog, a RELEASE.md, a SECURITY.md, a DISCLAIMER file, a dist.ini for the Lua packaging metadata and a rockspec named lua-resty-openidc-1.9.2-1 that pins the current release. The license is Apache-2.0, which is permissive and, unlike a copyleft license, does not oblige you to publish modifications to the library itself when you embed it in an internal NGINX deployment. That makes the library comfortable to vendor inside a private image. Two things a buyer should still check are named in the repository itself. The DISCLAIMER file indicates the maintainers do not stand behind the library in a contractual sense, and SECURITY.md implies a route for reporting vulnerabilities, so a team running this in front of production traffic should read both. The repository also ships a tests/ directory, which matters because the library's behaviour depends heavily on the identity provider it is pointed at, and a change in provider behaviour is more likely than a change in Lua.

Editorial conclusion

Adopt lua-resty-openidc when you have several NGINX-fronted services that all need the same identity provider and you would rather patch one proxy layer than four application stacks. Do not adopt it for a single application that already speaks OpenID Connect, or when your team cannot maintain NGINX configuration, TLS trust stores and shared dictionary sizing. Before you commit, confirm that your provider's discovery document exposes the endpoints you need, that lua-resty-session points at shared memory, memcache or redis rather than a browser cookie, and that release 1.9.2, published on 2026-10-09, covers the token endpoint authentication mode your provider actually requires.

Frequently asked questions

Do I need lua-resty-jwt to run lua-resty-openidc as a resource server?

Not always. The README states that lua-resty-jwt is not required when running as an OAuth 2.0 Resource Server only, using remote introspection for access token validation. It is typically needed when you act as an OpenID Connect Relying Party or consume JWT access tokens.

Where should lua-resty-openidc session state be stored?

The library uses lua-resty-session, and the README describes a configurable choice between a client-side browser cookie and server-side storage using shared memory, memcache or redis. Shared memory disappears with the NGINX instance, while redis survives restarts and supports server-side revocation.

How do I install lua-resty-openidc without luarocks?

Copy openidc.lua somewhere in your lua_package_path, under a directory named resty. With OpenResty the README gives the default location as /usr/local/openresty/lualib/resty. Older opm-based installation is no longer supported.

Does lua-resty-openidc contact the identity provider on every request?

No. The README states that the library supports server-wide caching of resolved Discovery documents and validated Access Tokens, using NGINX shared dictionaries declared in the configuration with lua_shared_dict.

Which client authentication methods does lua-resty-openidc support at the token endpoint?

The token_endpoint_auth_method option accepts client_secret_basic, client_secret_post, private_key_jwt and client_secret_jwt. The private_key_jwt mode signs a client assertion with client_rsa_private_key, and the configured signing algorithm must be one the provider advertises in its discovery document.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. README
  4. Releases
  5. zmartzone/lua-resty-openidc on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/zmartzone-lua-resty-openidc.svg)](https://hysenlabs.com/projects/zmartzone-lua-resty-openidc)

Was this useful?