Per-host HTTP basic auth (#57)
Adds opt-in basic auth per Host, following the existing per-host controls
pattern:
- Host fields basicauth_enabled / basicauth_realm / basicauth_users
({user: base64(sha1(pw))}). Credentials are parsed to plaintext by the pure
host_features normalizer and hashed at the route layer (utils/basicauth.js),
so plaintext never reaches Redis.
- ops/nginx_conf/hostfeatures.lua enforces it in access phase: verifies the
Authorization header against base64(sha1(password)), fails closed with a 401
WWW-Authenticate challenge.
- hosts.ejs gains an enable toggle, realm, and a username:password textarea
(passwords never echoed back; blank keeps the current set).
Unit tests cover hashing (matches the htpasswd {SHA} vector), credential
parsing, and normalization. Note: the Lua path needs verification on a live
OpenResty box.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,8 @@
|
||||
-- req_headers (JSON object) -- added to the upstream request
|
||||
-- resp_headers (JSON object) -- added to the client response
|
||||
-- ip_allow / ip_deny (JSON arrays of CIDRs)
|
||||
-- basicauth_enabled / basicauth_realm
|
||||
-- basicauth_users (JSON object {username: base64(sha1(password))})
|
||||
|
||||
local cjson = require "cjson.safe"
|
||||
|
||||
@@ -80,6 +82,47 @@ local function apply_ratelimit(res, host, ip)
|
||||
end
|
||||
end
|
||||
|
||||
-- Per-host HTTP basic auth. Credentials are stored as {user: base64(sha1(pw))}
|
||||
-- (htpasswd "{SHA}" scheme; hashed server-side in nodejs). Fails closed: any
|
||||
-- misconfig or bad credential returns 401 with a WWW-Authenticate challenge.
|
||||
local function apply_basicauth(res)
|
||||
if res["basicauth_enabled"] ~= "true" then return end
|
||||
|
||||
local realm = res["basicauth_realm"]
|
||||
if not realm or realm == "" then realm = "Restricted" end
|
||||
realm = realm:gsub('[\r\n"]', "") -- defense in depth for the header
|
||||
|
||||
local function deny()
|
||||
ngx.header["WWW-Authenticate"] = 'Basic realm="' .. realm .. '"'
|
||||
return ngx.exit(401)
|
||||
end
|
||||
|
||||
local users = decode_table(res["basicauth_users"])
|
||||
if not users then return deny() end -- enabled but no users -> deny all
|
||||
|
||||
local header = ngx.var.http_authorization
|
||||
if not header then return deny() end
|
||||
local b64 = header:match("^%s*[Bb]asic%s+(%S+)%s*$")
|
||||
if not b64 then return deny() end
|
||||
|
||||
local decoded = ngx.decode_base64(b64)
|
||||
if not decoded then return deny() end
|
||||
local user, pass = decoded:match("^([^:]*):(.*)$")
|
||||
if not user or user == "" then return deny() end
|
||||
|
||||
local stored = users[user]
|
||||
if not stored then return deny() end
|
||||
|
||||
local sha1 = require "resty.sha1"
|
||||
local hasher = sha1:new()
|
||||
if not hasher then return deny() end
|
||||
hasher:update(pass or "")
|
||||
local computed = ngx.encode_base64(hasher:final())
|
||||
|
||||
if computed ~= stored then return deny() end
|
||||
-- authenticated: fall through to the rest of the request
|
||||
end
|
||||
|
||||
-- Extra request headers sent to the upstream.
|
||||
local function apply_req_headers(res)
|
||||
local headers = decode_table(res["req_headers"])
|
||||
@@ -97,6 +140,7 @@ function M.access(ngx_, res)
|
||||
|
||||
apply_ip_access(res, ip)
|
||||
apply_ratelimit(res, host, ip)
|
||||
apply_basicauth(res)
|
||||
apply_req_headers(res)
|
||||
|
||||
-- Cache gate for proxy_no_cache / proxy_cache_bypass. Opt-in per host.
|
||||
|
||||
Reference in New Issue
Block a user