第48話で、イトはbrowser dataを全部消さなかった。
changed CSSだけをnew URIへつなぎ、Cookie boxとhistory ribbonをclosed coverの向こうへ残した。
operator login badgeも消えていない。
ところが、gold finale pageのstart buttonを押すと、account gateは401 cardを返した。
画面には「login済み」のbadgeがある。
browser shelfにはsidというCookieもある。
「あるのに届かないなら、全部のrequestへ付ければいい」
イトはCookie boxから、Domain、Path、Secure、HttpOnlyの四ringをまとめて外せるlarge scope barへ手を掛けた。
live projectorがfinaleを開始するまで、あと七十秒。
Cookieは、保存したあとに選ばれる
ピコは401 cardではなく、直前のthree-request traceを照らした。
イト
loginのCookieがあるなら、siteのrequest全部に乗るんじゃないの?
ピコ
保存されたCookie全部を、毎回まとめて送るわけじゃないよ
イト
requestごとに、送るcardを選び直す?
ピコ
うん。request targetと、Cookieが持つscopeを照らし合わせるんだ
この話のbrowser、cookie inspector、account gate、three-request traceは、外のInternetから切り離した学習fixtureだ。
実browserのstorage UI、Cookie blocking、third-party policyを再現しない。
host名とtokenもfixture専用で、実在serviceのcredentialではない。
Set-Cookieはresponse、Cookieはrequest
RFC 6265は、serverがSet-Cookie response headerでname / valueとmetadataをuser agentへ渡し、後のscope内requestでuser agentがCookie request headerへname / valueを戻す仕組みを定める。
向きが違う。
server → user agent
Set-Cookie: sid=opaque-7K; Path=/account/login; Secure; HttpOnly
user agent → server
Cookie: sid=opaque-7K
Cookie headerへ、Path、Secure、HttpOnly等のattributesはそのまま戻らない。
serverは受け取ったCookie headerだけを見て、そのCookieがどのPathやexpiryで保存されていたかを知るわけではない。
また、sidのvalueが何を意味するかもHTTP protocolは決めない。
このfixtureでは、server-side session ledgerの一行を探すopaque identifierとして扱う。
password、氏名、cart全部が、この短いvalueへそのまま書かれているとは限らない。
stored cardには、送信条件が残る
イトはlogin responseから保存されたrecordを開いた。
name:
sid
value:opaque-7K
host-only:account.pico.test
Path:/account/login
Secure: true
HttpOnly: true
expiry: current fixture session
Domain attributeはなかった。
RFC 6265のDomain attributeでは、Domainを省略したCookieはorigin serverだけへ返す形になる。
このfixture recordではhost-only flagが立ち、account.pico.test以外へ送らない。
Domain attributeを付けると、受け付け可能な範囲でsubdomain側まで対象を広げ得る。
しかし、unrelated hostを自由に指定できるわけではない。
serverを含まないDomain scopeはuser agentに拒否される。
「同じsiteに見えるから」という理由でDomainを広げると、不要なhostにもCookieを送る設計になり得る。
Pathは、URL文字列の飾りではない
start buttonが送ったexact requestはこれだった。
method: POST
target:https://account.pico.test/account/finale/start
hostは一致する。
HTTPSなのでSecure条件も満たす。
expiryも切れていない。
でも、stored Pathは/account/loginだ。
request pathの/account/finale/startは、そのcookie-pathにpath-matchしない。
RFC 6265のPath-Matchは、request pathがcookie pathと同一か、directory boundaryを保ってprefix関係になる条件を定める。
文字列が少し似ているだけでは足りない。
fixture user agentはこのsidをCookie headerへ含めなかった。
account serverにはidentifierが届かず、このfixtureは401を返した。
401だけで「Cookieが削除された」とは言えない。
保存されていても、current requestにはapplicableでない場合がある。
ただし、Pathは互いに信頼しないserviceを隔離するsecurity boundaryとして頼れない。
この回では、requestへCookieを含めるselection conditionとして観測する。
three requestsを、同じsite trafficにまとめない
同じpageから、三本のrequestが出ていた。
POST https://account.pico.test/account/finale/start
GET https://static.pico.test/assets/gold-curtain.avif
POST http://account.pico.test/account/finale/start
一番はaccount hostだが、stored Pathが狭すぎる。
二番はstaticという別hostなので、host-only sidを送らない。
三番はaccount host / matching candidate pathでも、HTTPでsecure channelではないため、Secure Cookieを送らない。
RFC 6265のSecure attributeは、Cookieをsecure channelへ限定する。
「二番と三番にもCookieがない」は、同じ故障ではない。
送らないことが正しいrequestもある。
HttpOnlyは、network送信を止めるringではない
イトはHttpOnly ringも見た。
RFC 6265のHttpOnly attributeは、scriptへCookieを露出するようなnon-HTTP APIでCookieをomitするためのattributeだ。
HttpOnlyだから、matching HTTP requestのCookie headerへ絶対に入らないわけではない。
このfixtureでは、scriptからsid valueを読めないまま、user agentがapplicableなHTTPS requestへ自動で含められる。
だから「button codeからCookieが見えない」を、保存失敗の証明にしない。
SecureとHttpOnlyも別の役割だ。
Secureはrequest channelの条件。
HttpOnlyはnon-HTTP APIへの露出制限。
名前がsecurityらしいという理由で一箱へまとめない。
expiryは、永久保存の約束ではない
Cookie recordにはexpiryもある。
ExpiresやMax-Ageがあれば、persistent cookieのlifetimeをserverが示せる。
なければ、current sessionの終わりまでとして扱う入口になる。
ただし、userが削除したり、user agentが容量等の理由で早くevictしたりする場合もある。
expiryは「この時刻まで必ず存在する保証」ではない。
今回のfixture clockではcurrent session内で、recordは存在している。
原因をexpiryへすり替えない。
イトの前に、三つの選択肢が開く
projector開始まで、あと四十秒。
A Domainを広げ、Pathを/にし、security ringsも外す
一番のrequestへ届く可能性は上がる。
しかし、static等の不要なhost / pathやinsecure channelへscopeを広げる。
どの条件が原因だったかも消える。
B sid valueをURLやpage dataへ複製する
Cookie selectionを避けて、identifierを別の場所へ運ぶ案だ。
この回はcredential transportの新方式を作る話ではない。
opaque identifierをURL等へ露出させず、採用しない。
C exact requestとstored scopeを一ringずつ比べる
host-only、Path、Secure、HttpOnly、expiryを別slotへ置く。
failureを作ったringだけを直す。
不要なrequestへCookieが付かないことも、修理後に確認する。
ピコはscope barを動かさなかった。
「Cookieを増やす前に、どのrequestがこのCookieを必要としているか決めよう」
イトはCを選んだ。
large all-scope barをclosed coverへ戻し、three request cardsを三本のtransparent railへ分けた。
old Pathだけを、exact tupleで閉じる
問題はhost-onlyでもSecureでもHttpOnlyでもexpiryでもない。
login endpointだけに限定したPath=/account/loginが、finale APIには狭すぎた。
イトはaccount fixtureのresponse planを直した。
ただし、同じnameでもPathが違えば、old recordが自動で同じ一枚へ上書きされるとは限らない。
old narrow-path cookieを残したままnew pathを追加すると、同じnameの複数Cookieが共存し得る。
serverはCookie header内の同名pair順序へ依存すべきではない。
イトはまず、old name / host / Pathとそろえたexpiration responseを返した。
Set-Cookie: sid=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/account/login; Secure; HttpOnly
次に、same host-onlyとsecurity attributesを保ち、必要なaccount subtreeだけを含むnew recordを返した。
Set-Cookie: sid=opaque-7K; Path=/account; Secure; HttpOnly
Domainは追加しない。
Pathをsite rootの/まで広げない。
Secure / HttpOnlyを外さない。

