Set up enterprise single sign-on by integrating Keycloak OAuth2 authentication with OpenResty using lua-resty-openidc. Configure secure authentication flows, JWT token validation, and session management for production web applications.
Prerequisites
- Root access to the server
- Domain name with SSL certificate
- Running Keycloak instance
- Basic knowledge of Lua and NGINX
What this solves
This tutorial configures Keycloak OAuth2 integration with OpenResty to provide enterprise single sign-on (SSO) authentication. You'll implement secure authentication flows using lua-resty-openidc, validate JWT tokens, and manage user sessions across multiple applications.
Step-by-step configuration
Update system packages and install dependencies
Start by updating your system and installing required packages for OpenResty and Lua modules.
sudo apt update && sudo apt upgrade -y
sudo apt install -y wget gnupg2 software-properties-common curl
Install OpenResty with Lua modules
Install OpenResty repository and the main package with required Lua modules for OAuth2 integration.
wget -qO - https://openresty.org/package/pubkey.gpg | sudo gpg --dearmor -o /usr/share/keyrings/openresty.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/openresty.gpg] http://openresty.org/package/ubuntu $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/openresty.list
sudo apt update
sudo apt install -y openresty openresty-opm luarocks
Install lua-resty-openidc module
Install the OpenID Connect client library for Lua that handles OAuth2 authentication flows with Keycloak.
sudo /usr/local/openresty/bin/opm get zmartzone/lua-resty-openidc
sudo luarocks install lua-resty-jwt
sudo luarocks install lua-resty-session
sudo luarocks install lua-resty-http
Create OpenResty directory structure
Set up the directory structure for OpenResty configuration files and Lua scripts.
sudo mkdir -p /etc/openresty/conf.d
sudo mkdir -p /etc/openresty/lua
sudo mkdir -p /var/log/openresty
sudo chown -R nobody:nobody /var/log/openresty
Configure Keycloak OAuth2 client
Create the Keycloak client configuration that OpenResty will use for OAuth2 authentication.
In your Keycloak admin console, create a new client with these settings:
- Client ID:
openresty-client - Client Protocol:
openid-connect - Access Type:
confidential - Valid Redirect URIs:
https://example.com/auth - Web Origins:
https://example.com
Save the client secret from the Credentials tab for the next step.
Create OpenResty main configuration
Configure the main OpenResty nginx.conf file with Lua module paths and basic settings.
user nobody;
worker_processes auto;
error_log /var/log/openresty/error.log;
pid /run/openresty.pid;
events {
worker_connections 1024;
use epoll;
}
http {
lua_package_path '/usr/local/openresty/site/lualib/?.lua;/etc/openresty/lua/?.lua;;';
lua_package_cpath '/usr/local/openresty/site/lualib/?.so;;';
lua_shared_dict discovery 1m;
lua_shared_dict jwks 1m;
lua_shared_dict introspection 10m;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
include /etc/openresty/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/openresty/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 4096;
include /etc/openresty/conf.d/*.conf;
}
Create Lua authentication script
Create the main Lua script that handles OAuth2 authentication flows with Keycloak.
local openidc = require("resty.openidc")
local cjson = require("cjson")
-- Keycloak configuration
local opts = {
redirect_uri = "https://example.com/auth",
discovery = "https://keycloak.example.com/realms/master/.well-known/openid_configuration",
client_id = "openresty-client",
client_secret = "your-client-secret-here",
scope = "openid profile email",
-- Session configuration
session_opts = {
secret = "change-this-to-a-random-32-character-string",
cookie_name = "OIDC_SESSION",
cookie_lifetime = 3600,
cookie_secure = true,
cookie_httponly = true,
cookie_samesite = "Lax"
},
-- Token validation
token_signing_alg_values_expected = { "RS256" },
accept_none_alg = false,
-- Security settings
ssl_verify = "yes",
timeout = 10,
-- Logout configuration
logout_path = "/logout",
post_logout_redirect_uri = "https://example.com/"
}
-- Handle authentication
local res, err = openidc.authenticate(opts)
if err then
ngx.log(ngx.ERR, "Authentication error: ", err)
ngx.status = 500
ngx.say("Authentication failed")
ngx.exit(500)
end
-- Set user information in headers
if res.user then
ngx.req.set_header("X-User-Id", res.user.sub or "")
ngx.req.set_header("X-User-Email", res.user.email or "")
ngx.req.set_header("X-User-Name", res.user.name or "")
ngx.req.set_header("X-User-Groups", cjson.encode(res.user.groups or {}))
end
-- Log successful authentication
ngx.log(ngx.INFO, "User authenticated: ", res.user.email or "unknown")
Create virtual host configuration
Configure a virtual host that uses the Lua authentication script to protect web applications.
server {
listen 443 ssl http2;
server_name example.com;
# SSL configuration
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# OAuth2 callback endpoint
location /auth {
access_by_lua_file /etc/openresty/lua/auth.lua;
return 302 /;
}
# Logout endpoint
location /logout {
access_by_lua_block {
local openidc = require("resty.openidc")
local opts = {
discovery = "https://keycloak.example.com/realms/master/.well-known/openid_configuration",
post_logout_redirect_uri = "https://example.com/"
}
openidc.logout(opts)
}
}
# Protected application routes
location / {
access_by_lua_file /etc/openresty/lua/auth.lua;
# Proxy to backend application
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Pass user information to backend
proxy_set_header X-User-Id $http_x_user_id;
proxy_set_header X-User-Email $http_x_user_email;
proxy_set_header X-User-Name $http_x_user_name;
proxy_set_header X-User-Groups $http_x_user_groups;
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering on;
proxy_buffer_size 8k;
proxy_buffers 8 8k;
}
# Health check endpoint (no authentication required)
location /health {
access_log off;
return 200 "OK";
add_header Content-Type text/plain;
}
# Static assets (no authentication required)
location /static {
root /var/www/html;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Create systemd service file
Create a systemd service file for better OpenResty management and automatic startup.
[Unit]
Description=OpenResty Web Server
After=network.target
[Service]
Type=forking
PIDFile=/run/openresty.pid
ExecStartPre=/usr/local/openresty/bin/openresty -t -c /etc/openresty/nginx.conf
ExecStart=/usr/local/openresty/bin/openresty -c /etc/openresty/nginx.conf
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Set correct file permissions
Configure proper ownership and permissions for OpenResty configuration files and directories.
sudo chown -R root:root /etc/openresty
sudo chmod 755 /etc/openresty
sudo chmod 755 /etc/openresty/conf.d
sudo chmod 755 /etc/openresty/lua
sudo chmod 644 /etc/openresty/nginx.conf
sudo chmod 644 /etc/openresty/conf.d/*.conf
sudo chmod 644 /etc/openresty/lua/*.lua
Create log rotation configuration
Set up log rotation to prevent OpenResty logs from filling up disk space.
/var/log/openresty/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 644 nobody nobody
postrotate
if [ -f /run/openresty.pid ]; then
kill -USR1 cat /run/openresty.pid
fi
endscript
}
Enable and start OpenResty service
Enable the OpenResty service to start automatically on boot and start it now.
sudo systemctl daemon-reload
sudo systemctl enable openresty
sudo systemctl start openresty
sudo systemctl status openresty
Configure firewall rules
Open the necessary ports for HTTPS traffic and close unnecessary ports.
sudo ufw allow 443/tcp comment 'OpenResty HTTPS'
sudo ufw allow 80/tcp comment 'OpenResty HTTP redirect'
sudo ufw reload
Verify your setup
Test the OpenResty configuration and OAuth2 integration with these verification commands.
sudo /usr/local/openresty/bin/openresty -t -c /etc/openresty/nginx.conf
sudo systemctl status openresty
curl -I https://example.com/health
grep -i error /var/log/openresty/error.log | tail -5
Test the OAuth2 flow by accessing your protected application:
curl -L -I https://example.com/
Should redirect to Keycloak login page
grep "User authenticated" /var/log/openresty/access.log | tail -5
Advanced security configuration
Configure session security
Enhance session security with additional protection mechanisms.
local openidc = require("resty.openidc")
local cjson = require("cjson")
-- Enhanced security configuration
local opts = {
session_opts = {
secret = "your-32-character-session-secret-key",
cookie_name = "OIDC_SESSION",
cookie_lifetime = 1800, -- 30 minutes
cookie_secure = true,
cookie_httponly = true,
cookie_samesite = "Strict",
storage = "cookie",
check_ssi = false
},
-- Token refresh settings
refresh_session_interval = 900, -- 15 minutes
access_token_expires_in = 300, -- 5 minutes
-- Rate limiting
rate_limit_count = 10,
rate_limit_period = 60,
-- IP whitelist (optional)
-- whitelist = { "203.0.113.0/24", "198.51.100.0/24" }
}
-- Implement rate limiting
local limit_req = require("resty.limit.req")
local lim, err = limit_req.new("rl_store", 10, 5)
if not lim then
ngx.log(ngx.ERR, "Failed to instantiate rate limiter: ", err)
return ngx.exit(500)
end
local key = ngx.var.binary_remote_addr
local delay, err = lim:incoming(key, true)
if not delay then
if err == "rejected" then
ngx.log(ngx.WARN, "Rate limit exceeded for ", ngx.var.remote_addr)
return ngx.exit(429)
end
ngx.log(ngx.ERR, "Rate limiter error: ", err)
return ngx.exit(500)
end
-- Continue with authentication
local res, err = openidc.authenticate(opts)
if err then
ngx.log(ngx.ERR, "Authentication failed: ", err)
return ngx.exit(500)
end
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Authentication loop redirects | Incorrect redirect URI in Keycloak client | Verify redirect URI matches exactly: https://example.com/auth |
| SSL certificate errors | Missing or invalid SSL certificates | Install valid SSL certificates or use Let's Encrypt: certbot --nginx |
| Lua module not found errors | Incorrect lua_package_path configuration | Check paths in nginx.conf and verify module installation with opm list |
| Session cookie not set | Insecure cookie settings over HTTP | Ensure cookie_secure is false for HTTP or use HTTPS with proper SSL setup |
| Keycloak discovery fails | DNS resolution or connectivity issues | Test connectivity: curl https://keycloak.example.com/realms/master/.well-known/openid_configuration |
| 403 Forbidden on static files | Incorrect file permissions or ownership | Set proper permissions: sudo chown -R www-data:www-data /var/www/html && sudo chmod -R 644 /var/www/html |
Performance optimization
For production environments, consider implementing these performance enhancements that work well with the OpenResty PostgreSQL connection pooling tutorial.
Configure Lua shared dictionaries
Optimize memory usage and caching for OAuth2 tokens and session data.
# Add to http block
lua_shared_dict discovery 10m;
lua_shared_dict jwks 10m;
lua_shared_dict introspection 50m;
lua_shared_dict sessions 100m;
lua_shared_dict rl_store 10m; # Rate limiting
Next steps
- Configure NGINX rate limiting and advanced security rules for additional DDoS protection
- Set up NGINX web application firewall with ModSecurity 3 for comprehensive threat protection
- Configure Keycloak SAML federation for enterprise identity management
- Implement OpenResty Lua caching with Redis backend for session storage scalability
- Configure OpenResty WebSocket authentication with JWT tokens
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Default values
DOMAIN=""
KEYCLOAK_URL=""
CLIENT_ID="openresty-client"
CLIENT_SECRET=""
# Cleanup function
cleanup() {
echo -e "${RED}[ERROR] Installation failed. Cleaning up...${NC}"
systemctl stop openresty 2>/dev/null || true
rm -f /etc/apt/sources.list.d/openresty.list 2>/dev/null || true
rm -f /etc/yum.repos.d/openresty.repo 2>/dev/null || true
exit 1
}
trap cleanup ERR
# Usage message
usage() {
echo "Usage: $0 -d domain -k keycloak_url -s client_secret [options]"
echo " -d domain Your domain (e.g., example.com)"
echo " -k keycloak_url Keycloak base URL (e.g., https://auth.example.com)"
echo " -s client_secret Keycloak client secret"
echo " -c client_id Client ID (default: openresty-client)"
echo " -h Show this help"
exit 1
}
# Parse arguments
while getopts "d:k:s:c:h" opt; do
case $opt in
d) DOMAIN="$OPTARG" ;;
k) KEYCLOAK_URL="$OPTARG" ;;
s) CLIENT_SECRET="$OPTARG" ;;
c) CLIENT_ID="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
# Check required arguments
if [[ -z "$DOMAIN" || -z "$KEYCLOAK_URL" || -z "$CLIENT_SECRET" ]]; then
echo -e "${RED}Error: Missing required arguments${NC}"
usage
fi
# Check prerequisites
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}Error: This script must be run as root${NC}"
exit 1
fi
# Auto-detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
NGINX_USER="www-data"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
NGINX_USER="nginx"
# Enable EPEL for RHEL-based systems
if command -v dnf >/dev/null 2>&1; then
dnf install -y epel-release 2>/dev/null || true
fi
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
NGINX_USER="nginx"
;;
*)
echo -e "${RED}Error: Unsupported distribution: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Error: Cannot detect distribution${NC}"
exit 1
fi
echo -e "${GREEN}Keycloak OAuth2 OpenResty Installation Script${NC}"
echo -e "${BLUE}Domain: $DOMAIN${NC}"
echo -e "${BLUE}Keycloak URL: $KEYCLOAK_URL${NC}"
echo -e "${YELLOW}[1/8] Updating system packages...${NC}"
$PKG_UPDATE
echo -e "${YELLOW}[2/8] Installing dependencies...${NC}"
if [[ "$PKG_MGR" == "apt" ]]; then
$PKG_INSTALL wget gnupg2 software-properties-common curl luarocks
else
$PKG_INSTALL wget gnupg2 curl luarocks
fi
echo -e "${YELLOW}[3/8] Adding OpenResty repository...${NC}"
if [[ "$PKG_MGR" == "apt" ]]; then
wget -qO - https://openresty.org/package/pubkey.gpg | gpg --dearmor -o /usr/share/keyrings/openresty.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/openresty.gpg] http://openresty.org/package/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/openresty.list
apt update
else
cat > /etc/yum.repos.d/openresty.repo << 'EOF'
[openresty]
name=Official OpenResty Open Source Repository for CentOS
baseurl=https://openresty.org/package/rhel/$releasever/$basearch
skip_if_unavailable=False
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://openresty.org/package/pubkey.gpg
enabled=1
enabled_metadata=1
EOF
fi
echo -e "${YELLOW}[4/8] Installing OpenResty...${NC}"
$PKG_INSTALL openresty openresty-opm
echo -e "${YELLOW}[5/8] Installing Lua modules...${NC}"
/usr/local/openresty/bin/opm get zmartzone/lua-resty-openidc
luarocks install lua-resty-jwt
luarocks install lua-resty-session
luarocks install lua-resty-http
echo -e "${YELLOW}[6/8] Creating directory structure...${NC}"
mkdir -p /etc/openresty/conf.d
mkdir -p /etc/openresty/lua
mkdir -p /var/log/openresty
chown -R $NGINX_USER:$NGINX_USER /var/log/openresty
echo -e "${YELLOW}[7/8] Creating configuration files...${NC}"
# Main nginx configuration
cat > /usr/local/openresty/nginx/conf/nginx.conf << EOF
user $NGINX_USER;
worker_processes auto;
error_log /var/log/openresty/error.log;
pid /run/openresty.pid;
events {
worker_connections 1024;
use epoll;
}
http {
lua_package_path '/usr/local/openresty/site/lualib/?.lua;/etc/openresty/lua/?.lua;;';
lua_package_cpath '/usr/local/openresty/site/lualib/?.so;;';
lua_shared_dict discovery 1m;
lua_shared_dict jwks 1m;
lua_shared_dict introspection 10m;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
include /usr/local/openresty/nginx/conf/mime.types;
default_type application/octet-stream;
log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" '
'\$status \$body_bytes_sent "\$http_referer" '
'"\$http_user_agent" "\$http_x_forwarded_for"';
access_log /var/log/openresty/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 4096;
include /etc/openresty/conf.d/*.conf;
}
EOF
# Lua authentication script
cat > /etc/openresty/lua/auth.lua << EOF
local openidc = require("resty.openidc")
local cjson = require("cjson")
local opts = {
redirect_uri = "https://$DOMAIN/auth",
discovery = "$KEYCLOAK_URL/realms/master/.well-known/openid_configuration",
client_id = "$CLIENT_ID",
client_secret = "$CLIENT_SECRET",
redirect_uri_scheme = "https",
logout_path = "/logout",
redirect_after_logout_uri = "https://$DOMAIN/",
session_contents = {id_token=true}
}
local res, err = openidc.authenticate(opts)
if err then
ngx.status = 500
ngx.say(err)
ngx.exit(500)
end
ngx.req.set_header("X-USER", res.id_token.sub)
ngx.req.set_header("X-EMAIL", res.id_token.email)
EOF
# Virtual host configuration
cat > /etc/openresty/conf.d/default.conf << EOF
server {
listen 80;
server_name $DOMAIN;
return 301 https://\$server_name\$request_uri;
}
server {
listen 443 ssl;
server_name $DOMAIN;
# SSL configuration - replace with your certificates
ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
access_by_lua_file /etc/openresty/lua/auth.lua;
location / {
default_type text/html;
content_by_lua_block {
ngx.say("<html><body><h1>Protected Content</h1>")
ngx.say("<p>Welcome, " .. (ngx.var.http_x_user or "unknown") .. "</p>")
ngx.say("<p>Email: " .. (ngx.var.http_x_email or "unknown") .. "</p>")
ngx.say('<p><a href="/logout">Logout</a></p>')
ngx.say("</body></html>")
}
}
location /auth {
access_by_lua_file /etc/openresty/lua/auth.lua;
return 302 /;
}
}
EOF
# Set proper permissions
chmod 644 /usr/local/openresty/nginx/conf/nginx.conf
chmod 644 /etc/openresty/conf.d/default.conf
chmod 644 /etc/openresty/lua/auth.lua
chown root:root /etc/openresty/conf.d/default.conf
chown root:root /etc/openresty/lua/auth.lua
# Create systemd service
cat > /etc/systemd/system/openresty.service << 'EOF'
[Unit]
Description=OpenResty web server
After=network.target
[Service]
Type=forking
PIDFile=/run/openresty.pid
ExecStartPre=/usr/local/openresty/bin/openresty -t
ExecStart=/usr/local/openresty/bin/openresty
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
KillSignal=SIGQUIT
TimeoutStopSec=5
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
echo -e "${YELLOW}[8/8] Starting and enabling OpenResty...${NC}"
systemctl enable openresty
systemctl start openresty
# Configure firewall
if command -v ufw >/dev/null 2>&1; then
ufw --force enable
ufw allow 80/tcp
ufw allow 443/tcp
elif command -v firewall-cmd >/dev/null 2>&1; then
systemctl enable --now firewalld
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
fi
echo -e "${GREEN}[SUCCESS] Keycloak OAuth2 OpenResty installation completed!${NC}"
echo -e "${BLUE}Next steps:${NC}"
echo "1. Replace SSL certificates in /etc/openresty/conf.d/default.conf"
echo "2. Configure DNS for $DOMAIN"
echo "3. Create Keycloak client with ID: $CLIENT_ID"
echo "4. Set redirect URI in Keycloak: https://$DOMAIN/auth"
echo "5. Test authentication at https://$DOMAIN"
Review the script before running. Execute with: bash install.sh