HTTPOnly Cookie在浏览器的本地主机上未设置。

huangapple go评论85阅读模式
英文:

HTTPOnly Cookie not being set in browser localhost

问题

问题

我有一个具有登录端点的 REST API。登录端点接受用户名和密码,服务器通过发送包含一些有效负载(如JWT)的HTTPOnly Cookie来响应。

我一直使用的方法在几年前就开始运行,直到大约上周Set-Cookie头部停止工作。在REST API的功能失效之前,我没有触碰过它的源代码,因为我一直在处理基于Svelte的前端。

我怀疑问题可能与Secure属性被设置为false有关,因为它在本地主机上。然而,根据使用HTTP cookies的说明,只要是本地主机,使用不安全的连接应该是可以的。我以这种方式开发REST API已经有一段时间了,所以看到Cookie不再被设置,我感到很惊讶。

使用Postman测试API会得到预期的结果,即设置Cookie。

使用的方法

我尝试重新创建真实API的一般流程,并将其简化为其核心要素。

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/cors"
	"github.com/golang-jwt/jwt/v4"
)

const idleTimeout = 5 * time.Second

func main() {
	app := fiber.New(fiber.Config{
		IdleTimeout: idleTimeout,
	})

	app.Use(cors.New(cors.Config{
		AllowOrigins:     "*",
		AllowHeaders:     "Origin, Content-Type, Accept, Range",
		AllowCredentials: true,
		AllowMethods:     "GET,POST,HEAD,DELETE,PUT",
		ExposeHeaders:    "X-Total-Count, Content-Range",
	}))

	app.Get("/", hello)
	app.Post("/login", login)

	go func() {
		if err := app.Listen("0.0.0.0:8080"); err != nil {
			log.Panic(err)
		}
	}()

	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt, syscall.SIGTERM)

	_ = <-c
	fmt.Println("\n\nShutting down server...")
	_ = app.Shutdown()
}

func hello(c *fiber.Ctx) error {
	return c.SendString("Hello, World!")
}

func login(c *fiber.Ctx) error {
	type LoginInput struct {
		Email string `json:"email"`
	}

	var input LoginInput

	if err := c.BodyParser(&input); err != nil {
		return c.Status(400).SendString(err.Error())
	}

	stringUrl := fmt.Sprintf("https://jsonplaceholder.typicode.com/users?email=%s", input.Email)

	resp, err := http.Get(stringUrl)
	if err != nil {
		return c.Status(500).SendString(err.Error())
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return c.Status(500).SendString(err.Error())
	}

	if len(body) > 0 {
		fmt.Println(string(body))
	} else {
		return c.Status(400).JSON(fiber.Map{
			"message": "Yeah, we couldn't find that user",
		})
	}

	token := jwt.New(jwt.SigningMethodHS256)
	cookie := new(fiber.Cookie)

	claims := token.Claims.(jwt.MapClaims)
	claims["purpose"] = "Just a test really"

	signedToken, err := token.SignedString([]byte("NiceSecret"))
	if err != nil {
		// Internal Server Error if anything goes wrong in getting the signed token
		fmt.Println(err)
		return c.SendStatus(500)
	}

	cookie.Name = "access"
	cookie.HTTPOnly = true
	cookie.Secure = false
	cookie.Domain = "localhost"
	cookie.SameSite = "Lax"
	cookie.Path = "/"
	cookie.Value = signedToken
	cookie.Expires = time.Now().Add(time.Hour * 24)

	c.Cookie(cookie)

	return c.Status(200).JSON(fiber.Map{
		"message": "You have logged in",
	})
}

这段代码的作用是查找JSON Placeholder的用户,如果找到与电子邮件匹配的用户,则发送带有附加数据的HTTPOnly Cookie。

考虑到可能是我使用的库的问题,我决定使用Express编写一个Node版本。

import axios from 'axios'
import express from 'express'
import cookieParser from 'cookie-parser'
import jwt from 'jsonwebtoken'

const app = express()

app.use(express.json())
app.use(cookieParser())
app.use(express.urlencoded({ extended: true }))
app.disable('x-powered-by')

app.get("/", (req, res) => {
    res.send("Hello there!")
})

app.post("/login", async (req, res, next) => {
    try {
        const { email } = req.body

        const { data } = await axios.get(`https://jsonplaceholder.typicode.com/users?email=${email}`)

        if (data) {
            if (data.length > 0) {
                res.locals.user = data[0]
                next()
            } else {
                return res.status(404).json({
                    message: "No results found"
                })
            }
        }
    } catch (error) {
        return console.error(error)
    }
}, async (req, res) => {
    try {
        let { user } = res.locals

        const token = jwt.sign({
            user: user.name
        }, "mega ultra secret sauce 123")

        res
            .cookie(
                'access',
                token,
                {
                    httpOnly: true,
                    secure: false,
                    maxAge: 3600
                }
            )
            .status(200)
            .json({
                message: "You have logged in, check your cookies"
            })
    } catch (error) {
        return console.error(error)
    }
})

