Skip to content

Commit

Permalink
fix(hmac): add missing www-authenticate headers
Browse files Browse the repository at this point in the history
When serve returns 401 Unauthorized response it should
return WWW-Authenticate header as well with proper challenge.
HMAC auth was missing this header.

Fix: #7772
KAG-321
  • Loading branch information
nowNick committed Oct 19, 2023
1 parent 30d90f3 commit c35d2da
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 10 deletions.
3 changes: 3 additions & 0 deletions changelog/unreleased/kong/hmac_www_authenticate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
message: Add WWW-Authenticate headers to 401 response in hmac auth plugin.
type: bugfix
scope: Plugin
23 changes: 13 additions & 10 deletions kong/plugins/hmac-auth/access.lua
Original file line number Diff line number Diff line change
Expand Up @@ -276,24 +276,27 @@ local function set_consumer(consumer, credential)
end
end

local function unauthorized(message)
return { status = 401, message = message, headers = { ["WWW-Authenticate"] = 'hmac' } }
end


local function do_authentication(conf)
local authorization = kong_request.get_header(AUTHORIZATION)
local proxy_authorization = kong_request.get_header(PROXY_AUTHORIZATION)

-- If both headers are missing, return 401
if not (authorization or proxy_authorization) then
return false, { status = 401, message = "Unauthorized" }
return false, unauthorized("Unauthorized")
end

-- validate clock skew
if not (validate_clock_skew(X_DATE, conf.clock_skew) or
validate_clock_skew(DATE, conf.clock_skew)) then
return false, {
status = 401,
message = "HMAC signature cannot be verified, a valid date or " ..
"x-date header is required for HMAC Authentication"
}
return false, unauthorized(
"HMAC signature cannot be verified, a valid date or " ..
"x-date header is required for HMAC Authentication"
)
end

-- retrieve hmac parameter from Proxy-Authorization header
Expand All @@ -313,26 +316,26 @@ local function do_authentication(conf)
local ok, err = validate_params(hmac_params, conf)
if not ok then
kong.log.debug(err)
return false, { status = 401, message = SIGNATURE_NOT_VALID }
return false, unauthorized(SIGNATURE_NOT_VALID)
end

-- validate signature
local credential = load_credential(hmac_params.username)
if not credential then
kong.log.debug("failed to retrieve credential for ", hmac_params.username)
return false, { status = 401, message = SIGNATURE_NOT_VALID }
return false, unauthorized(SIGNATURE_NOT_VALID)
end

hmac_params.secret = credential.secret

if not validate_signature(hmac_params) then
return false, { status = 401, message = SIGNATURE_NOT_SAME }
return false, unauthorized(SIGNATURE_NOT_SAME)
end

-- If request body validation is enabled, then verify digest.
if conf.validate_request_body and not validate_body() then
kong.log.debug("digest validation failed")
return false, { status = 401, message = SIGNATURE_NOT_SAME }
return false, unauthorized(SIGNATURE_NOT_SAME)
end

-- Retrieve consumer
Expand Down
16 changes: 16 additions & 0 deletions spec/03-plugins/19-hmac-auth/03-access_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ local openssl_hmac = require "resty.openssl.hmac"
local helpers = require "spec.helpers"
local utils = require "kong.tools.utils"
local resty_sha256 = require "resty.sha256"
local meta = require "kong.meta"

local fmt = string.format

Expand Down Expand Up @@ -188,6 +189,7 @@ for _, strategy in helpers.each_strategy() do
local body = assert.res_status(401, res)
body = cjson.decode(body)
assert.equal("Unauthorized", body.message)
assert.equal('hmac', res.headers["WWW-Authenticate"])
end)

it("rejects gRPC call without credentials", function()
Expand All @@ -213,6 +215,7 @@ for _, strategy in helpers.each_strategy() do
local body = assert.res_status(401, res)
body = cjson.decode(body)
assert.equal(SIGNATURE_NOT_VALID, body.message)
assert.equal('hmac', res.headers["WWW-Authenticate"])
end)

it("should not be authorized when the HMAC signature is not properly base64 encoded", function()
Expand All @@ -230,6 +233,7 @@ for _, strategy in helpers.each_strategy() do
local body = assert.res_status(401, res)
body = cjson.decode(body)
assert.equal("HMAC signature does not match", body.message)
assert.equal('hmac', res.headers["WWW-Authenticate"])
end)

