Similar to the issue mentioned here https://forum.openresty.us/d/6503-get-content-of-second-set-cookie-header
I have an NGINX configuration that gets the cookies stored in Set-Cookie by the upstream auth_request and I need to return those set-cookie to the client, however whenever I try to return those cookies only the first set-cookie is returned to the client.
Below is an example configuration to demonstrate the issue
location /auth/ {
proxy_pass http://auth/;
proxy_pass_request_body off;
proxy_redirect off;
}
location / {
auth_request /auth/loggedin;
auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie;
proxy_set_header Cookie "$http_cookie; $auth_cookie";
proxy_pass http://someservice/;
}
In my above example I expect that multiple cookies could be returned in a Set-Cookie header a=12; PATH:"/", b=2; PATH:/"
and I want to pass whatever set-cookies come from the upstream service to clients browser via add_header. Currently only the cookie a
is making it to the client and b
is always missing.
Note: I want it to be generic and so I can't grab the exact cookie names from a header.
Thank you for any help you can provide!
CodePudding user response:
Unfortunately it is impossible to do it the way you want to. Setting cookies using multiple Set-Cookie
header is a common approach, the MDN documentation on Set-Cookie
header explicitly says that:
To send multiple cookies, multiple
Set-Cookie
headers should be sent in the same response.
When multiple headers with the same name are received from the upstream, only the first one is accessible using $upstream_http_<header_name>
variable (with a few exceptions, e.g. Cache-Control
one, if I remember correctly). There is a ticket for that on nginx bug tracker (although I did't consider it a bug). Set-Cookie
header is really a special case that can't be folded in opposite to many other headers, check this answer and comments below it to see why is it so. (Of course, you are still free to use any $upstream_cookie_<cookie_name>
per-cookie variable).
However it is possible to do it using OpenResty (mentioned in your question) or lua-nginx-module
. The bad news, it will be incompatible with the auth_request
directive since it is impossible to add those lua_...
handlers to the auth location (or any other subrequest location that can be used, e.g., by add_before_body
or add_after_body
directives from the ngx_http_addition_module
). You didn't get an error, but those handlers won't be fired on a subrequest. The good news, functionality similar to auth_request
can be implemented using ngx.location.capture
API call.
So if using nginx-lua-module
is suitable for you (and if it isn't, maybe the solution will help some others), this can be done the following way:
location /auth/ {
internal;
proxy_pass http://auth/;
proxy_redirect off;
}
location / {
# -- this one is taken from the official lua-nginx-module documentation example
# -- see https://github.com/openresty/lua-nginx-module#access_by_lua
access_by_lua_block {
local res = ngx.location.capture("/auth/loggedin", {body = ""})
if res.status == ngx.HTTP_OK then
ngx.ctx.auth_cookies = res.header["Set-Cookie"]
return
end
if res.status == ngx.HTTP_FORBIDDEN then
ngx.exit(res.status)
end
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
}
# -- it doesn't matter where the 'proxy_pass' line actually would be, it doesn't change the workflow
# -- see https://cloud.githubusercontent.com/assets/2137369/15272097/77d1c09e-1a37-11e6-97ef-d9767035fc3e.png
proxy_pass http://someservice/;
header_filter_by_lua_block {
local function merge_cookies(a, b)
local c = a or b
-- if either "a" or "b" is empty, "c" already has the required result
if (a and b) == nil then return c end
-- neither "a" nor "b" are empty, result will be a table, "c" now equals to "a"
-- if "c" is a string, lets made it a table instead
if type(c) == "string" then c = {c} end
-- append "b" to "c"
if type(b) == "string" then table.insert(c, b) else
for _, v in ipairs(b) do table.insert(c, v) end
end
return c
end
ngx.header["Set-Cookie"] = merge_cookies(ngx.header["Set-Cookie"], ngx.ctx.auth_cookies)
}
}
This code does not check possible cookie names intersection, or it will be much more complex. However I think (not checked this on practice) that it doesn't really matter because even if a two Set-Cookie
requests with the same cookie name but different values will be returned to the client, the last one will be used. This code makes Set-Cookie
requests from the auth server arriving after the Set-Cookie
requests from the main upstream. To do the opposite you'd need to change the
ngx.header["Set-Cookie"] = merge_cookies(ngx.header["Set-Cookie"], ngx.ctx.auth_cookies)
line to the
ngx.header["Set-Cookie"] = merge_cookies(ngx.ctx.auth_cookies, ngx.header["Set-Cookie"])
Special thanks to the @wilsonzlin for the extremely helpful answer on working with the ngx.header["Set-Cookie"]
table.
Update
Though you didn't mention it in your question, looking at your example I saw you not only want to pass the Set-Cookie
headers to the client but also to send those cookies to the someservice
upstream. Again, you are trying to do it a wrong way. Using proxy_set_header Cookie "$http_cookie; $auth_cookie";
you are appending those cookies including their attributes like Path
, Max-Age
, etc. while the Cookie
header should contain only the name=value
pairs. Well, using the lua-nginx-module
this is also posible. You'd need to change the above access_by_lua_block
to the following one.
access_by_lua_block {
local res = ngx.location.capture("/auth/loggedin", {body = ""})
if res.status == ngx.HTTP_OK then
ngx.ctx.auth_cookies = res.header["Set-Cookie"]
if ngx.ctx.auth_cookies then
-- helper functions
-- strip all Set-Cookie attributes, e.g. "Name=value; Path=/; Max-Age=2592000" => "Name=value"
local function strip_attributes(cookie)
return string.match(cookie, "[^;] ")
end
-- iterator for use in "for in" loop, works both with strings and tables
local function iterate_cookies(cookies)
local i = 0
return function()
i = i 1
if type(cookies) == "string" then
if i == 1 then return strip_attributes(cookies) end
elseif type(cookies) == "table" then
if cookies[i] then return strip_attributes(cookies[i]) end
end
end
end
local cookies = ngx.req.get_headers()["Cookie"]
-- at the first loop iteration separator should be an empty string if client browser send no cookies or "; " otherwise
local separator = cookies and "; " or ""
-- if there are no cookies in original request, make "cookies" variable an empty string instead of nil to prevent errors
cookies = cookies or ""
for cookie in iterate_cookies(ngx.ctx.auth_cookies) do
cookies = cookies .. separator .. cookie
-- next separator definitely should be a "; "
separator = "; "
end
ngx.req.set_header("Cookie", cookies)
end
return
end
if res.status == ngx.HTTP_FORBIDDEN then
ngx.exit(res.status)
end
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
}
All the above examples are tested (using OpenResty 1.17.8.2) and confirmed to be workable.