Skip to content

Commit 0307787

Browse files
authored
Merge pull request #37 from jcmoraisjr/jm-auth-intercept
Add auth-intercept action
2 parents 085d700 + d1c9239 commit 0307787

8 files changed

Lines changed: 444 additions & 8 deletions

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,13 @@ loosely based on the [ngx_http_auth_request_module] module for nginx.
3939
4040
# *snip*
4141
42+
# auth-request syntax:
4243
# Backend name Path to request
4344
http-request lua.auth-request auth_request /is-allowed
45+
46+
# auth-intercept syntax: (Headers to copy)
47+
# Backend name Path Method Request Success Failure
48+
http-request lua.auth-intercept auth_request /is-allowed HEAD * - -
4449
```
4550
4651
4. Act on the results:
@@ -51,6 +56,51 @@ loosely based on the [ngx_http_auth_request_module] module for nginx.
5156
http-request deny if ! { var(txn.auth_response_successful) -m bool }
5257
```
5358
59+
### Parameters
60+
61+
The scripts receive a list of parameters used to build the authentication
62+
request:
63+
64+
* **Backend name**: is the name of an HAProxy backend. See the
65+
[Inner Workings](#inner-workings) section.
66+
* **Path to request**: the request URL sent to the auth-request backend.
67+
68+
The following parameters are only available in the `auth-intercept` script:
69+
70+
* **Method**: the HTTP method that should be used. Use an asterisk `*` to ask
71+
`auth-intercept` to copy the same method used by the client. `auth-request`
72+
uses the `HEAD` method.
73+
* **Headers to copy on Request**: a comma-separated list of a simplified glob
74+
pattern that should match the HTTP header names to copy from the client to the
75+
auth-intercept backend. Use a dash `-` to not copy any header.
76+
* **Headers to copy on Success**: a comma-separated list of a simplified glob
77+
pattern that should match the HTTP header names to copy from the auth-intercept
78+
backend to the protected backend server, if the auth-intercept backend respond
79+
with 2xx response code and the request succeed. All headers received from the
80+
auth-intercept will override headers with the same name provided by the client.
81+
Use `*` to copy all headers, or use a dash `-` to not copy any header. HAProxy
82+
variables are always created, see the [Available Variables](#available-variables)
83+
section.
84+
* **Headers to copy on Failure**: a comma-separated list of a simplified glob
85+
pattern that should match the HTTP header names to copy from the auth-intercept
86+
backend to the client, if the request failed. `auth-intercept` will use the
87+
same HTTP method and body sent by the auth-intercept backend to respond to the
88+
client, closing the transaction. The protected backend server will not be used.
89+
Use `*` to copy all headers. Use a dash `-` to not close the transaction and
90+
leave to the HAProxy configuration the task to deny the request based on the
91+
`txn.auth_response_successful` variable. HAProxy variables are always created,
92+
see the [Available Variables](#available-variables) section.
93+
94+
Simplified glob pattern: use an asterisk `*` to match any sequence of
95+
characters and `?` to match a single char. `*` will match any header name.
96+
`x-*` will match all header names started with `x-`. `x-????` will match
97+
`x-user` but will not match neither `x-token` nor `x-id`.
98+
99+
HAProxy 2.1 or older: the On Failure param (the last one) will close the
100+
transaction and respond to the client if the value is not a dash `-`, however
101+
this feature is only supported on HAProxy 2.2 or newer. The only supported
102+
option on 2.1 and older is a dash `-`.
103+
54104
### Available Variables
55105
56106
auth-request uses HAProxy variables to communicate the results back to you. The

auth-request.lua

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,37 @@
2424

2525
local http = require("haproxy-lua-http")
2626

27+
core.register_action("auth-request", { "http-req" }, function(txn, be, path)
28+
auth_request(txn, be, path, "HEAD", ".*", "-", "-")
29+
end, 2)
30+
31+
core.register_action("auth-intercept", { "http-req" }, function(txn, be, path, method, hdr_req, hdr_succeed, hdr_fail)
32+
hdr_req = globToLuaPattern(hdr_req)
33+
hdr_succeed = globToLuaPattern(hdr_succeed)
34+
hdr_fail = globToLuaPattern(hdr_fail)
35+
auth_request(txn, be, path, method, hdr_req, hdr_succeed, hdr_fail)
36+
end, 6)
37+
38+
function globToLuaPattern(glob)
39+
if glob == "-" then
40+
return "-"
41+
end
42+
-- magic chars: '^', '$', '(', ')', '%', '.', '[', ']', '*', '+', '-', '?'
43+
-- https://www.lua.org/manual/5.4/manual.html#6.4.1
44+
--
45+
-- this chain is:
46+
-- 1. escaping all the magic chars, adding a `%` in front of all of them,
47+
-- except the chars being processed later in the chain;
48+
-- 1.1. all the chars inside the [set] are magic chars and have special
49+
-- meaning inside a set, so we're also escaping all of them to avoid
50+
-- misbehavior;
51+
-- 2. converting "match all" `*` and "match one" `?` to their Lua pattern
52+
-- counterparts;
53+
-- 3. adding start and finish boundaries outside the whole string and,
54+
-- being a comma-separated list, between every single item as well.
55+
return "^" .. glob:gsub("[%^%$%(%)%%%.%[%]%+%-]", "%%%1"):gsub("*", ".*"):gsub("?", "."):gsub(",", "$,^") .. "$"
56+
end
57+
2758
function set_var_pre_2_2(txn, var, value)
2859
return txn:set_var(var, value)
2960
end
@@ -46,8 +77,49 @@ function sanitize_header_for_variable(header)
4677
return header:gsub("[^a-zA-Z0-9]", "_")
4778
end
4879

80+
-- header_match checks whether the provided header matches the pattern.
81+
-- pattern is a comma-separated list of Lua Patterns.
82+
function header_match(header, pattern)
83+
if header == "content-length" or header == "host" or pattern == "-" then
84+
return false
85+
end
86+
for p in pattern:gmatch("[^,]*") do
87+
if header:match(p) then
88+
return true
89+
end
90+
end
91+
return false
92+
end
4993

50-
core.register_action("auth-request", { "http-req" }, function(txn, be, path)
94+
-- Terminates the transaction and sends the provided response to the client.
95+
-- hdr_fail filters header names that should be provided using Lua Patterns.
96+
function send_response(txn, response, hdr_fail)
97+
local reply = txn:reply()
98+
if response then
99+
reply:set_status(response.status_code)
100+
for header, value in response:get_headers(true) do
101+
if header_match(header, hdr_fail) then
102+
reply:add_header(header, value)
103+
end
104+
end
105+
if response.content then
106+
reply:set_body(response.content)
107+
end
108+
else
109+
reply:set_status(500)
110+
end
111+
txn:done(reply)
112+
end
113+
114+
-- auth_request makes the request to the external authentication service
115+
-- and waits for the response. hdr_* params receive a comma-separated
116+
-- list of Lua Patterns used to identify the headers that should be
117+
-- copied between the requests and responses. A dash `-` in these params
118+
-- mean that the headers shouldn't be copied at all.
119+
-- Special values and behavior:
120+
-- * method == "*": call the auth service using the same method used by the client.
121+
-- * hdr_fail == "-": make the Lua script to not terminate the request.
122+
function auth_request(txn, be, path, method, hdr_req, hdr_succeed, hdr_fail)
51123
set_var(txn, "txn.auth_response_successful", false)
52124

53125
-- Check whether the given backend exists.
@@ -77,7 +149,7 @@ core.register_action("auth-request", { "http-req" }, function(txn, be, path)
77149
-- socket.http's format.
78150
local headers = {}
79151
for header, values in pairs(txn.http:req_get_headers()) do
80-
if header ~= 'content-length' then
152+
if header_match(header, hdr_req) then
81153
for i, v in pairs(values) do
82154
if headers[header] == nil then
83155
headers[header] = v
@@ -89,33 +161,51 @@ core.register_action("auth-request", { "http-req" }, function(txn, be, path)
89161
end
90162

91163
-- Make request to backend.
92-
local response, err = http.head {
164+
if method == "*" then
165+
method = txn.sf:method()
166+
end
167+
local response, err = http.send(method:upper(), {
93168
url = "http://" .. addr .. path,
94169
headers = headers,
95-
}
170+
})
171+
172+
-- `terminate_on_failure == true` means that the Lua script should send the response
173+
-- and terminate the transaction in the case of a failure. This will happen when
174+
-- hdr_fail content isn't a dash `-`.
175+
local terminate_on_failure = hdr_fail ~= "-"
96176

97177
-- Check whether we received a valid HTTP response.
98178
if response == nil then
99179
txn:Warning("Failure in auth-request backend '" .. be .. "': " .. err)
100180
set_var(txn, "txn.auth_response_code", 500)
181+
if terminate_on_failure then
182+
send_response(txn)
183+
end
101184
return
102185
end
103186

104187
set_var(txn, "txn.auth_response_code", response.status_code)
188+
local response_ok = 200 <= response.status_code and response.status_code < 300
105189

106190
for header, value in response:get_headers(true) do
107191
set_var(txn, "req.auth_response_header." .. sanitize_header_for_variable(header), value)
192+
if response_ok and hdr_succeed ~= "-" and header_match(header, hdr_succeed) then
193+
txn.http:req_set_header(header, value)
194+
end
108195
end
109196

110-
-- 2xx: Allow request.
111-
if 200 <= response.status_code and response.status_code < 300 then
197+
-- response_ok means 2xx: allow request.
198+
if response_ok then
112199
set_var(txn, "txn.auth_response_successful", true)
113-
-- Don't allow other codes.
200+
-- Don't allow codes < 200 or >= 300.
201+
-- Forward the response to the client if required.
202+
elseif terminate_on_failure then
203+
send_response(txn, response, hdr_fail)
114204
-- Codes with Location: Passthrough location at redirect.
115205
elseif response.status_code == 301 or response.status_code == 302 or response.status_code == 303 or response.status_code == 307 or response.status_code == 308 then
116206
set_var(txn, "txn.auth_response_location", response:get_header("location", "last"))
117207
-- 401 / 403: Do nothing, everything else: log.
118208
elseif response.status_code ~= 401 and response.status_code ~= 403 then
119209
txn:Warning("Invalid status code in auth-request backend '" .. be .. "': " .. response.status_code)
120210
end
121-
end, 2)
211+
end

test/dynamic_method.vtc

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# SPDX-License-Identifier: MIT
2+
3+
varnishtest "Verify that auth-request backend receives the client method."
4+
feature ignore_unknown_macro
5+
6+
server s1 {
7+
rxreq
8+
txresp
9+
} -repeat 3 -start
10+
11+
server s_auth_backend {
12+
rxreq
13+
expect req.method == "POST"
14+
txresp
15+
16+
accept
17+
rxreq
18+
expect req.method == "GET"
19+
txresp
20+
21+
accept
22+
rxreq
23+
expect req.method == "HEAD"
24+
txresp
25+
} -start
26+
27+
haproxy h1 -conf {
28+
global
29+
lua-load ${testdir}/../auth-request.lua
30+
31+
listen fe1
32+
mode http
33+
bind "fd@${fe1}"
34+
http-request lua.auth-intercept auth_backend / * * - -
35+
http-request deny if ! { var(txn.auth_response_successful) -m bool }
36+
server s1 ${s1_addr}:${s1_port}
37+
38+
backend auth_backend
39+
mode http
40+
server auth_backend ${s_auth_backend_addr}:${s_auth_backend_port}
41+
} -start
42+
43+
client c1 -connect ${h1_fe1_sock} {
44+
txreq -method "POST"
45+
rxresp
46+
txreq -method "GET"
47+
rxresp
48+
txreq -method "HEAD"
49+
rxresp
50+
} -run

test/headers_complete.vtc

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# SPDX-License-Identifier: MIT
2+
3+
varnishtest "Verify that filtered request headers are passed to the auth-intercept backend."
4+
feature ignore_unknown_macro
5+
feature cmd "dpkg --compare-versions ${haproxy_version} ge 2.2"
6+
7+
server s1 {
8+
rxreq
9+
expect req.http.x-user == "logan"
10+
expect req.http.x-app-data == "secret"
11+
expect req.http.token == "bearer value"
12+
txresp \
13+
-status 201 \
14+
-hdr "x-field: value"
15+
} -start
16+
17+
server s_auth_backend {
18+
rxreq
19+
expect req.http.x-app-data == "<undef>"
20+
expect req.http.token == "bearer value"
21+
txresp \
22+
-status 401 \
23+
-hdr "x-reason: invalid pwd"
24+
25+
accept
26+
rxreq
27+
expect req.http.x-app-data == "<undef>"
28+
expect req.http.token == "bearer value"
29+
txresp \
30+
-status 200 \
31+
-hdr "x-user: logan"
32+
} -start
33+
34+
haproxy h1 -conf {
35+
global
36+
lua-load ${testdir}/../auth-request.lua
37+
38+
listen fe1
39+
mode http
40+
bind "fd@${fe1}"
41+
http-request lua.auth-intercept auth_backend / HEAD token x-user x-reason
42+
server s1 ${s1_addr}:${s1_port}
43+
44+
backend auth_backend
45+
mode http
46+
server auth_backend ${s_auth_backend_addr}:${s_auth_backend_port}
47+
} -start
48+
49+
client c1 -connect ${h1_fe1_sock} {
50+
txreq \
51+
-hdr "x-user: try-to-override" \
52+
-hdr "x-app-data: secret" \
53+
-hdr "token: bearer value"
54+
rxresp
55+
expect resp.status == 401
56+
expect resp.http.x-reason == "invalid pwd"
57+
expect resp.http.x-field == "<undef>"
58+
59+
txreq \
60+
-hdr "x-user: try-to-override" \
61+
-hdr "x-app-data: secret" \
62+
-hdr "token: bearer value"
63+
rxresp
64+
expect resp.status == 201
65+
expect resp.http.x-reason == "<undef>"
66+
expect resp.http.x-field == "value"
67+
} -run

test/headers_fail.vtc

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# SPDX-License-Identifier: MIT
2+
3+
varnishtest "Verify that filtered auth backend response headers are passed to the client."
4+
feature ignore_unknown_macro
5+
feature cmd "dpkg --compare-versions ${haproxy_version} ge 2.2"
6+
7+
server s_auth_backend {
8+
rxreq
9+
txresp \
10+
-status 401 \
11+
-hdr "x-user: admin" \
12+
-hdr "x-passwd: 123" \
13+
-hdr "x-reason: invalid pwd" \
14+
-hdr "token: asd" \
15+
-hdr "other: value" \
16+
-body "{\"msg\":\"invalid pwd\"}"
17+
} -start
18+
19+
haproxy h1 -conf {
20+
global
21+
lua-load ${testdir}/../auth-request.lua
22+
23+
listen fe1
24+
mode http
25+
bind "fd@${fe1}"
26+
http-request lua.auth-intercept auth_backend / * * - x-user,x-reason
27+
28+
backend auth_backend
29+
mode http
30+
server auth_backend ${s_auth_backend_addr}:${s_auth_backend_port}
31+
} -start
32+
33+
client c1 -connect ${h1_fe1_sock} {
34+
txreq
35+
rxresp
36+
expect resp.status == 401
37+
expect resp.http.x-user == "admin"
38+
expect resp.http.x-passwd == "<undef>"
39+
expect resp.http.x-reason == "invalid pwd"
40+
expect resp.http.token == "<undef>"
41+
expect resp.http.other == "<undef>"
42+
expect resp.body == "{\"msg\":\"invalid pwd\"}"
43+
} -run

0 commit comments

Comments
 (0)