Implement OpenResty JWT authentication with OAuth2 integration for secure web applications

Advanced 45 min Apr 04, 2026 169 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Set up secure JWT-based authentication in OpenResty with OAuth2 provider integration using lua-resty-jwt module. Configure token validation, authentication middleware, and security policies for production web applications.

Prerequisites

  • Root or sudo access
  • Domain name with DNS configured
  • OAuth2 provider (Google, Auth0, Keycloak, etc.)
  • SSL certificates
  • Basic understanding of JWT and OAuth2 flows

What this solves

OpenResty JWT authentication with OAuth2 integration provides secure, stateless authentication for modern web applications. This implementation eliminates the need for server-side session storage while ensuring secure token validation and OAuth2 provider integration. You'll configure lua-resty-jwt for token handling, implement authentication middleware, and establish security policies that protect your applications from unauthorized access.

Step-by-step configuration

Update system packages

Start by updating your package manager to ensure you get the latest versions of all dependencies.

sudo apt update && sudo apt upgrade -y
sudo dnf update -y

Install OpenResty and required dependencies

Install OpenResty web server with Lua scripting support and additional tools needed for JWT authentication.

sudo apt install -y wget gnupg ca-certificates
wget -qO - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
echo "deb 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 luarocks git build-essential libssl-dev
sudo dnf install -y wget
wget https://openresty.org/package/rhel/openresty.repo
sudo mv openresty.repo /etc/yum.repos.d/
sudo dnf install -y openresty luarocks git gcc gcc-c++ openssl-devel

Install lua-resty-jwt module

Install the JWT library for OpenResty that handles token creation, parsing, and validation with cryptographic signatures.

sudo luarocks install lua-resty-jwt
sudo luarocks install lua-resty-http
sudo luarocks install lua-cjson

Create directory structure

Set up the necessary directories for OpenResty configuration, Lua modules, and SSL certificates with proper permissions.

sudo mkdir -p /etc/openresty/conf.d
sudo mkdir -p /etc/openresty/lua
sudo mkdir -p /etc/openresty/ssl
sudo mkdir -p /var/log/openresty
sudo chown -R openresty:openresty /etc/openresty /var/log/openresty
sudo chmod 755 /etc/openresty /var/log/openresty
sudo chmod 750 /etc/openresty/ssl

Generate JWT signing keys

Create RSA key pairs for JWT token signing and verification. The private key signs tokens while the public key validates them.

sudo openssl genrsa -out /etc/openresty/ssl/jwt_private.key 2048
sudo openssl rsa -in /etc/openresty/ssl/jwt_private.key -pubout -out /etc/openresty/ssl/jwt_public.key
sudo chown openresty:openresty /etc/openresty/ssl/jwt_*.key
sudo chmod 600 /etc/openresty/ssl/jwt_private.key
sudo chmod 644 /etc/openresty/ssl/jwt_public.key
Never use chmod 777. It gives every user on the system full access to your files. Instead, fix ownership with chown and use minimal permissions like 600 for private keys and 644 for public keys.

Create JWT authentication module

Develop the core Lua module that handles JWT token validation, OAuth2 integration, and authentication logic.

local jwt = require "resty.jwt"
local http = require "resty.http"
local cjson = require "cjson"
local io = require "io"

local _M = {}

-- Configuration
_M.jwt_secret_key = nil
_M.oauth2_introspect_url = "https://oauth.example.com/oauth/introspect"
_M.oauth2_client_id = "your-client-id"
_M.oauth2_client_secret = "your-client-secret"

-- Load JWT public key
function _M.load_jwt_key()
    local file = io.open("/etc/openresty/ssl/jwt_public.key", "r")
    if not file then
        ngx.log(ngx.ERR, "Could not open JWT public key file")
        return nil
    end
    local key = file:read("*all")
    file:close()
    return key
end

-- Validate JWT token
function _M.validate_jwt(token)
    if not _M.jwt_secret_key then
        _M.jwt_secret_key = _M.load_jwt_key()
        if not _M.jwt_secret_key then
            return false, "JWT key not available"
        end
    end
    
    local jwt_token = jwt:verify(_M.jwt_secret_key, token, {
        alg = "RS256"
    })
    
    if not jwt_token.verified then
        return false, "JWT verification failed"
    end
    
    -- Check expiration
    if jwt_token.payload.exp and jwt_token.payload.exp < ngx.time() then
        return false, "JWT token expired"
    end
    
    return true, jwt_token.payload
