skip to Main Content

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!

2

Answers


  1. 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.

    Login or Signup to reply.
  2. I found no other way but to use the lua module. This should partially answer your question, it does answer the question "How do I get all the Set-Cookie headers".

    location / {
        log_by_lua_block {
            local cookies = ngx.resp.get_headers()["Set-Cookie"]
            if cookies~=nil then
                ngx.log(ngx.ERR, table.concat(cookies, ','))
            end
        }
    ...
    }
    

    If you use different directives than log_by_lua_block I can’t guarantee you’ll be able to see that particular header, with access_by_lua_block I couldn’t.
    If you want to pass the stuff to the client, maybe log_by_lua_block is a bit late in the workflow, maybe try an earlier one.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search