-- JWT validation module for OpenResty edge authentication -- Validates JWT tokens before requests hit the backend local jwt = require "resty.jwt" local _M = {} -- Get JWT secret (cached after first call) local jwt_secret_cache = nil local function get_jwt_secret() if jwt_secret_cache then return jwt_secret_cache end -- os.getenv works in OpenResty when 'env JWT_SECRET;' is in nginx.conf local secret = os.getenv("JWT_SECRET") if secret and secret ~= "" then jwt_secret_cache = secret ngx.log(ngx.INFO, "JWT_SECRET loaded successfully (", string.len(secret), " chars)") return secret end ngx.log(ngx.ERR, "JWT_SECRET environment variable not set or empty") return nil end -- Validate JWT and inject user headers for backend -- Call this in access_by_lua_block for authenticated endpoints function _M.validate_and_inject() -- Get token from httpOnly cookie (name: auth_token) local token = ngx.var.cookie_auth_token if not token or token == "" then ngx.log(ngx.DEBUG, "No auth_token cookie found") ngx.status = 401 ngx.header["Content-Type"] = "application/json" ngx.say('{"error":"No authentication token"}') return ngx.exit(401) end -- Get JWT secret local secret = get_jwt_secret() if not secret then -- SECURITY FIX: Fail closed - do NOT allow requests through if secret is missing -- This prevents authentication bypass if env var is misconfigured ngx.log(ngx.ERR, "JWT_SECRET not configured - blocking request") ngx.status = 500 ngx.header["Content-Type"] = "application/json" ngx.say('{"error":"Server configuration error"}') return ngx.exit(500) end -- Verify JWT signature and decode local jwt_obj = jwt:verify(secret, token) if not jwt_obj then ngx.log(ngx.ERR, "JWT verification returned nil") ngx.status = 401 ngx.header["Content-Type"] = "application/json" ngx.say('{"error":"Invalid token"}') return ngx.exit(401) end if not jwt_obj.verified then local reason = jwt_obj.reason or "unknown" ngx.log(ngx.WARN, "JWT verification failed: ", reason) ngx.status = 401 ngx.header["Content-Type"] = "application/json" ngx.say('{"error":"Invalid token: ' .. reason .. '"}') return ngx.exit(401) end -- Payload should exist if verified local payload = jwt_obj.payload if not payload then ngx.log(ngx.ERR, "JWT verified but no payload") ngx.status = 401 ngx.header["Content-Type"] = "application/json" ngx.say('{"error":"Invalid token structure"}') return ngx.exit(401) end -- Check expiry (jwt library should handle this, but be safe) local exp = payload.exp if exp then local now = ngx.time() if exp < now then ngx.log(ngx.DEBUG, "JWT expired: exp=", exp, " now=", now) ngx.status = 401 ngx.header["Content-Type"] = "application/json" ngx.say('{"error":"Token expired"}') return ngx.exit(401) end end -- Check issuer local iss = payload.iss if iss ~= "streaming-app" then ngx.log(ngx.WARN, "Invalid JWT issuer: ", tostring(iss)) ngx.status = 401 ngx.header["Content-Type"] = "application/json" ngx.say('{"error":"Invalid token issuer"}') return ngx.exit(401) end -- SECURITY: Check if user account is disabled local is_disabled = payload.is_disabled if is_disabled == "1" or is_disabled == true then ngx.log(ngx.WARN, "Disabled user attempted access: ", tostring(payload.username)) ngx.status = 403 ngx.header["Content-Type"] = "application/json" ngx.say('{"error":"Account disabled"}') return ngx.exit(403) end -- Inject user info headers for backend (allows backend to skip re-validation) ngx.req.set_header("X-User-ID", tostring(payload.user_id or "")) ngx.req.set_header("X-Username", tostring(payload.username or "")) ngx.req.set_header("X-Is-Admin", tostring(payload.is_admin or "0")) ngx.req.set_header("X-Is-Moderator", tostring(payload.is_moderator or "0")) ngx.req.set_header("X-Is-Streamer", tostring(payload.is_streamer or "0")) ngx.req.set_header("X-Is-Restreamer", tostring(payload.is_restreamer or "0")) ngx.req.set_header("X-Token-Version", tostring(payload.token_version or "1")) ngx.req.set_header("X-User-Color", tostring(payload.color_code or "#561D5E")) ngx.req.set_header("X-Avatar-URL", tostring(payload.avatar_url or "")) ngx.req.set_header("X-JWT-Validated", "true") ngx.log(ngx.DEBUG, "JWT validated for user: ", tostring(payload.username)) end -- Optional: Get user info without blocking (for endpoints that work with or without auth) -- Returns user payload if authenticated, nil otherwise function _M.get_user_if_authenticated() local token = ngx.var.cookie_auth_token if not token or token == "" then return nil end local secret = get_jwt_secret() if not secret then return nil end local jwt_obj = jwt:verify(secret, token) if not jwt_obj or not jwt_obj.verified then return nil end local payload = jwt_obj.payload if not payload then return nil end -- Check expiry if payload.exp and payload.exp < ngx.time() then return nil end -- Check disabled local is_disabled = payload.is_disabled if is_disabled == "1" or is_disabled == true then return nil end return payload end return _M