end

-- OAuth2 token introspection
function _M.introspect_oauth2_token(token)
    local httpc = http.new()
    httpc:set_timeout(5000)
    
    local res, err = httpc:request_uri(_M.oauth2_introspect_url, {
        method = "POST",
        headers = {
            ["Content-Type"] = "application/x-www-form-urlencoded",
            ["Authorization"] = "Basic " .. ngx.encode_base64(_M.oauth2_client_id .. ":" .. _M.oauth2_client_secret)
        },
        body = "token=" .. token
    })
    
    if not res then
        ngx.log(ngx.ERR, "OAuth2 introspection failed: ", err)
        return false, "OAuth2 introspection error"
    end
    
    if res.status ~= 200 then
        return false, "OAuth2 introspection failed with status: " .. res.status
    end
    
    local data = cjson.decode(res.body)
    if not data.active then
        return false, "OAuth2 token is not active"
    end
    
    return true, data
end

-- Extract token from request
function _M.extract_token()
    local auth_header = ngx.var.http_authorization
    if not auth_header then
        return nil, "No authorization header"
    end
    
    local token = auth_header:match("Bearer%s+(.+)")
    if not token then
        return nil, "Invalid authorization header format"
    end
    
    return token
end

-- Main authentication function
function _M.authenticate()
    local token, err = _M.extract_token()
    if not token then
        ngx.log(ngx.ERR, "Token extraction failed: ", err)
        ngx.status = 401
        ngx.header["Content-Type"] = "application/json"
        ngx.say(cjson.encode({error = "unauthorized", message = err}))
        ngx.exit(401)
    end
    
    -- Try JWT validation first
    local valid, payload = _M.validate_jwt(token)
    if valid then
        -- Set user context
        ngx.var.auth_user_id = payload.sub or payload.user_id
        ngx.var.auth_user_email = payload.email
        ngx.var.auth_user_roles = cjson.encode(payload.roles or {})
        return
    end
    
    -- Fallback to OAuth2 introspection
    valid, payload = _M.introspect_oauth2_token(token)
    if valid then
        -- Set user context from OAuth2 response
        ngx.var.auth_user_id = payload.sub or payload.username
        ngx.var.auth_user_email = payload.email
        ngx.var.auth_user_roles = cjson.encode(payload.scope and {payload.scope} or {})
        return
    end
    
    -- Authentication failed
    ngx.log(ngx.ERR, "Authentication failed: ", payload)
    ngx.status = 401
    ngx.header["Content-Type"] = "application/json"
    ngx.say(cjson.encode({error = "unauthorized", message = "Invalid or expired token"}))
    ngx.exit(401)
end

-- Role-based access control
function _M.require_role(required_role)
    local user_roles = ngx.var.auth_user_roles
    if not user_roles then
        ngx.status = 403
        ngx.header["Content-Type"] = "application/json"
        ngx.say(cjson.encode({error = "forbidden", message = "No roles assigned"}))
        ngx.exit(403)
    end
    
    local roles = cjson.decode(user_roles)
    for _, role in ipairs(roles) do
        if role == required_role then
            return
        end
    end
    
    ngx.status = 403
    ngx.header["Content-Type"] = "application/json"
    ngx.say(cjson.encode({error = "forbidden", message = "Insufficient permissions"}))
    ngx.exit(403)
end

return _M

Configure main OpenResty configuration

Set up the primary OpenResty configuration with Lua package paths, security headers, and authentication variables.

user openresty;
worker_processes auto;
error_log /var/log/openresty/error.log;
pid /var/run/openresty.pid;

events {
    worker_connections 1024;
    use epoll;
}

