Set up Keycloak as an OAuth2 identity provider with OIDC authentication flows. Configure client applications, implement JWT token validation, and secure NGINX reverse proxy with lua-resty-openidc for production web applications.
Prerequisites
- Keycloak server running and accessible
- Domain with SSL certificates
- Basic understanding of OAuth2 and JWT concepts
- Root or sudo access to web server
What this solves
Keycloak OAuth2 integration provides centralized authentication and authorization for web applications using OpenID Connect (OIDC) protocol. This tutorial shows you how to configure Keycloak as an identity provider, create OAuth2 client applications, implement JWT token validation, and secure NGINX reverse proxy with lua-resty-openidc for production-grade single sign-on (SSO).
Step-by-step configuration
Install required packages
Install OpenResty with lua-resty-openidc module for OAuth2 integration and JWT token handling.
sudo apt update
sudo apt install -y openresty lua-resty-http lua-resty-jwt
wget https://github.com/zmartzone/lua-resty-openidc/archive/refs/tags/v1.7.6.tar.gz
tar xzf v1.7.6.tar.gz
sudo cp -r lua-resty-openidc-1.7.6/lib/resty/* /usr/local/openresty/lualib/resty/
Configure Keycloak OAuth2 client
Create a new client application in Keycloak admin console for your web application.
Access Keycloak admin console at https://your-keycloak-server:8443/auth/admin and create a new client:
- Client ID:
webapp-client - Client Protocol:
openid-connect - Access Type:
confidential - Valid Redirect URIs:
https://example.com/auth/callback - Web Origins:
https://example.com
Configure NGINX with OpenResty
Set up NGINX configuration with lua-resty-openidc for OAuth2 authentication and JWT token validation.
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
# Lua package path for resty modules
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
lua_shared_dict discovery 1m;
lua_shared_dict sessions 10m;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
# OIDC configuration
set $session_secret "your-32-char-session-secret-key";
set $oidc_discovery "https://your-keycloak-server:8443/auth/realms/master/.well-known/openid_configuration";
set $oidc_client_id "webapp-client";
set $oidc_client_secret "your-client-secret-from-keycloak";
set $oidc_redirect_uri "https://example.com/auth/callback";
location / {
access_by_lua_block {
local opts = {
discovery = ngx.var.oidc_discovery,
client_id = ngx.var.oidc_client_id,
client_secret = ngx.var.oidc_client_secret,
redirect_uri = ngx.var.oidc_redirect_uri,
scope = "openid email profile",
session_secret = ngx.var.session_secret,
token_signing_alg_values_expected = { "RS256" },
logout_path = "/logout",
redirect_after_logout_uri = "https://example.com",
refresh_session_interval = 900,
access_token_expires_leeway = 300
}
local res, err = require("resty.openidc").authenticate(opts)
if err then
ngx.log(ngx.ERR, "OIDC authentication error: ", err)
ngx.status = 500
ngx.say("Authentication error")
ngx.exit(500)
end
-- Set user info headers for backend application
ngx.req.set_header("X-User", res.id_token.preferred_username or res.id_token.sub)
ngx.req.set_header("X-User-Email", res.id_token.email or "")
ngx.req.set_header("X-Access-Token", res.access_token)
}
# Proxy to your 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;
}
location /auth/callback {
access_by_lua_block {
local opts = {
discovery = ngx.var.oidc_discovery,
client_id = ngx.var.oidc_client_id,
client_secret = ngx.var.oidc_client_secret,
redirect_uri = ngx.var.oidc_redirect_uri,
scope = "openid email profile",
session_secret = ngx.var.session_secret
}
require("resty.openidc").authenticate(opts)
}
return 302 https://example.com/;
}
location /logout {
access_by_lua_block {
local opts = {
discovery = ngx.var.oidc_discovery,
client_id = ngx.var.oidc_client_id,
client_secret = ngx.var.oidc_client_secret,
redirect_uri = ngx.var.oidc_redirect_uri,
session_secret = ngx.var.session_secret,
redirect_after_logout_uri = "https://example.com"
}
require("resty.openidc").logout(opts)
}
}
}
}
Generate session secret
Create a secure 32-character session secret for encrypting user sessions.
openssl rand -hex 32
Replace your-32-char-session-secret-key in the NGINX configuration with the generated value.
Configure Keycloak realm settings
Set up proper token settings and user attributes in Keycloak for JWT token validation.
In Keycloak admin console, navigate to Realm Settings and configure:
- Access Token Lifespan:
15 minutes - SSO Session Idle:
30 minutes - SSO Session Max:
10 hours - Access Token Lifespan For Implicit Flow:
15 minutes
Under Client Scopes, ensure the email and profile scopes include the necessary user attributes.
Create backend application JWT validation
Configure your backend application to validate JWT tokens from NGINX headers.
import jwt
import requests
from functools import wraps
from flask import request, jsonify, current_app
class KeycloakJWTValidator:
def __init__(self, keycloak_url, realm, client_id):
self.keycloak_url = keycloak_url
self.realm = realm
self.client_id = client_id
self.public_keys = {}
def get_public_keys(self):
"""Fetch public keys from Keycloak JWKS endpoint"""
jwks_url = f"{self.keycloak_url}/auth/realms/{self.realm}/protocol/openid-connect/certs"
response = requests.get(jwks_url)
jwks = response.json()
for key in jwks['keys']:
kid = key['kid']
self.public_keys[kid] = jwt.algorithms.RSAAlgorithm.from_jwk(key)
return self.public_keys
def validate_token(self, token):
"""Validate JWT token and return user claims"""
try:
# Decode token header to get key ID
unverified_header = jwt.get_unverified_header(token)
kid = unverified_header['kid']
# Get public key for token validation
if kid not in self.public_keys:
self.get_public_keys()
public_key = self.public_keys.get(kid)
if not public_key:
raise jwt.InvalidTokenError("Public key not found")
# Validate and decode token
decoded_token = jwt.decode(
token,
public_key,
algorithms=['RS256'],
audience=self.client_id,
options={"verify_exp": True, "verify_aud": True}
)
return decoded_token
except jwt.ExpiredSignatureError:
raise Exception("Token has expired")
except jwt.InvalidAudienceError:
raise Exception("Invalid token audience")
except jwt.InvalidTokenError as e:
raise Exception(f"Invalid token: {str(e)}")
def require_auth(validator):
"""Decorator to require valid JWT token"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
token = request.headers.get('X-Access-Token')
if not token:
return jsonify({'error': 'No access token provided'}), 401
try:
user_claims = validator.validate_token(token)
request.user = user_claims
return f(*args, **kwargs)
except Exception as e:
return jsonify({'error': str(e)}), 401
return decorated_function
return decorator
Usage example
validator = KeycloakJWTValidator(
keycloak_url="https://your-keycloak-server:8443",
realm="master",
client_id="webapp-client"
)
@app.route('/api/protected')
@require_auth(validator)
def protected_endpoint():
user_email = request.headers.get('X-User-Email')
return jsonify({
'message': 'Access granted',
'user': request.user.get('preferred_username'),
'email': user_email
})
Start and enable services
Start OpenResty with the OAuth2 configuration and enable it to start on boot.
sudo systemctl start openresty
sudo systemctl enable openresty
sudo systemctl status openresty
Configure firewall rules
Open necessary ports for HTTPS traffic and backend application communication.
sudo ufw allow 443/tcp
sudo ufw allow 80/tcp
sudo ufw reload
Verify your setup
Test the OAuth2 authentication flow and JWT token validation.
# Test NGINX configuration
sudo openresty -t
Check OpenResty status
sudo systemctl status openresty
Test OAuth2 redirect (should redirect to Keycloak)
curl -I https://example.com/
Verify Keycloak OIDC discovery endpoint
curl -s https://your-keycloak-server:8443/auth/realms/master/.well-known/openid_configuration | jq .
Access your application at https://example.com. You should be redirected to Keycloak for authentication, then redirected back to your application after successful login.
Advanced configuration options
Configure token refresh
Set up automatic token refresh to maintain user sessions without re-authentication.
local function refresh_access_token(opts, session)
local http = require("resty.http")
local httpc = http.new()
local res, err = httpc:request_uri(opts.token_endpoint, {
method = "POST",
body = ngx.encode_args({
grant_type = "refresh_token",
refresh_token = session.refresh_token,
client_id = opts.client_id,
client_secret = opts.client_secret
}),
headers = {
["Content-Type"] = "application/x-www-form-urlencoded"
}
})
if not res or res.status ~= 200 then
return nil, "Failed to refresh token"
end
local token_response = require("cjson").decode(res.body)
session.access_token = token_response.access_token
session.refresh_token = token_response.refresh_token or session.refresh_token
return session, nil
end
Configure role-based access control
Add role-based authorization using Keycloak realm roles and client roles.
local function check_user_roles(required_roles, user_roles)
for _, required_role in ipairs(required_roles) do
local has_role = false
for _, user_role in ipairs(user_roles or {}) do
if user_role == required_role then
has_role = true
break
end
end
if not has_role then
return false
end
end
return true
end
-- Usage in location block
location /admin {
access_by_lua_block {
local opts = {
discovery = ngx.var.oidc_discovery,
client_id = ngx.var.oidc_client_id,
client_secret = ngx.var.oidc_client_secret,
redirect_uri = ngx.var.oidc_redirect_uri,
scope = "openid email profile roles",
session_secret = ngx.var.session_secret
}
local res, err = require("resty.openidc").authenticate(opts)
if err then
ngx.status = 500
ngx.say("Authentication error")
ngx.exit(500)
end
-- Check for admin role
local realm_roles = res.id_token.realm_access and res.id_token.realm_access.roles or {}
if not check_user_roles({"admin"}, realm_roles) then
ngx.status = 403
ngx.say("Access denied: Admin role required")
ngx.exit(403)
end
}
proxy_pass http://127.0.0.1:8080;
}
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| "Invalid redirect URI" error | Keycloak client redirect URI mismatch | Update client Valid Redirect URIs in Keycloak admin console |
| "Failed to retrieve discovery document" | Keycloak URL or realm name incorrect | Verify $oidc_discovery URL returns valid JSON |
| "Session secret too short" | Session secret less than 32 characters | Generate new 32-character secret with openssl rand -hex 32 |
| "Token signature verification failed" | Client secret mismatch or algorithm issue | Check client secret in Keycloak and verify RS256 algorithm |
| "lua-resty-openidc not found" | Module not installed correctly | Verify module files in /usr/local/openresty/lualib/resty/ |
| Backend receives no user headers | Headers not passed through proxy | Check proxy_set_header directives in NGINX config |
Security considerations
Implement additional security measures for production environments.
- Use strong session secrets (32+ random characters)
- Implement proper CORS policies for your application domain
- Set appropriate token lifespans (15 minutes for access tokens)
- Enable Keycloak security headers and brute force protection
- Monitor authentication logs for suspicious activity
- Implement rate limiting on authentication endpoints
Next steps
- Secure Grafana with OAuth authentication and RBAC integration
- Configure Keycloak OAuth2 integration with OpenResty for enterprise SSO
- Set up Keycloak LDAP integration with Active Directory
- Implement Keycloak multi-factor authentication with TOTP
- Configure Keycloak high availability clustering for production