Search code examples
authenticationgocookiesdevops

How can I solve the issue of cookies not being transferred between localhost ports in my Go chat app?


I am having problems with cookie based token auth in my chat app. I am using a Go backend with the standard net library to add tokens to response cookies. When a user passes the password validation (by POSTing to the /login path on the auth server), the response cookies should contain an access token for generating API tokens and a refresh token for regenerating the access token.

Here is a markup file containing the structure of the apps services in my dev environment. Each server is being run on sequential ports using Go net/http on localhost (irrelevant services are not shown).


auth_server (
    dependencies []
    url (scheme "http" domain "localhost" port "8081")
    listenAddress ":8081"
    endpoints (
        /jwtkeypub (
            methods [GET]
        )
        /register (
            methods [POST]
        )
        /logout (
            methods [POST]
        )
        /login (
            methods [POST]
        )
        /apitokens (
            methods [GET]
        )
        /accesstokens (
            methods [GET]
        )
    )
    jwtInfo (
        issuerName "auth_server"
        audienceName "auth_server"
    )
)

message_server (
    dependencies [auth_server]
    url (scheme "http" domain "localhost" port "8083")
    listenAddress ":8083"
    endpoints (
        /ws (
            methods [GET]
        )
    )
    jwtInfo (
        audienceName "message_server"
    )
)

static (
    dependencies [auth_server, message_server]
    url (scheme "http" domain "localhost" port "8080")
    listenAddress ":8080"
)

This is the code that sets the cookies on login. This happens after the password check

    // Set a new refresh token
    refreshToken := s.jwtIssuer.StringifyJwt(
        s.jwtIssuer.MintToken(userId, s.jwtIssuer.Name, RefreshTokenTTL),
    )
    kit.SetHttpOnlyCookie(w, "refreshToken", refreshToken, int(RefreshTokenTTL.Seconds()))

    // set a new access token
    accessToken := s.jwtIssuer.StringifyJwt(
        s.jwtIssuer.MintToken(userId, s.jwtAudience.Name, AccessTokenTTL),
    )
    kit.SetHttpOnlyCookie(w, "accessToken", accessToken, int(AccessTokenTTL.Seconds()))
}

func SetHttpOnlyCookie(w http.ResponseWriter, name, value string, maxAge int) {
    http.SetCookie(w, &http.Cookie{
        Name:     name,
        Value:    value,
        HttpOnly: true,
        MaxAge:   maxAge,
    })
}

And here is how I am accessing the cookie when the user requests a API token. The handler calls the GetTokenFromCookie() function and responds with a 401 if an error is returned. The error is this case is "http: named cookie not present"


func GetHttpCookie(r *http.Request, name string) (*http.Cookie, error) {
    return r.Cookie(name)
}

func GetTokenFromCookie(r *http.Request, name string) (jwt.Jwt, error) {
    tokenCookie, err := GetHttpCookie(r, name)
    if err != nil {
        // DEBUG
        log.Println(err)
        return jwt.Jwt{}, err
    }

    return jwt.FromString(tokenCookie.Value)
}

After a 200 response from the login endpoint, the page redirects to the main app page. On this page, a request is made to the auth server to receive an API token for connecting the live chat message server. As you can see from the log output on the auth server, the access token cookie was not received in the request, so the request returns a 401 code.

2023/05/19 02:33:57 GET [/jwtkeypub] - 200
2023/05/19 02:33:57 GET [/jwtkeypub] - 200
2023/05/19 02:34:23 POST [/login] - 200
2023/05/19 02:34:23 http: named cookie not present
{{ } {    } []} http: named cookie not present
2023/05/19 02:34:23 GET [/apitokens?aud=MSGSERVICE] - 401

I believe the problem lies in that I am using localhost and the browser does not transfer the cookie from locahost:8080 to localhost:8081. I was planning on implmenting some sort of mock auth that circumvents reading the cookies for the dev environment to get around this, but I am not sure if this is actually the cause of my problem. Just want to get a second look and see if I can get it working without needing to do that.

UPDATE: I have looked into the network tabs in dev tools: The images show that the response after logging in returns the cookies, but they are not subsequently sent to the auth server on port 8081. I have also looked in cookie storage after getting the 200 response from logging in, there is no cookie present even after receiving them in the response. I am using Firefox on private mode to access the site. Note that the cookie does not include MaxAge even though I set MaxAge in the Go code, this seems like a problem.