http {
    include       /etc/openresty/mime.types;
    default_type  application/octet-stream;
    
    # Lua package path
    lua_package_path "/etc/openresty/lua/?.lua;;";
    lua_shared_dict jwt_cache 10m;
    
    # Security headers
    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;
    
    # Logging
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for" '
                    'auth_user="$auth_user_id"';
    
    access_log /var/log/openresty/access.log main;
    
    # Performance settings
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    
    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1000;
    gzip_types text/plain application/json application/javascript text/css;
    
    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/s;
    
    include /etc/openresty/conf.d/*.conf;
}

Create application server configuration

Configure a virtual server with JWT authentication, OAuth2 integration, and protected API endpoints with role-based access control.

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com;
    
    # SSL configuration
    ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
    ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    
    # Authentication variables
    set $auth_user_id "";
    set $auth_user_email "";
    set $auth_user_roles "";
    
    # Health check endpoint (no auth required)
    location = /health {
        access_log off;
        return 200 'healthy';
        add_header Content-Type text/plain;
    }
    
    # Authentication endpoint for OAuth2 callback
    location = /auth/callback {
        limit_req zone=auth burst=10 nodelay;
        
        access_by_lua_block {
            local jwt_auth = require "jwt_auth"
            local cjson = require "cjson"
            
            -- Handle OAuth2 callback and create JWT
            local code = ngx.var.arg_code
            if not code then
                ngx.status = 400
                ngx.say(cjson.encode({error = "missing_code"}))
                ngx.exit(400)
            end
            
            -- Exchange code for token (implement based on your OAuth2 provider)
            -- This is a simplified example
            ngx.header["Content-Type"] = "application/json"
            ngx.say(cjson.encode({message = "OAuth2 callback processed"}))
        }
    }
    
    # Public API endpoints (no authentication)
    location /api/public {
        limit_req zone=api burst=20 nodelay;
        
        # Proxy to your application backend
        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;
    }
    
    # Protected API endpoints (JWT authentication required)
    location /api/protected {
        limit_req zone=api burst=20 nodelay;
        
        access_by_lua_block {
            local jwt_auth = require "jwt_auth"
            jwt_auth.authenticate()
        }
        
        # Proxy to your application backend with user context
        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;
        proxy_set_header X-Auth-User-ID $auth_user_id;
        proxy_set_header X-Auth-User-Email $auth_user_email;
        proxy_set_header X-Auth-User-Roles $auth_user_roles;
    }
    
    # Admin endpoints (requires admin role)
    location /api/admin {
        limit_req zone=api burst=10 nodelay;
        
        access_by_lua_block {
            local jwt_auth = require "jwt_auth"
            jwt_auth.authenticate()
            jwt_auth.require_role("admin")
        }
        
        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;
        proxy_set_header X-Auth-User-ID $auth_user_id;
        proxy_set_header X-Auth-User-Email $auth_user_email;
        proxy_set_header X-Auth-User-Roles $auth_user_roles;
    }
    
    # Static files
    location / {
        root /var/www/html;
        index index.html index.htm;
        try_files $uri $uri/ =404;
    }
}

HTTP redirect to HTTPS

server { listen 80; listen [::]:80; server_name example.com; return 301 https://$server_name$request_uri; }

Create JWT token generation utility

Build a Lua script for generating JWT tokens during development and testing of your authentication system.

local jwt = require "resty.jwt"
local cjson = require "cjson"
local io = require "io"

-- Load private key for signing
local function load_private_key()
    local file = io.open("/etc/openresty/ssl/jwt_private.key", "r")
    if not file then
        error("Could not open JWT private key file")
    end
    local key = file:read("*all")
    file:close()
    return key
end

-- Generate JWT token
local function generate_token(payload)
    local private_key = load_private_key()
    
    -- Set default expiration (1 hour)
    payload.exp = payload.exp or (os.time() + 3600)
    payload.iat = payload.iat or os.time()
    payload.iss = payload.iss or "openresty-jwt"
    
    local token = jwt:sign(private_key, {
        header = {
            typ = "JWT",
            alg = "RS256"
        },
        payload = payload
    })
    
    return token
end

-- Example usage
if arg and arg[1] then
    local payload = {
        sub = arg[1] or "user123",
        email = arg[2] or "user@example.com",
        roles = {arg[3] or "user"}
    }
    
    local token = generate_token(payload)
    print("JWT Token:")
    print(token)
    print("\nTest with:")
    print("curl -H 'Authorization: Bearer " .. token .. "' https://example.com/api/protected")
else
    print("Usage: lua jwt_generator.lua  [email] [role]")
    print("Example: lua jwt_generator.lua user123 user@example.com admin")
end

return {
    generate_token = generate_token
}

Configure systemd service

Create a systemd service file to manage OpenResty with proper security settings and automatic restart capabilities.

[Unit]
Description=OpenResty HTTP Server
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/var/run/openresty.pid
ExecStartPre=/usr/bin/openresty -t -c /etc/openresty/nginx.conf
ExecStart=/usr/bin/openresty -c /etc/openresty/nginx.conf
ExecReload=/bin/kill -s HUP $MAINPID
KillSignal=SIGQUIT
TimeoutStopSec=5
KillMode=process
PrivateTmp=true
RestartSec=2
Restart=on-failure
User=openresty
Group=openresty

Security settings

NoNewPrivileges=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/var/log/openresty /var/run [Install] WantedBy=multi-user.target

Create OpenResty user and set permissions

Create a dedicated system user for OpenResty with minimal privileges and proper directory ownership.

sudo useradd --system --no-create-home --shell /bin/false --group openresty openresty
sudo chown -R openresty:openresty /etc/openresty /var/log/openresty
sudo chmod -R 755 /etc/openresty
sudo chmod -R 750 /etc/openresty/ssl /var/log/openresty
sudo chmod 600 /etc/openresty/ssl/jwt_private.key

Enable and start OpenResty service

Enable OpenResty to start automatically on boot and start the service immediately.

sudo systemctl daemon-reload
sudo systemctl enable openresty
sudo systemctl start openresty

Configure firewall rules

Open the necessary ports for HTTP and HTTPS traffic while maintaining security.

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reload
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

Verify your setup

Test your OpenResty JWT authentication system to ensure all components are working correctly.

# Check OpenResty service status
sudo systemctl status openresty

Verify configuration syntax

sudo openresty -t -c /etc/openresty/nginx.conf

Generate a test JWT token

cd /etc/openresty/lua sudo -u openresty lua jwt_generator.lua testuser user@example.com admin

Test public endpoint (should work without token)

curl -i https://example.com/api/public

Test protected endpoint without token (should return 401)

curl -i https://example.com/api/protected

Test protected endpoint with token (replace TOKEN with generated JWT)

curl -i -H "Authorization: Bearer TOKEN" https://example.com/api/protected

Check logs for authentication events

sudo tail -f /var/log/openresty/access.log

Configure OAuth2 provider integration

Update OAuth2 configuration

Modify the JWT authentication module to include your specific OAuth2 provider settings and endpoints.

sudo nano /etc/openresty/lua/jwt_auth.lua

Update the OAuth2 configuration variables at the top of the file:

-- Replace with your OAuth2 provider settings
_M.oauth2_introspect_url = "https://auth.yourprovider.com/oauth/introspect"
_M.oauth2_client_id = "your-actual-client-id"
_M.oauth2_client_secret = "your-actual-client-secret"
_M.oauth2_authorize_url = "https://auth.yourprovider.com/oauth/authorize"
_M.oauth2_token_url = "https://auth.yourprovider.com/oauth/token"

Implement OAuth2 authorization flow

Add a complete OAuth2 authorization flow handler to your application configuration.

local http = require "resty.http"
local cjson = require "cjson"
local jwt = require "resty.jwt"

local _M = {}

-- OAuth2 configuration
_M.client_id = "your-client-id"
_M.client_secret = "your-client-secret"
_M.redirect_uri = "https://example.com/auth/callback"
_M.authorize_url = "https://auth.yourprovider.com/oauth/authorize"
_M.token_url = "https://auth.yourprovider.com/oauth/token"
_M.userinfo_url = "https://auth.yourprovider.com/userinfo"

-- Generate OAuth2 authorization URL
function _M.get_auth_url(state, scope)
    local params = {
        "response_type=code",
        "client_id=" .. _M.client_id,
        "redirect_uri=" .. ngx.escape_uri(_M.redirect_uri),
        "scope=" .. ngx.escape_uri(scope or "openid profile email"),
        "state=" .. ngx.escape_uri(state or ngx.time())
    }
    return _M.authorize_url .. "?" .. table.concat(params, "&")
end

-- Exchange authorization code for access token
function _M.exchange_code(code, state)
    local httpc = http.new()
    httpc:set_timeout(10000)
    
    local res, err = httpc:request_uri(_M.token_url, {
        method = "POST",
        headers = {
            ["Content-Type"] = "application/x-www-form-urlencoded",
            ["Accept"] = "application/json"
        },
        body = table.concat({
            "grant_type=authorization_code",
            "code=" .. ngx.escape_uri(code),
            "client_id=" .. _M.client_id,
            "client_secret=" .. _M.client_secret,
            "redirect_uri=" .. ngx.escape_uri(_M.redirect_uri)
        }, "&")
    })
    
    if not res or res.status ~= 200 then
        return nil, "Token exchange failed"
    end
    
    return cjson.decode(res.body)
end

-- Get user information from OAuth2 provider
function _M.get_userinfo(access_token)
    local httpc = http.new()
    httpc:set_timeout(5000)
    
    local res, err = httpc:request_uri(_M.userinfo_url, {
        method = "GET",
        headers = {
            ["Authorization"] = "Bearer " .. access_token,
            ["Accept"] = "application/json"
        }
    })
    
    if not res or res.status ~= 200 then
        return nil, "Failed to get user info"
    end
    
    return cjson.decode(res.body)
end

return _M

Add OAuth2 login endpoint

Create a login endpoint that redirects users to the OAuth2 provider for authentication.

Add this location block to your /etc/openresty/conf.d/app.conf file:

    # OAuth2 login endpoint
    location = /auth/login {
        access_by_lua_block {
            local oauth2 = require "oauth2_handler"
            local auth_url = oauth2.get_auth_url()
            ngx.redirect(auth_url)
        }
    }

Update OAuth2 callback handler

Replace the simple callback handler with a complete OAuth2 flow implementation that exchanges codes for tokens.

Replace the existing /auth/callback location block in /etc/openresty/conf.d/app.conf:

    # OAuth2 callback endpoint
    location = /auth/callback {
        limit_req zone=auth burst=10 nodelay;
        
        access_by_lua_block {
            local oauth2 = require "oauth2_handler"
            local jwt_auth = require "jwt_auth"
            local cjson = require "cjson"
            
            local code = ngx.var.arg_code
            local state = ngx.var.arg_state
            local error = ngx.var.arg_error
            
            if error then
                ngx.status = 400
                ngx.header["Content-Type"] = "application/json"
                ngx.say(cjson.encode({error = "oauth2_error", message = error}))
                ngx.exit(400)
            end
            
            if not code then
                ngx.status = 400
                ngx.header["Content-Type"] = "application/json"
                ngx.say(cjson.encode({error = "missing_code", message = "Authorization code required"}))
                ngx.exit(400)
            end
            
            -- Exchange code for tokens
            local tokens, err = oauth2.exchange_code(code, state)
            if not tokens then
                ngx.status = 400
                ngx.header["Content-Type"] = "application/json"
                ngx.say(cjson.encode({error = "token_exchange_failed", message = err}))
                ngx.exit(400)
            end
            
            -- Get user information
            local userinfo, err = oauth2.get_userinfo(tokens.access_token)
            if not userinfo then
                ngx.status = 400
                ngx.header["Content-Type"] = "application/json"
                ngx.say(cjson.encode({error = "userinfo_failed", message = err}))
                ngx.exit(400)
            end
            
            -- Create JWT payload from OAuth2 user info
            local jwt_payload = {
                sub = userinfo.sub or userinfo.id,
                email = userinfo.email,
                name = userinfo.name,
                roles = userinfo.roles or {"user"},
                exp = ngx.time() + 3600, -- 1 hour
                iat = ngx.time(),
                iss = "openresty-oauth2"
            }
            
            -- Generate internal JWT token
            local generator = require "jwt_generator"
            local jwt_token = generator.generate_token(jwt_payload)
            
            -- Set secure cookie with JWT
            ngx.header["Set-Cookie"] = "auth_token=" .. jwt_token .. "; HttpOnly; Secure; SameSite=Strict; Max-Age=3600; Path=/"
            
            -- Redirect to application
            ngx.redirect("/dashboard")
        }
    }

Reload OpenResty configuration

Apply the new OAuth2 configuration changes by reloading the OpenResty service.

sudo openresty -t -c /etc/openresty/nginx.conf
sudo systemctl reload openresty

Common issues

SymptomCauseFix
"JWT verification failed" errorWrong public key or algorithm mismatchCheck key file permissions and ensure RS256 algorithm
"Could not open JWT key file" errorFile permissions or missing key filesudo chmod 644 /etc/openresty/ssl/jwt_public.key
OAuth2 introspection timeoutNetwork connectivity or wrong endpointVerify OAuth2 provider URL and network access
"No authorization header" errorMissing Bearer token in requestInclude Authorization: Bearer token header
403 Forbidden with valid tokenInsufficient role permissionsCheck user roles in JWT payload and required roles
OpenResty won't startConfiguration syntax errorsudo openresty -t to check configuration
Module not found errorLua module path incorrectVerify lua_package_path in nginx.conf
SSL certificate errorsInvalid or self-signed certificatesInstall valid SSL certificates or update client trust

Next steps

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle infrastructure security hardening for businesses that depend on uptime. From initial setup to ongoing operations.