Identity-aware proxy based on NGINX, OpenResty and lua-resty-openidc.
IPAx adds OpenID Connect authentication in front of web applications that don't do it themselves, and it can also run as a ready-made OIDC demo application to test an Identity Provider (IdP). It is shipped as a container image:
docker pull ghcr.io/identicum/ipax:latest- OIDC Authorization Code flow, with a client secret (confidential client) or PKCE (public client).
- Session handling with lua-resty-session: idle timeout, "remember me", SameSite/Secure cookies and token renewal on expiry.
- Custom authorization request parameters:
prompt,acr_valuesand any extra query parameter (for examplekc_idp_hintorui_locales). - A demo info page that shows the ID token, access token, refresh token, userinfo response and request headers.
- Buttons for Keycloak Application Initiated Actions (update password, update email, delete account, add passkey, …).
- Lua helpers to authorize by claim and to pass user data to the upstream app as headers.
/ipax/healthhealth-check endpoint and arobots.txtthat blocks all crawlers.
IPAx is configured with environment variables. Most of them can also be set as an nginx variable (set $oidc_client_id '...';) inside a server or location block. When both are present, a non-empty nginx variable wins. This lets one IPAx instance protect several apps, each with its own OIDC client.
See VARIABLES.md for the full list of variables, their defaults and example values. See FILESYSTEM.md for the container's file layout and what to mount where.
IPAX_MODE selects how IPAx runs:
| Mode | IPAX_MODE |
What it does | Mount your config at |
|---|---|---|---|
| Proxy | proxy |
Protects one or more upstream apps (reverse proxy) | /var/ipax/proxy_conf.d/ |
| Single | single |
Runs one demo app, configured only with env variables | (none) |
| Multi | demoapps |
Runs several demo apps on one host, one per URL path | /var/ipax/demoapps/ |
At startup, entrypoint.sh generates nginx.conf from conf/nginx.conf.template, copies the server config for the selected mode from conf/server/, creates the Lua shared dictionaries each app needs, and starts OpenResty.
Each mode has a ready-to-run example under localhost/. See Running the local examples.
IPAx sits in front of your apps. Each protected app is an nginx server block (a .conf file) mounted in /var/ipax/proxy_conf.d/. The request is authenticated in access_by_lua_block, and then forwarded with proxy_pass.
OIDC settings shared by all apps go in environment variables. Settings specific to one app (for example the redirect URI for its hostname) go as nginx variables in its server block.
server {
include /var/ipax/conf/location_norobots.conf;
server_name demoapp1;
set $oidc_redirect_uri 'http://demoapp1/private/redirect_uri';
location / {
access_by_lua_block {
local ipax = require "ipax"
local oidc_opts = ipax.get_oidc_opts()
local session_opts = ipax.get_session_opts()
ngx.ctx.res = ipax.get_res(oidc_opts, session_opts);
-- don't send the IPAx session cookies to the app (avoids HTTP 413 on large cookies)
ipax.filter_session_cookies(session_opts)
local user = ipax.get_user(ngx.ctx.res)
}
proxy_pass http://app1:80/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}Inside access_by_lua_block you can use these helpers from lua/ipax.lua:
| Function | Use |
|---|---|
get_oidc_opts() / get_session_opts() |
Build the lua-resty-openidc / lua-resty-session options |
get_res(oidc_opts, session_opts) |
Authenticate the user (redirects to the IdP if there's no session) |
filter_session_cookies(session_opts) |
Remove the IPAx session cookies from the request sent to the app |
get_user(res) |
Userinfo claims, as a Lua table |
get_id_token(res) / get_access_token(res) |
Tokens, for example to send a Bearer token to an API |
check_multivalued_user_claim(values, required) |
Returns HTTP 403 if required is not in the claim values |
get_ldap_object_names(dns, separator) |
Turns a list of LDAP DNs (e.g. member_of) into a list of names |
For example, to pass user data to the app as headers:
local user = ipax.get_user(ngx.ctx.res)
ngx.req.set_header("IPAx-USERNAME", user.preferred_username)
ngx.req.set_header("IPAx-EMAIL", user.email)conf/samples/ has more examples: authorization by group, header injection, an SPA calling an API with the access token, and an HTTP to HTTPS redirect.
For HTTPS, mount your certificate and key as /var/ipax/certs/public.pem and /var/ipax/certs/private.pem, and add include /var/ipax/conf/ssl/default.settings; to your server block.
IPAx runs as a single OIDC demo application, with no upstream. It needs no config files: set the environment variables only.
services:
ipax:
image: ghcr.io/identicum/ipax:latest
ports:
- 80:80
environment:
IPAX_MODE: single
IPAX_DISPLAY_NAME: My demo app
IPAX_BASE_URL: http://demoapp1
OIDC_DISCOVERY: http://idp:8080/realms/demorealm/.well-known/openid-configuration
OIDC_CLIENT_ID: ipax_client_id
OIDC_CLIENT_SECRET: ipax_client_secret
OIDC_SCOPE: openid profile email
KC_UPDATE_PASSWORD_ACTION: UPDATE_PASSWORDRegister ${IPAX_BASE_URL}/private/redirect_uri as a redirect URI of the client in your IdP.
The app has these pages:
/and/landing: public landing page with a Login button./private/info: protected page. It shows the user's tokens, userinfo claims and request headers, the configured Keycloak action buttons, and a Logout button./private/logout: ends the session and logs out at the IdP (RP-initiated logout), then redirects to/logoutSuccess.html.
IPAx runs several demo apps on the same host, each one under its own path (/demoapp1/, /demoapp2/, …) and each with its own OIDC client. Use it to test several clients side by side, for example SSO between apps, or a confidential client next to a PKCE one.
Mount one .conf file per app in /var/ipax/demoapps/. Each file only sets nginx variables:
# /var/ipax/demoapps/demoapp1.conf
set $ipax_app_name 'demoapp1';
set $ipax_display_name 'DemoApp1';
set $ipax_base_url 'http://demoapps/demoapp1';
set $oidc_discovery 'http://idp:8080/realms/demorealm/.well-known/openid-configuration';
set $oidc_client_id 'demoapp1_client_id';
set $oidc_client_secret 'demoapp1_client_secret';
set $kc_update_password_action 'UPDATE_PASSWORD';For each file, the entrypoint creates the same pages as the single demo app, under /<file name>/. The root URL (/) lists all the configured apps, using their $ipax_display_name. Keep these rules:
$ipax_app_namemust be the same as the file name (without.conf). IPAx uses it to find the app's Lua shared dictionaries, and to build the logout path.$ipax_base_urlmust include the app's path (http://demoapps/demoapp1).- Anything you don't set falls back to the environment variable, so shared values (for example
OIDC_DISCOVERY) can go in the container environment.
Each folder under localhost/ has a compose.yaml that starts IPAx in one mode, together with a Keycloak IdP preloaded with a demorealm realm. The examples mount the files of this repo into the container, so you can edit them and restart the container without rebuilding the image.
-
Add these hostnames to
/etc/hosts. The browser and IPAx must both reach Keycloak atidp:8080:127.0.0.1 idp demoapps demoapp1 demoapp2 demoapp3 -
Start the example:
cd localhost/single # or localhost/demoapps, localhost/proxy docker compose up -d
-
Open the app and log in with user
demo, passworddemo:Example URL localhost/singlehttp://demoapp1 localhost/demoappshttp://demoapps (index of the apps), http://demoapps/demoapp1/, http://demoapps/demoapp2/, http://demoapps/demoapp3/ localhost/proxyhttp://demoapp1, http://demoapp2, http://demoapp3 In the
proxyexample,demoapp1is a static nginx page.demoapp2anddemoapp3run wam-test-page, which shows the HTTP headers the app receives.demoapp2filters the IPAx session cookies withfilter_session_cookies().demoapp3doesn't, so it receives theCookieheader unchanged.
Other services in the examples:
- Keycloak admin console: http://localhost:8080 (
admin/admin). - Mailcatcher, to read the emails Keycloak sends (
singleanddemoappsonly): http://localhost:1080.
docker build -t ipax .To use PKCE, remove
OIDC_CLIENT_SECRETand addOIDC_USE_PKCEwith value "true".