UPDATE: Here is the HAR file after logging in. You can see that the response has Max-Age, but it doesn't show up in the cookies tab afterwards.

{
  "log": {
    "version": "1.2",
    "creator": {
      "name": "Firefox",
      "version": "113.0.1"
    },
    "browser": {
      "name": "Firefox",
      "version": "113.0.1"
    },
    "pages": [
      {
        "startedDateTime": "2023-05-19T12:16:37.081-04:00",
        "id": "page_1",
        "title": "Login Page",
        "pageTimings": {
          "onContentLoad": -8105,
          "onLoad": -8077
        }
      }
    ],
    "entries": [
      {
        "pageref": "page_1",
        "startedDateTime": "2023-05-19T12:16:37.081-04:00",
        "request": {
          "bodySize": 31,
          "method": "POST",
          "url": "http://0.0.0.0:8081/login",
          "httpVersion": "HTTP/1.1",
          "headers": [
            {
              "name": "Host",
              "value": "0.0.0.0:8081"
            },
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0"
            },
            {
              "name": "Accept",
              "value": "*/*"
            },
            {
              "name": "Accept-Language",
              "value": "en-US,en;q=0.5"
            },
            {
              "name": "Accept-Encoding",
              "value": "gzip, deflate"
            },
            {
              "name": "Referer",
              "value": "http://localhost:8080/"
            },
            {
              "name": "Content-Type",
              "value": "text/plain;charset=UTF-8"
            },
            {
              "name": "Content-Length",
              "value": "31"
            },
            {
              "name": "Origin",
              "value": "http://localhost:8080"
            },
            {
              "name": "DNT",
              "value": "1"
            },
            {
              "name": "Connection",
              "value": "keep-alive"
            }
          ],
          "cookies": [],
          "queryString": [],
          "headersSize": 370,
          "postData": {
            "mimeType": "text/plain;charset=UTF-8",
            "params": [],
            "text": "{\"username\":\"a\",\"password\":\"a\"}"
          }
        },
        "response": {
          "status": 200,
          "statusText": "OK",
          "httpVersion": "HTTP/1.1",
          "headers": [
            {
              "name": "Access-Control-Allow-Origin",
              "value": "*"
            },
            {
              "name": "Set-Cookie",
              "value": "refreshToken=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJQMmY2RHg1RWxlYTF5THBUaVpEejBaS3Z1dk1FUkFPZEtBVGkwNDZSc2JNPSIsImF1ZCI6InN0ZWVsaXgiLCJpc3MiOiJzdGVlbGl4IiwiZXhwIjoiMTY4NTExNzc5NyIsImp0aSI6IjIwMUQzODZDNTRBQzlEOUMwRjdCODFBMDVDNDlFQTE1In0.SbxFgEAtZbh0zS-SXZmrVW9iLk-cFz6HcDMU0FHNl-K9BwCeb_boc5igEgImMSYK-NBVQZh1km7YknE-jkBWyF0rIYjSnTzjNUHHwMnn0jE1N-dtEfNRnF1OT0R2bxPSz8gmhtJ3B839xa-jh9uMPMkXEB8BYtABgPH1FqBdijHPUtRVKq6C3ulVleurp2eyF8EHpGLc9rr5wBYSFBk0HQ3FNjjUxfRQLDnzl2xYovoQ2em4grExnkdACxCSpXNtF5bQ7lCnEZyf7-CehrRNwZCpteGKj5ux_wrX_nxma3OEWwrlatML_j-e420TM1tub0C9Ymyt0bMugHw8vaiOGA; Max-Age=604800; HttpOnly"
            },
            {
              "name": "Set-Cookie",
              "value": "accessToken=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJQMmY2RHg1RWxlYTF5THBUaVpEejBaS3Z1dk1FUkFPZEtBVGkwNDZSc2JNPSIsImF1ZCI6InN0ZWVsaXgiLCJpc3MiOiJzdGVlbGl4IiwiZXhwIjoiMTY4NDUxNDE5NyIsImp0aSI6IjY2NjU1QjAyNTc4NkRBRTE1M0VDNDI3MzBGMjMxQ0FGIn0.cIs6KGjRGTHaWX_uFTts_V2a3YcBb7LA0jNOBTZeyDmpPQgRlcABnuYkWUIdjUdR6VYnDitFRV-XK2ZSq6Pk_ZgyfvJ3yRzvWGYjXMu7Nq7MLpVvUh9mLKSbKvlqunW6YVamHSCAbYS8-D_pY9fpWxIcXw0qbwA2XfTdzr0Mrw7ntrkdyK7O1QqWamnEHCmpLfJ2XJlQsU0KaD8FjkL76pO3lWmrca3VYnTmjP1Oo1HEhbK3nImtrNeL2khAyb8ns8ROj2HX41IDNK1aHWPfn9J04pgH3AfBfcwhhqZkrKjTVFQAkSYzuvjKPWOfpgYmBMw3Y5nG_PDf-zlvVPrdpQ; Max-Age=1200; HttpOnly"
            },
            {
              "name": "Date",
              "value": "Fri, 19 May 2023 16:16:37 GMT"
            },
            {
              "name": "Content-Length",
              "value": "0"
            }
          ],
          "cookies": [
            {
              "name": "refreshToken",
              "value": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJQMmY2RHg1RWxlYTF5THBUaVpEejBaS3Z1dk1FUkFPZEtBVGkwNDZSc2JNPSIsImF1ZCI6InN0ZWVsaXgiLCJpc3MiOiJzdGVlbGl4IiwiZXhwIjoiMTY4NTExNzc5NyIsImp0aSI6IjIwMUQzODZDNTRBQzlEOUMwRjdCODFBMDVDNDlFQTE1In0.SbxFgEAtZbh0zS-SXZmrVW9iLk-cFz6HcDMU0FHNl-K9BwCeb_boc5igEgImMSYK-NBVQZh1km7YknE-jkBWyF0rIYjSnTzjNUHHwMnn0jE1N-dtEfNRnF1OT0R2bxPSz8gmhtJ3B839xa-jh9uMPMkXEB8BYtABgPH1FqBdijHPUtRVKq6C3ulVleurp2eyF8EHpGLc9rr5wBYSFBk0HQ3FNjjUxfRQLDnzl2xYovoQ2em4grExnkdACxCSpXNtF5bQ7lCnEZyf7-CehrRNwZCpteGKj5ux_wrX_nxma3OEWwrlatML_j-e420TM1tub0C9Ymyt0bMugHw8vaiOGA"
            },
            {
              "name": "accessToken",
              "value": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJQMmY2RHg1RWxlYTF5THBUaVpEejBaS3Z1dk1FUkFPZEtBVGkwNDZSc2JNPSIsImF1ZCI6InN0ZWVsaXgiLCJpc3MiOiJzdGVlbGl4IiwiZXhwIjoiMTY4NDUxNDE5NyIsImp0aSI6IjY2NjU1QjAyNTc4NkRBRTE1M0VDNDI3MzBGMjMxQ0FGIn0.cIs6KGjRGTHaWX_uFTts_V2a3YcBb7LA0jNOBTZeyDmpPQgRlcABnuYkWUIdjUdR6VYnDitFRV-XK2ZSq6Pk_ZgyfvJ3yRzvWGYjXMu7Nq7MLpVvUh9mLKSbKvlqunW6YVamHSCAbYS8-D_pY9fpWxIcXw0qbwA2XfTdzr0Mrw7ntrkdyK7O1QqWamnEHCmpLfJ2XJlQsU0KaD8FjkL76pO3lWmrca3VYnTmjP1Oo1HEhbK3nImtrNeL2khAyb8ns8ROj2HX41IDNK1aHWPfn9J04pgH3AfBfcwhhqZkrKjTVFQAkSYzuvjKPWOfpgYmBMw3Y5nG_PDf-zlvVPrdpQ"
            }
          ],
          "content": {
            "mimeType": "text/plain",
            "size": 0,
            "text": ""
          },
          "redirectURL": "",
          "headersSize": 1347,
          "bodySize": 1748
        },
        "cache": {},
        "timings": {
          "blocked": 0,
          "dns": 0,
          "connect": 0,
          "ssl": 0,
          "send": 0,
          "wait": 13,
          "receive": 0
        },
        "time": 13,
        "_securityState": "insecure",
        "serverIPAddress": "0.0.0.0",
        "connection": "8081"
      }
    ]
  }
}

