Home > database >  How can I get the content of all cookies in "Set-Cookie" header returned by upstream in ng
How can I get the content of all cookies in "Set-Cookie" header returned by upstream in ng

Time:11-08

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.

  • Related