app.listen(8000, () => console.log(`Server is up at localhost:8000`))

这两个在我测试过的浏览器上都不起作用。

结果

Go的响应如下。

HTTP/1.1 200 OK
Date: Mon, 21 Feb 2022 05:17:36 GMT
Content-Type: application/json
Content-Length: 32
Vary: Origin
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-Total-Count,Content-Range
Set-Cookie: access=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwdXJwb3NlIjoiSnVzdCBhIHRlc3QgcmVhbGx5In0.8YKepcvnMreP1gUoe_S3S7uYngsLFd9Rrd4Jto-6UPI; expires=Tue, 22 Feb 2022 05:17:36 GMT; domain=localhost; path=/; HttpOnly; SameSite=Lax

对于Node API,这是响应头。

HTTP/1.1 200 OK
Set-Cookie: access=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiTGVhbm5lIEdyYWhhbSIsImlhdCI6MTY0NTQyMDM4N30.z1NQcYm5XN-L6Bge_ECsMGFDCgxJi2eNy9sg8GCnhIU; Max-Age=3; Path=/; Expires=Mon, 21 Feb 2022 05:13:11 GMT; HttpOnly
Content-Type: application/json; charset=utf-8
Content-Length: 52
ETag: W/"34-TsGOkRa49turdlOQSt5gB2H3nxw"
Date: Mon, 21 Feb 2022 05:13:07 GMT
Connection: keep-alive
Keep-Alive: timeout=5

客户端源代码

我使用这个作为测试表单来发送和接收数据。

<script>
    let email = "";

    async function handleSubmit() {
        try {
            let response = await fetch(`http://localhost:8000/login`, {
                method: "POST",
                body: JSON.stringify({
                    email,
                }),
                headers: {
                    "Content-Type": "application/json",
                },
            });

            if (response) {
                console.info(response);
                let result = await response.json();

                if (result) {
                    console.info(result);
                }
            }
        } catch (error) {
            alert("Something went wrong. Check your console.");
            return console.error(error);
        }
    }
</script>

<h1>Please Login</h1>

<svelte:head>
    <title>Just a basic login form</title>
</svelte:head>

<form on:submit|preventDefault={handleSubmit}>
    <label for="email">Email:</label>
    <input
        type="email"
        name="email"
        bind:value={email}
        placeholder="enter your email"
    />
</form>

附加信息

Postman: 9.8.3

语言版本

Go: 1.17.6

Node.js: v16.13.1

Svelte: 3.44.0

使用的浏览器

Mozilla Firefox: 97.0.1

Microsoft Edge: 98.0.1108.56

Chromium: 99.0.4781.0

英文:

Problem

I have a REST API that has a login endpoint. The login endpoint accepts a username and password, the server responds by sending a HTTPOnly Cookie containing some payload (like JWT).

The approach I always use had been working for a few years until the Set-Cookie header stopped working roughly last week. I have not touched the REST API's source prior to its non-functionality, as I was working on a Svelte-based front-end.

I suspect it has something to do with the Secure attribute being set to false as it is in localhost. However, according to Using HTTP cookies, having an insecure connection should be fine as long as it's localhost. I've been developing REST APIs in this manner for some time now and was surprised to see the cookie no longer being set.

Testing the API with Postman yields the expected result of having the cookie set.

Approaches Used

I tried to recreate the general flow of the real API and stripped it down to its core essentials.

package main

import (
	&quot;fmt&quot;
	&quot;io/ioutil&quot;
	&quot;log&quot;
	&quot;net/http&quot;
	&quot;os&quot;
	&quot;os/signal&quot;
	&quot;syscall&quot;
	&quot;time&quot;

	&quot;github.com/gofiber/fiber/v2&quot;
	&quot;github.com/gofiber/fiber/v2/middleware/cors&quot;
	&quot;github.com/golang-jwt/jwt/v4&quot;
)

const idleTimeout = 5 * time.Second

func main() {
	app := fiber.New(fiber.Config{
		IdleTimeout: idleTimeout,
	})

	app.Use(cors.New(cors.Config{
		AllowOrigins:     &quot;*&quot;,
		AllowHeaders:     &quot;Origin, Content-Type, Accept, Range&quot;,
		AllowCredentials: true,
		AllowMethods:     &quot;GET,POST,HEAD,DELETE,PUT&quot;,
		ExposeHeaders:    &quot;X-Total-Count, Content-Range&quot;,
	}))

	app.Get(&quot;/&quot;, hello)
	app.Post(&quot;/login&quot;, login)

	go func() {
		if err := app.Listen(&quot;0.0.0.0:8080&quot;); err != nil {
			log.Panic(err)
		}
	}()

	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt, syscall.SIGTERM)

	_ = &lt;-c
	fmt.Println(&quot;\n\nShutting down server...&quot;)
	_ = app.Shutdown()
}

