Initial commit - realms platform
This commit is contained in:
parent
c590ab6d18
commit
c717c3751c
234 changed files with 74103 additions and 15231 deletions
167
openresty/lua/jwt.lua
Normal file
167
openresty/lua/jwt.lua
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
-- 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue