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
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
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.
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
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:
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,
})
}
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