func hello(c *fiber.Ctx) error {
	return c.SendString(&quot;Hello, World!&quot;)
}

func login(c *fiber.Ctx) error {
	type LoginInput struct {
		Email string `json:&quot;email&quot;`
	}

	var input LoginInput

	if err := c.BodyParser(&amp;input); err != nil {
		return c.Status(400).SendString(err.Error())
	}

	stringUrl := fmt.Sprintf(&quot;https://jsonplaceholder.typicode.com/users?email=%s&quot;, input.Email)

	resp, err := http.Get(stringUrl)
	if err != nil {
		return c.Status(500).SendString(err.Error())
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return c.Status(500).SendString(err.Error())
	}

	if len(body) &gt; 0 {
		fmt.Println(string(body))
	} else {
		return c.Status(400).JSON(fiber.Map{
			&quot;message&quot;: &quot;Yeah, we couldn&#39;t find that user&quot;,
		})
	}

	token := jwt.New(jwt.SigningMethodHS256)
	cookie := new(fiber.Cookie)

	claims := token.Claims.(jwt.MapClaims)
	claims[&quot;purpose&quot;] = &quot;Just a test really&quot;

	signedToken, err := token.SignedString([]byte(&quot;NiceSecret&quot;))
	if err != nil {
		// Internal Server Error if anything goes wrong in getting the signed token
		fmt.Println(err)
		return c.SendStatus(500)
	}

	cookie.Name = &quot;access&quot;
	cookie.HTTPOnly = true
	cookie.Secure = false
	cookie.Domain = &quot;localhost&quot;
	cookie.SameSite = &quot;Lax&quot;
	cookie.Path = &quot;/&quot;
	cookie.Value = signedToken
	cookie.Expires = time.Now().Add(time.Hour * 24)

	c.Cookie(cookie)

	return c.Status(200).JSON(fiber.Map{
		&quot;message&quot;: &quot;You have logged in&quot;,
	})
}

What does this is basically look through JSON Placeholder's Users and if it finds one with a matching email, it sends the HTTPOnly Cookie with some data attached to it.

Seeing as it might be a problem with the library I'm using, I decided to write a Node version with Express.

import axios from &#39;axios&#39;
import express from &#39;express&#39;
import cookieParser from &#39;cookie-parser&#39;
import jwt from &#39;jsonwebtoken&#39;

const app = express()

app.use(express.json())
app.use(cookieParser())
app.use(express.urlencoded({ extended: true }))
app.disable(&#39;x-powered-by&#39;)

app.get(&quot;/&quot;, (req, res) =&gt; {
    res.send(&quot;Hello there!&quot;)
})

app.post(&quot;/login&quot;, async (req, res, next) =&gt; {
    try {
        const { email } = req.body

        const { data } = await axios.get(`https://jsonplaceholder.typicode.com/users?email=${email}`)

        if (data) {
            if (data.length &gt; 0) {
                res.locals.user = data[0]
                next()
            } else {
                return res.status(404).json({
                    message: &quot;No results found&quot;
                })
            }
        }
    } catch (error) {
        return console.error(error)
    }
}, async (req, res) =&gt; {
    try {
        let { user } = res.locals

        const token = jwt.sign({
            user: user.name
        }, &quot;mega ultra secret sauce 123&quot;)

        res
            .cookie(
                &#39;access&#39;,
                token,
                {
                    httpOnly: true,
                    secure: false,
                    maxAge: 3600
                }
            )
            .status(200)
            .json({
                message: &quot;You have logged in, check your cookies&quot;
            })
    } catch (error) {
        return console.error(error)
    }
})

app.listen(8000, () =&gt; console.log(`Server is up at localhost:8000`))

Both of these do not work on the browsers I've tested them on.

Results

Go responds with this.

HTTP/1.1 200 OK
Date: Mon, 21 Feb 2022 05:17:36 GMT
Content-Type: application/json
Content-Length: 32
Vary: Origin
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-Total-Count,Content-Range
Set-Cookie: access=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwdXJwb3NlIjoiSnVzdCBhIHRlc3QgcmVhbGx5In0.8YKepcvnMreP1gUoe_S3S7uYngsLFd9Rrd4Jto-6UPI; expires=Tue, 22 Feb 2022 05:17:36 GMT; domain=localhost; path=/; HttpOnly; SameSite=Lax

For the Node API, this is the response header.

HTTP/1.1 200 OK
Set-Cookie: access=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiTGVhbm5lIEdyYWhhbSIsImlhdCI6MTY0NTQyMDM4N30.z1NQcYm5XN-L6Bge_ECsMGFDCgxJi2eNy9sg8GCnhIU; Max-Age=3; Path=/; Expires=Mon, 21 Feb 2022 05:13:11 GMT; HttpOnly
Content-Type: application/json; charset=utf-8
Content-Length: 52
ETag: W/&quot;34-TsGOkRa49turdlOQSt5gB2H3nxw&quot;
Date: Mon, 21 Feb 2022 05:13:07 GMT
Connection: keep-alive
Keep-Alive: timeout=5

Client Source

I'm using this as a test form to send and receive data.

&lt;script&gt;
    let email = &quot;&quot;;

    async function handleSubmit() {
        try {
            let response = await fetch(`http://localhost:8000/login`, {
                method: &quot;POST&quot;,
                body: JSON.stringify({
                    email,
                }),
                headers: {
                    &quot;Content-Type&quot;: &quot;application/json&quot;,
                },
            });

            if (response) {
                console.info(response);
                let result = await response.json();

                if (result) {
                    console.info(result);
                }
            }
        } catch (error) {
            alert(&quot;Something went wrong. Check your console.&quot;);
            return console.error(error);
        }
    }
&lt;/script&gt;

&lt;h1&gt;Please Login&lt;/h1&gt;

&lt;svelte:head&gt;
    &lt;title&gt;Just a basic login form&lt;/title&gt;
&lt;/svelte:head&gt;

&lt;form on:submit|preventDefault={handleSubmit}&gt;
    &lt;label for=&quot;email&quot;&gt;Email:&lt;/label&gt;
    &lt;input
        type=&quot;email&quot;
        name=&quot;email&quot;
        bind:value={email}
        placeholder=&quot;enter your email&quot;
    /&gt;
&lt;/form&gt;

Additional Information

Postman: 9.8.3

Language Versions

Go: 1.17.6

Node.js: v16.13.1

Svelte: 3.44.0

Browsers Used

Mozilla Firefox: 97.0.1

Microsoft Edge: 98.0.1108.56

Chromium: 99.0.4781.0

答案1

得分: 2

解决方案

问题出在前端,具体来说是JavaScript的fetch()方法。

let response = await fetch(`http://localhost:8000/login`, {
                method: "POST",
                credentials: "include", //---> 发送/接收cookie
                body: JSON.stringify({
                    email,
                }),
                headers: {
                    "Content-Type": "application/json",
                },
            });

你需要在RequestInit对象中添加credentials: include属性,不仅用于发送需要cookie身份验证的请求,还用于接收cookie。

根据经验,Axios通常会自动填充这部分内容,但如果没有自动填充,你还需要在请求的第三个config参数中添加withCredentials: true,以允许浏览器设置cookie。

英文:

Solution

It turns out the problem is in the front-end, specifically JavaScript's fetch() method.

let response = await fetch(`http://localhost:8000/login`, {
                method: &quot;POST&quot;,
                credentials: &quot;include&quot;, //--&gt; send/receive cookies
                body: JSON.stringify({
                    email,
                }),
                headers: {
                    &quot;Content-Type&quot;: &quot;application/json&quot;,
                },
            });

You'll need credentials: include property in your RequestInit object, not just for making requests that require cookie authentication, but for also receiving said cookie.

Axios usually fills this part out automatically (based from experience), but if it doesn't, you'll also need to put withCredentials: true on the third config argument of your request to allow the browser to set the cookie.

答案2

得分: 2

我刚刚遇到了与axios相同的问题,这导致Set-Cookie响应头被静默忽略。这真的很烦人,因为通常如果它拒绝了这些头部,它会在网络检查器中显示一个小黄色三角形,并说明原因。

我通过添加一个请求拦截器来解决这个问题,强制将其设置为每个请求都为true

axios.interceptors.request.use(
    (config) => {
      config.withCredentials = true
      return config
    },
    (error) => {
      return Promise.reject(error)
    }
  )
英文:

I just had the same issue with axios, this was causing the Set-Cookie response header to be silently ignored. Which was annoying as usually if it rejects them it will show that little yellow triangle against that header and say why in the network inspector.

I solved this by adding a request interceptor to force it true for every request:

axios.interceptors.request.use(
(config) =&gt; {
config.withCredentials = true
return config
},
(error) =&gt; {
return Promise.reject(error)
}
)

huangapple
  • 本文由 发表于 2022年2月21日 13:36:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/71201627.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定