enter image description here

The response appears to have the cookies, but they don't get saved. enter image description here

enter image description here

And the next request to the auth server doesn't have any cookies added.

enter image description here


Solution

  • TL;DR:

    1. cookies won't be shared between 0.0.0.0 and localhost.
    2. both session cookies and normal cookies can be shared between http://localhost:8080 and http://localhost:8081.
    3. a request sent from the page http://localhost:8080/ to http://localhost:8081/ is considered as a cross-origin request.
    4. cross-origin requests sent by fetch should be initialized with credentials: 'include' to cause the browser to save cookies.

    The HAR shows that the URL of the web page is http://localhost:8080/, but the login endpoint is http://0.0.0.0:8081/login. Cookies for 0.0.0.0 won't be shared with localhost.

    You can run the demo below to observe the behavior:

    1. run the demo: go run main.go;

    2. open http://localhost:8080/ in the browser. The web page will do these things:

      1. it sends a request to http://0.0.0.0:8081/login1 (the purpose is to verify that cookies for 0.0.0.0 won't be shared with localhost;
      2. it sends a request to http://localhost:8081/login2 (the purpose is to verify that session cookie will be shared between http://localhost:8080 and http://localhost:8081;
      3. it sends a request to http://localhost:8081/login3 (the purpose is to verify that normal cookie will be shared between http://localhost:8080 and http://localhost:8081;
      4. it navigates to http://localhost:8080/resource and the server will dump the request. It shows that this header is sent to the server: Cookie: login2=localhost-session; login3=localhost.

    Notes: credentials: 'include' requires that the Access-Control-Allow-Origin header be set to the exact origin (that means * will be rejected), and the Access-Control-Allow-Credentials header be set to true.

    package main
    
    import (
        "fmt"
        "log"
        "net/http"
        "net/http/httputil"
    )
    
    func setHeader(w http.ResponseWriter, cookieName, cookieValue string, maxAge int) {
        w.Header().Set("Access-Control-Allow-Origin", "http://localhost:8080")
        w.Header().Set("Access-Control-Allow-Credentials", "true")
        http.SetCookie(w, &http.Cookie{
            Name:     cookieName,
            Value:    cookieValue,
            MaxAge:   maxAge,
            HttpOnly: true,
        })
    }
    
    func main() {
        muxWeb := http.NewServeMux()
        // serve the HTML page.
        muxWeb.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            _, err := w.Write([]byte(page))
            if err != nil {
                panic(err)
            }
        }))
        // Dump the request to see what cookies is sent to the server.
        muxWeb.Handle("/resource", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            dump, err := httputil.DumpRequest(r, false)
            if err != nil {
                panic(err)
            }
            _, _ = w.Write(dump)
        }))
        web := &http.Server{
            Addr:    ":8080",
            Handler: muxWeb,
        }
        go func() {
            log.Fatal(web.ListenAndServe())
        }()
    
        muxAPI := http.NewServeMux()
        muxAPI.Handle("/login1", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            setHeader(w, "login1", "0.0.0.0", 1200)
        }))
        muxAPI.Handle("/login2", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            setHeader(w, "login2", "localhost-session", 0)
        }))
        muxAPI.Handle("/login3", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            setHeader(w, "login3", "localhost", 1200)
        }))
        api := &http.Server{
            Addr:    ":8081",
            Handler: muxAPI,
        }
        go func() {
            log.Fatal(api.ListenAndServe())
        }()
    
        fmt.Println("Open http://localhost:8080/ in the browser")
    
        select {}
    }
    
    var page string = `
    <!DOCTYPE html>
    <html>
      <body>
        <script type="module">
          async function login(url) {
            const response = await fetch(url, {
              mode: 'cors',
              credentials: 'include',
            });
          }
          await login('http://0.0.0.0:8081/login1');
          await login('http://localhost:8081/login2');
          await login('http://localhost:8081/login3');
    
          window.location = '/resource';
        </script>
      </body>
    </html>
    `