it("should not be authorized when date header is missing", function()
Expand All @@ -246,6 +250,7 @@ for _, strategy in helpers.each_strategy() do
assert.equal([[HMAC signature cannot be verified, ]]
.. [[a valid date or x-date header is]]
.. [[ required for HMAC Authentication]], body.message)
assert.equal('hmac', res.headers["WWW-Authenticate"])
end)

it("should not be authorized with signature is wrong in proxy-authorization", function()
Expand Down Expand Up @@ -312,6 +317,7 @@ for _, strategy in helpers.each_strategy() do
local body = assert.res_status(401, res)
body = cjson.decode(body)
assert.equal(SIGNATURE_NOT_VALID, body.message)
assert.equal('hmac', res.headers["WWW-Authenticate"])
end)

it("should not be authorized when passing only the username", function()
Expand All @@ -328,6 +334,7 @@ for _, strategy in helpers.each_strategy() do
local body = assert.res_status(401, res)
body = cjson.decode(body)
assert.equal(SIGNATURE_NOT_VALID, body.message)
assert.equal('hmac', res.headers["WWW-Authenticate"])
end)

it("should not be authorized when authorization header is missing", function()
Expand All @@ -343,6 +350,7 @@ for _, strategy in helpers.each_strategy() do
local body = assert.res_status(401, res)
body = cjson.decode(body)
assert.equal("Unauthorized", body.message)
assert.equal('hmac', res.headers["WWW-Authenticate"])
end)

it("should not pass with username missing", function()
Expand All @@ -364,6 +372,7 @@ for _, strategy in helpers.each_strategy() do
body = cjson.decode(body)
assert.not_nil(body.message)
assert.matches("HMAC signature cannot be verified", body.message)
assert.equal('hmac', res.headers["WWW-Authenticate"])
end)

it("should not pass with signature missing", function()
Expand All @@ -384,6 +393,7 @@ for _, strategy in helpers.each_strategy() do
body = cjson.decode(body)
assert.not_nil(body.message)
assert.matches("HMAC signature cannot be verified", body.message)
assert.equal('hmac', res.headers["WWW-Authenticate"])
end)

it("should pass with GET", function()
Expand Down Expand Up @@ -1073,6 +1083,7 @@ for _, strategy in helpers.each_strategy() do
local body = assert.res_status(401, res)
body = cjson.decode(body)
assert.equal("HMAC signature does not match", body.message)
assert.equal('hmac', res.headers["WWW-Authenticate"])
end)

it("should return 200 when body validation enabled and no body and no digest header is present", function()
Expand Down Expand Up @@ -1255,6 +1266,7 @@ for _, strategy in helpers.each_strategy() do
local body = assert.res_status(401, res)
body = cjson.decode(body)
assert.equal("HMAC signature does not match", body.message)
assert.equal('hmac', res.headers["WWW-Authenticate"])
end)

it("should not pass with POST when body validation enabled and postBody is tampered", function()
Expand Down Expand Up @@ -1282,6 +1294,7 @@ for _, strategy in helpers.each_strategy() do
local body = assert.res_status(401, res)
body = cjson.decode(body)
assert.equal("HMAC signature does not match", body.message)
assert.equal('hmac', res.headers["WWW-Authenticate"])
end)

it("should not pass with POST when body validation enabled and digest header is tampered", function()
Expand Down Expand Up @@ -1309,6 +1322,7 @@ for _, strategy in helpers.each_strategy() do
local body = assert.res_status(401, res)
body = cjson.decode(body)
assert.equal("HMAC signature does not match", body.message)
assert.equal('hmac', res.headers["WWW-Authenticate"])
end)

it("should pass with GET with request-line", function()
Expand Down Expand Up @@ -1831,6 +1845,7 @@ for _, strategy in helpers.each_strategy() do
},
})
assert.response(res).has.status(401)
assert.equal('hmac', res.headers["WWW-Authenticate"])
end)

it("fails 401, with only the second credential provided", function()
Expand All @@ -1844,6 +1859,7 @@ for _, strategy in helpers.each_strategy() do
},
})
assert.response(res).has.status(401)
assert.equal('Key realm="' .. meta._NAME .. '"', res.headers["WWW-Authenticate"])
end)

it("fails 401, with no credential provided", function()
Expand Down

0 comments on commit c35d2da

Please sign in to comment.