reactjscookiesjwtgo-fiber

Why is my cookie not being sent? ReactJS front-end, Go back-end


I am working on a personal finance application with a Go back-end (go-fiber framework) and ReactJS front-end.

My authentication method is to return a JWT as a cookie when a user signs in.

The front end sends a sign-in request using fetch, then follows up with another fetch to acquire user data. The fetch calls, as well as the server handler functions, can be found in the Appendix included at the end of this question.

When I test this out, I get a successful sign-in. A Set-Cookie header is returned and I see the cookie in the Response as I would expect it. However, the JWT is not being included as a header in the Request for user data. The handler returns {"status": "unauthorized"} as the parsed JWT is nil.

Why is the JWT not being included in the Request for user data?

Here is the Set-Cookie header, and a screenshot of all the Sign-In Response headers. jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDY1MTczOTMsImlzcyI6IjE3In0.SDKnxjsVImuVOHw_hnsPX1ZhtS7-_6s8Cqk79SwniCY; expires=Sat, 05 Mar 2022 21:56:33 GMT; path=/; HttpOnly; SameSite=Lax

Sign-In Response Headers

Here is the JWT cookie being returned upon sign-in, and a screenshot of the cookie from Chrome Developer Tools. jwt eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDY1MTczOTMsImlzcyI6IjE3In0.SDKnxjsVImuVOHw_hnsPX1ZhtS7-_6s8Cqk79SwniCY localhost / 2022-03-05T21:56:33.000Z 195 ✓ Lax Medium

Sign-In Response Cookie

I do not see anything in the "Cookies" section of the Application tab. However, I read somewhere else that I should not expect to see any cookies with httpOnly set to true here.

Application Cookies

I am expecting to see a header called "Cookies" in the user data Request. But I am only seeing these:

Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Host: localhost:9000
Origin: http://localhost:3000
Referer: http://localhost:3000/
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="99", "Google Chrome";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36

User Data Request Headers

Would greatly appreciate any nudges or help. Here are links to the GitHub pages for the front-end and back-end code in case it helps explain the problem:

ReactJS Front-End

Go-Fiber Back-End

APPENDIX

Sign-In Request fetch:

        fetch('http://localhost:9000/signIn', requestOptions)
            .then(res => {
                if (res.status === 200) {
                    res.json()
                        .then(
                            (result) => {
                                if (result.status && result.status === "success") {
                                    this.props.onRouteChange('home');
                                } else {
                                    // TO DO: display failure message on UI
                                    console.log('Failed to sign in');
                                }
                            },
                            (error) => {
                                this.setState({
                                    isLoaded: true,
                                    error
                                });
                            }
                        );
                } else {
                    console.log('Error signing in');
                    res.json()
                        .then(
                            (result) => {
                                console.log(result);
                            },
                            (error) => {
                                console.log('Error reading JSON of response with status !== 200');
                                console.log(error);
                            }
                        );
                }
            });

Sign-In Handler Function:

func handleSignIn(c *fiber.Ctx) error {
    // unmarshal received sign in data into User struct
    var signIn User
    if err := c.BodyParser(&signIn); err != nil {
        err = fmt.Errorf("failed to process HTTP request body to /signIn: %w", err)
        log.Println("Error:", err)
        c.Status(fiber.StatusBadRequest)
        return err
    }

    // look for the identified user in the users database
    usr, err := getUserByUsername(signIn.Username)
    if err != nil && err == sql.ErrNoRows {
        log.Println("Error: user", signIn.Username, "attempted to sign in but not found in users database")
        c.Status(fiber.StatusBadRequest)
        return fmt.Errorf("invalid username/password combination")
    }
    if err != nil {
        err = fmt.Errorf("failed to query database for user %s: %w", signIn.Username, err)
        log.Println("Error:", err)
        c.Status(fiber.StatusInternalServerError)
        return err
    }

    // hash the given password for comparison with the recorded password
    err = bcrypt.CompareHashAndPassword([]byte(usr.Password), []byte(signIn.Password))
    if err != nil {
        log.Println("CompareHashAndPassword returned error during sign in attempt:", err)
        c.Status(fiber.StatusBadRequest)
        return fmt.Errorf("invalid username/password combination")
    }

    // declare claims for the JWT that will be sent back
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
        Issuer:    strconv.Itoa(int(usr.Id)),
        ExpiresAt: time.Now().Add(time.Hour * 24).Unix(), // 1 day
    })
    if token == nil {
        err = fmt.Errorf("failed to instantiate JWT")
        log.Println("Error:", err)
        c.Status(fiber.StatusInternalServerError)
        return err
    }

    // encrypt the JWT with the private key
    tokenString, err := token.SignedString([]byte(jwtPrivateKey))
    if err != nil {
        err = fmt.Errorf("failed to encrypt JWT: %w", err)
        log.Println("Error:", err)
        c.Status(fiber.StatusInternalServerError)
        return err
    }

    c.Cookie(&fiber.Cookie{
        Name:     "jwt",
        Value:    tokenString,
        Expires:  time.Now().Add(time.Hour * 24),
        HTTPOnly: true,
    })

    // send response
    return c.JSON(fiber.Map{
        "status": "success",
    })
}

User Data Request Fetch:

    componentDidMount() {
        fetch("http://localhost:9000/getExpenses")
            .then(res => res.json())
            .then(
                (result) => {
                    if (result.status !== null && result.status === "unauthorized") {
                        console.log('Failed authorization when requesting expenses!');
                    } else if (result.expenses === null) {
                        console.log('Response did not contain expenses map');
                    } else {
                        this.setState({
                            isLoaded: true,
                            expenses: result.expenses
                        });
                    }
                },
                (error) => {
                    this.setState({
                        isLoaded: true,
                        error
                    });
                }
            );
    }

User Data Request Handler:

func handleGetExpenses(c *fiber.Ctx) error {
    // parse JWT from HTTP cookie
    token, err := parseCookie(c)
    if err != nil {
        c.Status(fiber.StatusUnauthorized)
        return c.JSON(fiber.Map{
            "status": "unauthorized",
        })
    }

    // check which user is getting their expenses
    claims := token.Claims.(*jwt.StandardClaims)
    userId, err := strconv.ParseInt(claims.Issuer, 10, 64)
    if err != nil {
        err = fmt.Errorf("invalid Issuer field in JWT")
        log.Println("Error:", err)
        c.Status(fiber.StatusUnauthorized)
        return err
    }

    // get all expenses from the database
    expenses, err := getAllExpenses(userId)
    if err != nil {
        err = fmt.Errorf("failed to get expenses from expense table: %w", err)
        log.Println("Error:", err)
        c.Status(fiber.StatusInternalServerError)
        return err
    }

    // send response
    return c.JSON(fiber.Map{
        "expenses": expenses,
    })
}

Solution

  • By default, fetch doesn't use cookies. You can make fetch use cookies like this:

    fetch(url, {
      credentials: "same-origin",
    }).then(responseHandler).catch(errorHandler);
    

    You can check the docs for more details: https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters