ライブラリに頼らず Cookie を扱うためには

thumbnail

簡単な開発をしたいときにちょっとだけ Cookie を使いたいときがあると思う。私は日頃から Web 標準な何かをするときはライブラリを使うことに抵抗があり、Cookie 周りの操作もライブラリを使わずにやりたい。が、そんなちょっとカッコ良い発言をしている裏で、私はこっそり「Cookie 付けるのってどうするんだっけ?」「Cookie のフォーマットってどんなんだっけ?」といつも Google で調べている。もちろん Set-Cookie くらいは覚えているが、「順番は?どういうデリミタで?どういうパーサーが必要で?」というのは結構忘れているし、意外と皆さんもすっとは出てこないのではないだろうか。あ、私だけですか、すみません。。。と、私は毎回調べているが、毎回調べるのはめんどくさいのでメモを書いておこうと思う。

OGP は クッキーに見せかけた空気だ。「こいつ最近 OGP でふざけてるし、どうせクッキーの画像を OGP にしてるんでしょ」という予想を裏切れたと思う。

Cookie は RFC 6265 HTTP State Management Mechanism で定義されている。

冒頭に

This document defines the HTTP Cookie and Set-Cookie header fields. These header fields can be used by HTTP servers to store state (called cookies) at HTTP user agents, letting the servers maintain a stateful session over the mostly stateless HTTP protocol.

とある通り、HTTP は stateless だがそれを Cookie は stateful に扱えるような仕組みを提供する。簡単にいうとあるリクエストで認証したけど次のリクエストではその認証した事実をサーバーもブラウザも覚えていないから、それを覚えられるようにできるものだ。きっとなんらかの JWT を埋め込んだり取り出した経験はあると思う。これはだいたいサーバーのフレームワークで res.setCookie のようなメソッドが生えているのでそれを使って何かを埋め込むのも見覚えがあるだろう。

そしてその Cookie のセットは、FW に頼らずとも Web 標準な機能としてある。それも先ほどの HTTP State Management Mechanism で定義されており、

The Set-Cookie HTTP response header is used to send cookies from the server to the user agent.

とあるように、Response Header に Set-Cookie 付けることで実現できる。

文法

RFC では ABNF が

 set-cookie-header = "Set-Cookie:" SP set-cookie-string
 set-cookie-string = cookie-pair *( ";" SP cookie-av )
 cookie-pair       = cookie-name "=" cookie-value
 cookie-name       = token
 cookie-value      = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
 cookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
                       ; US-ASCII characters excluding CTLs,
                       ; whitespace DQUOTE, comma, semicolon,
                       ; and backslash
 token             = <token, defined in [RFC2616], Section 2.2>

 cookie-av         = expires-av / max-age-av / domain-av /
                     path-av / secure-av / httponly-av /
                     extension-av
 expires-av        = "Expires=" sane-cookie-date
 sane-cookie-date  = <rfc1123-date, defined in [RFC2616], Section 3.3.1>
 max-age-av        = "Max-Age=" non-zero-digit *DIGIT
                       ; In practice, both expires-av and max-age-av
                       ; are limited to dates representable by the
                       ; user agent.
 non-zero-digit    = %x31-39
                       ; digits 1 through 9
 domain-av         = "Domain=" domain-value
 domain-value      = <subdomain>
                       ; defined in [RFC1034], Section 3.5, as
                       ; enhanced by [RFC1123], Section 2.1
 path-av           = "Path=" path-value
 path-value        = <any CHAR except CTLs or ";">
 secure-av         = "Secure"
 httponly-av       = "HttpOnly"
 extension-av      = <any CHAR except CTLs or ";">

で与えられている。MDN を見るともう少し人間にわかりやすく書かれているのでそれを確認すると、

Set-Cookie: <cookie-name>=<cookie-value>
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>
Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly
Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<number>
Set-Cookie: <cookie-name>=<cookie-value>; Partitioned
Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
Set-Cookie: <cookie-name>=<cookie-value>; Secure

Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Strict
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Lax
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=None; Secure

// Multiple attributes are also possible, for example:
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly

とある。 key=value; attributes というのが一般的なようだ。だが色々な種類がある。ただ、とはいってもこの全パターンを覚える必要がないというか、使うことは滅多にないだろうということで、自分用メモに使う表現を書いておく。

  • Set-Cookie: hoge というレスポンスヘッダーでクッキーを付けられる
  • cookie は name と value のペア以外にも Path や Domain や HttpOnly のような属性がある
  • 属性は name=value を書いた後に ; (スペース有)を入れて 属性名=属性値 か値のみを書く
  • 属性は複数つけれるのでそれは属性間に ; (スペース有)を入れて書く

といった感じだろう。

で、どういう属性がつけられるかを RFC を確認したいが、実はこれは rfc6265bis という改訂版があるのでこちらで確認してみる。そうすると、

  • The Expires Attribute
  • The Max-Age Attribute
  • The Domain Attribute
  • The Path Attribute
  • The Secure Attribute
  • The HttpOnly Attribute
  • The SameSite Attribute

が使えるようだ。いまは内容については触れない。RFC もしくは MDNを見て欲しい。

あと Chrome では Partitioned Cookie というのも使えるらしい。(これは何も知らないし、使ったこともない)

FYI: https://developer.chrome.com/ja/docs/privacy-sandbox/chips/

chips

FYI: https://developer.mozilla.org/en-US/docs/Web/Privacy/Partitioned_cookies

コピペ用の具体例

つまりは、

Set-Cookie: ore_no_cookie=ore_no_value; Max-Age=99999 Secure; HttpOnly

のようなレスポンスヘッダを付ければいい。

複数のクッキーを付けたい

1つの set-cookie でつけれるクッキーは、

set-cookie-header = "Set-Cookie:" SP set-cookie-string
set-cookie-string = cookie-pair *( ";" SP cookie-av )
cookie-pair       = cookie-name "=" cookie-value

という ABNF を見る限りは1つだ。

では複数の Cookie を付けるにはどうすればいいかというと、複数回 Set-Cookie するのである。

つまりレスポンスヘッダに

Set-Cookie: cookie1=value1;
Set-Cookie: cookie2=value2; HttpOnly
Set-Cookie: cookie3=value3; Secure

とすればいい。RFC にも

As shown in the next example, the server can store multiple cookies at the user agent. For example, the server can store a session identifier as well as the user's preferred language by returning two Set-Cookie header fields.

とある。

Cookie は何に使うかというとサーバーが別のリクエストでの state を引き継ぐためだ。つまりサーバーは Cookie 文字列から目的となるデータを取り出す必要がある。サーバーが受け取るクッキーはリクエストヘッダに

Cookie: cookie1=value1; cookie2=value2

という形で含まれる。

RFC 上では section-4.2 にある。そこでは

4.2.1. Syntax The user agent sends stored cookies to the origin server in the Cookie header. If the server conforms to the requirements in Section 4.1 (and the user agent conforms to the requirements in Section 5), the user agent will send a Cookie header that conforms to the following grammar

とある。4.1 というのは最初に示した ABNF だ。つまりその ABNF の定義を踏まえて(cookie-pair)ここでされている ABNF

cookie-header = "Cookie:" OWS cookie-string OWS
cookie-string = cookie-pair *( ";" SP cookie-pair )

を見ると、Cookie: cookie1=value1; cookie2=value2 という説明であっている。

ここではサーバーが受け取るときは HttpOnly などの Attributes がないことに注意しよう。RFC にも

Notice that the cookie attributes are not returned.

とある。パースが楽で嬉しい。

なので cookie1=value1; cookie2=value2 のような文字列を受け取ったら、

const keyValuePairs = cookieString.split("; ");

keyValuePairs.forEach((pair) => {
  const [key, value] = pair.split("=");
  console.log(key, value);
});

のようにして parse できる。こんな感じで parse できるだろう。

❯ node
Welcome to Node.js v18.14.0.
Type ".help" for more information.

> const cookieString = "cookie1=value1; cookie2=value2"
undefined

> const keyValuePairs = cookieString.split("; ");
undefined
>
> keyValuePairs.forEach((pair) => {
...   const [key, value] = pair.split("=");
...   console.log(key, value);
... });

cookie1 value1
cookie2 value2

おわりに

くどくど書いたが、Cookie の使い方は、RFC の Examplesにある

== Server -> User Agent ==

Set-Cookie: SID=31d4d96e407aad42; Path=/; Secure; HttpOnly
Set-Cookie: lang=en-US; Path=/; Domain=example.com

== User Agent -> Server ==

Cookie: SID=31d4d96e407aad42; lang=en-US

例が全てを物語っていると思う。迷ったらこれを見るようにすると良さそうだ。