一本だけに届くことが、成功だった
fixture user agentはnew recordを保存した。
イトはthree-request traceをもう一度流す。
一番。
POST https://account.pico.test/account/finale/start
host-only一致。
/account/finale/startはPath=/accountにpath-matchする。
secure channelで、expiry内だ。
user agentはCookie: sid=opaque-7Kを含めた。
account fixtureはserver-side ledgerを照合し、200 start cardを返した。
二番のstatic hostにはsidを付けないまま、gold curtain imageは200で取得できた。
三番のinsecure HTTP requestにもSecure sidを付けず、fixture gateはclosedのままにした。
three requests全部へCookieが付いたことが成功ではない。
必要な一本へだけ届き、残り二本へ届かなかったことがobservable resultだった。
Cookieを疑うときは、送信先を主語にする
projectorでfinaleが始まった。
browser shelfには、四つの結果が残っている。
matching HTTPS account requestへ渡ったsingle sid card。
Cookieなしで成功したstatic image request。
Secure Cookieを渡さず閉じたinsecure request gate。
retired trayに残したold narrow Path ring。
Cookieがあるかないかだけで決めず、Set-Cookie recordとexact requestを分ける。host / Domain、Path、Secure、expiryを確認し、必要なrequestだけがapplicableになる最小scopeを作る。原因を見ないままDomain、Path、security attributesをまとめて広げない。
「送らなかった二本も、修理の結果なんだ」
イトはamber tokenをaccount railだけへ残し、static railとinsecure railのempty holderを消さなかった。
account gateの奥で、opaque-7Kと同じkeyを持つledger rowが青く光った。
Cookieは届いた。
でも、その短いvalueだけでは、login中、cart、expiry、権限のどこまでが続いているか分からない。
次回:sessionは、どこで続く?
server-side ledgerには、同じidentifierへ結び付くstateとdeadlineがある。
「Cookieが残っていれば、sessionも必ず生きているの?」
イトはbrowserのCookie cardとserverのledger rowを、透明separatorの両側へ置いた。
次回、ピコルート第50話。
イトは、Cookie cardだけでlogin中と決めなかった|sessionはどこで続いている?

