Slide 1

Slide 1 text

疎通2024 Web Developer Conference 2024 @sadnessOjisan

Slide 2

Slide 2 text

自己紹介 ● ID: sadnessOjisan ● 所属: ● 自慢: 勢いで会社を辞めたらバズっ た。癖になりそう。

Slide 3

Slide 3 text

この発表の概要 ● 目的: 疎通において同じようなトラブルで何度も時間を浪費してしまっていま す。そのときに見返せる資料としての発表を目指しています。 ● はじめに、ローカルホストで開発を完結させるための努力とその限界について 述べます。 ● 実APIに繋ぎに行くときによく出会うトラブルを紹介します。 ● プラクティスについては自分も正解を知らないこともあります。この後のアフ タートークでの議論を楽しみにしています。

Slide 4

Slide 4 text

トークの前提 ● 疎通はクライアントとサーバーで行うものです ● クライアント目線、サーバー目線双方の話が含まれます ● クライアントはブラウザを前提として話が進みます ● ブラウザのセキュリティモデルを前提とします。ネイティブアプリについては 触れません。

Slide 5

Slide 5 text

ローカルで開発を完結させるための努力

Slide 6

Slide 6 text

よくあるアプローチ ● モックを駆使する ● 対向相手をローカルで起動する

Slide 7

Slide 7 text

コンポーネントカタログ(Storybookなど) ● APIと疎通せずにUIを構築できる ● インタラクションをテストでき、Integration Test にも使える ● MSWを組み合わせればAPIモックをしてUIを作れる

Slide 8

Slide 8 text

コンポーネントカタログの限界 ● story ファイルを用意するのが大変 ● story と相性の悪いライブラリや実装もある ● story のメンテナンスが困難 ○ spec の追従 ○ ライブラリの更新 ○ 増えるコンポーネントや追加仕様に対する追従 ● 完璧にstoryを用意してもフロントエンドの正しさしか表現できない ※ story は storybook に限らず open な spec. https://github.com/ComponentDriven/csf

Slide 9

Slide 9 text

API Spec ● Client, Mock Server, TypeDef, Validator を生成でき、ドキュメントとしても 利用できる ● レスポンスの型が分かるので、実APIを叩かずに開発を進められる

Slide 10

Slide 10 text

Swagger Code Gen ● OASがあれば Swagger エコシステムからモック サーバーを立てられる ● jar や docker image でジェネレータが配布されて いる ● docker run --rm -v ${PWD}:/local swaggerapi/swagger-codegen-cli-v3 generate -i /local/spec.yaml -l nodejs-server -o /local ● モックサーバーの出来は言語によってまちまち

Slide 11

Slide 11 text

CORS をケアしないといけない ● SwaggerUI からのリクエストに対す るレスポンスを返すサーバーとして 動作する ● 開発のためのモックサーバーとして 動作させるためには、CORSをケアす る必要がある ● モックサーバーを生成後、人力で サーバーを改変しないといけない ● 具体的には utils/writer.js に CORS のためのヘッダーを差し込む

Slide 12

Slide 12 text

Cookieがセットされない ● swagger-ui から認証をしても set-cookie されないことがある ● しかしモックサーバーの実装によっては、Cookie を使うことを明記している と、Cookie がないリクエストは不正リクエストとして弾かれる ● そこでブラウザのインスペクタから無理やり cookie をつける

Slide 13

Slide 13 text

ローカルで対向サーバーそのものを全部動かす ● 開発環境全てをコンテナ化していればコンテナオーケストレーションツール (docker compose, kind など) を使ってローカルで全てを動かせる ● DBごとローカルで動かせるので、自由にデータを差し替えたり、デバッガビ リティはかなり高い

Slide 14

Slide 14 text

ハードルは高い ● 動作確認が悩ましい ○ 確認のたびにコンテナをビルドするのはサイクルが悪い ○ 確認対象だけそのまま起動させたいが、そのためには docker compose up で立ち上げたものの うち、不要なものを手動で落とさないといけない ● 結局は認証やらで外部SaaSに頼ると、そのシークレットを持ち回る必要があ る。自チーム以外のシークレットの管理はしたくない ● 開発に携わるチームメンバー全員が熟練者である必要がある。連携を密にとれ る必要もある。

Slide 15

Slide 15 text

ローカルのみで開発を完結させるのは難しい ● 結局は開発の時点でローカルホストから開発環境のAPIに繋ぎたい時が来る ● 開発環境のAPIに実際に繋いで初めて見つかるトラブルも往々にしてある ● どういうトラブルがあるのか見ていこう!

Slide 16

Slide 16 text

CORS

Slide 17

Slide 17 text

よくあるシナリオ ● http://localhost:3000 -> https://api.ojisan.dev への通信 ● なにもしなければ fetch 自体が失敗する

Slide 18

Slide 18 text

よくある対応 ● FW的には ○ app.cors() ● 仕様的には ○ Access-Control-Allow-Origin: *

Slide 19

Slide 19 text

よくある誤解: CORS がセキュリティを守っている ● 🔺「CORSエラー」 ● ❌「CORSに弾かれた」 ● ❌「CORSが守っている」 ● ⭕セキュリティを守っているのは Same Origin Policy とブラウザ。CORSは どちらかというとそこに安全に穴を開けるための仕組み

Slide 20

Slide 20 text

Same Origin Policy は何から我々を守っているのか ● 同一オリジンポリシーは重要なセキュリティの仕組みであり、あるオリジンに よって読み込まれた文書やスクリプトが、他のオリジンにあるリソースにアク セスできる方法を制限するものです。https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy ● ブラウザが取り入れているセキュリティポリシー ● 例: https://atack.ojisan.dev から取得した JS からは、 https://atack.ojisan.dev 以外のリソースにリクエストして、情報を読み取る ことができない。 ● 注: ブラウザが情報を読み取ることを防いでいるだけで、リクエスト自体は送 信できていることに注意(後述する preflight に議論が繋がる)

Slide 21

Slide 21 text

SOPだとリクエストは送信される ● attack の JS を読み込んで、そこから自ドメインに不正にアクセスしようとし ているので、リクエストは飛んでしまっている。 ● GETであればブラウザ側でコンテンツ読み込みをエラーにしてくれていたが、 POSTやDELETEであればブラウザ側ではエラーになっているが、サーバー側 ではその実行に成功してしまっている。 ● 本当にサーバーで処理を実行させるか一旦止める必要がある。

Slide 22

Slide 22 text

preflight ● CORS のプリフライトリクエストは CORS のリクエストの一つであり、サー バーが CORS プロトコルを理解していて 準備がされていることを、特定のメソッ ドとヘッダーを使用してチェックしま す。 ● これは OPTIONS リクエストであり、 Access-Control-Request-Method,Acc ess-Control-Request-Headers, Origin の 3 つの HTTP リクエストヘッダー使 用します。 https://developer.mozilla.org/ja/docs/Glossary/Preflight_request preflight request OPTION 204 + CORS(origin, method, header) Post 不正なリクエストはこ こで止められる

Slide 23

Slide 23 text

サーバーの対応だけでいいのか ● CORS自体はサーバーが返すヘッダーで制御するが、クロスオリジンのリクエ ストを成功させるためにはクライアント側にも仕事がある ● ここでいう credentials は、Cookie, 認証ヘッダー, TLSクライアント証明書 ● つまり、Cookieを付けて fetch する場合、`credentials: true` を付けてリクエ ストしないといけない ● Access-Control-Allow-Credentials: true をサーバーが返す必要がある。 Request.credentials が include である場合、レスポンスを JS に公開してい いことをブラウザに伝えるフラグ

Slide 24

Slide 24 text

credential は Cookie の受信にも関係する ● "omit" ○ Excludes credentials from this request, and causes any credentials sent back in the response to be ignored. ● "same-origin" ○ Include credentials with requests made to same-origin URLs, and use any credentials sent back in responses from same-origin URLs. ● "include" ○ Always includes credentials with this request, and always use any credentials sent back in the response. https://fetch.spec.whatwg.org/#concept-request-credentials-mode

Slide 25

Slide 25 text

fetch の mode ● fetch には mode option があり、cors を指定できるが、どのように設定すべ きか ● 設定は意識しなくて良い。fetch でリクエストを作ると自動で cors となる

Slide 26

Slide 26 text

CSRF

Slide 27

Slide 27 text

とある会社の試験: CSRF を説明してください ● 「悪意あるページ踏んだらなんか勝手に認証情報を使って送られるやつです」 ● 「もっと具体的に」 ● 「えーっと、えーっと、えーーー、、、あれ、どうやるんだっけ、、、」 ● -> その結果、現在に至る

Slide 28

Slide 28 text

おそらく想定されていた模範解答 ● ユーザーが正規のサイトでログインしていて認証情報を持っているとする ● 攻撃者が用意したページにアクセスし、その中に含まれている正規サイトへの リクエストボタンをクリックしてしまう ● ユーザーは正規の認証情報を持っているので、正規のサイトに対して不正なリ クエストが通ってしまった 正規サイト ログイン 罠サイト 引っかかる フォーム送信など 正規の認証情報があるか ら成功してしまう

Slide 29

Slide 29 text

現代は模範解答通りに行かない ● SOPによって攻撃者のサイトから正規のサイトへの副作用があるリクエストは 止められる ● デフォルトでクッキーのSameSiteはNoneにならないので、認証情報が別サイ トに送られることがない

Slide 30

Slide 30 text

古くはCSRFトークンで防いでいた ● ログイン時にセッション情報以外に、ユーザーごとのトークンを生成しておく ● synchronizer pattern: sessionに紐づいたトークンをサーバーに保持(推奨) ● Double Submit Cookie pattern: トークンをサーバーに保持しないやり方

Slide 31

Slide 31 text

CSRFトークンのことを忘れていると疎通で痛い目を見る ● サーバーのFWによっては CSRF トークンを使うことがデフォルト設定のもの がある ● 例: https://docs.djangoproject.com/ja/5.1/howto/csrf/ ● その挙動を参考にして作られた API Spec から生成した Swagger Client(zodious) を使っていたら、ある日急に例外を吐くようになって焦った 思い出

Slide 32

Slide 32 text

Cookie

Slide 33

Slide 33 text

なぜ Cookie を使いたいのか ● HTTP は stateless なのでログインしたユーザーであることをサーバーに伝え る手段を持たないといけない ● Cookieはサーバーが勝手に読み取ってくれる上に、ブラウザが勝手に永続化 してくれる ● 仕様も安定していたり、インターネットに情報も多い なんだけどなんかよくトラブルに出会うんだよな

Slide 34

Slide 34 text

Cookie の簡単な例 サーバーに set-cookie ヘッダーを付けると良い

Slide 35

Slide 35 text

Cookie のトラブル ● Cookie がセットされない ● Cookie がサーバーに送信されない

Slide 36

Slide 36 text

fetch で Cookie を付けてみよう ● 先の Cookie を付けた画像例は、set-cookie するエンドポイントに直接ブラウ ザでアクセスした ● 代わりに set-cookie してくれるエンドポイントにlocalhost から fetch して みよう ● CORS が必要でこける

Slide 37

Slide 37 text

資格情報のリクエスト ● access-control-allow-origin をセット をする ● その結果、サーバーは set-cookie してくれるがブラウザに Cookie は付いて いない ● {credentials: 'include'} が必要

Slide 38

Slide 38 text

Access-Control-Allow-Credentials も必要 ● {credentials: 'include'} を使うならセットで Access-Control-Allow-Credentials: true が必要 ● その結果、クッキーがブラウザにセットされる ● ただし SameSite 属性の部分で警告が出ていて、まだこのままだと送信ができ ない

Slide 39

Slide 39 text

Domain 属性の扱い ● 先の画像によると、Set-Cookie したとき、Domain 属性を指定しないのであ れば、そのSet-CookieしたURLでDomainがセットされる。先の例は localhost でアクセスしているが、APIのドメインのクッキーがついている。 ● Domain属性はそのクッキーをブラウザが送れる先を指定している。 ● では、domain 属性で適当なURLを指定するとどうなるか

Slide 40

Slide 40 text

Domain 属性に自由な値は入れられない ● localhost:3000 で待ち受けているサーバーに localhost:3000 からアクセス し、set-cookie で example.com をセットしようとするとエラー。 ● リクエストのHostに対してしかクッキーをつけれない。 ● つまり example.com にアクセスして example.com が set-cookie しないとつ けれない。

Slide 41

Slide 41 text

クロスサイトなリクエストにクッキーが乗らない ● デフォルトで SameSite=Lax が指定される ● fetch を使ってクロスサイト(ここでは localhost からインターネット上の サーバー)にアクセスしているので、set した Cookie を使えない

Slide 42

Slide 42 text

Noneにするなら Secure 属性が必要 ● Cookie送信をやり切るために、一旦 None を指定してみる ● SameSite=None は Secure と一緒に使う必要がある(※1)ので、ブラウザは クッキーを送ってくれない ● (SameSite 関係なく Secure 属性は付けた方が良いと思う) ※1: https://developers.google.com/search/blog/2020/01/get-ready-for-new-samesitenone-secure?hl=ja

Slide 43

Slide 43 text

クッキーの送受信を確認

Slide 44

Slide 44 text

SameSite=None は使いたくない ● None は CSRF の脅威にさらされる ● ブラウザのデフォルトは Lax であり、リンクの遷移を除いてクロスサイトに クッキーを送らない ● クロスサイトを諦める必要がある?

Slide 45

Slide 45 text

Domain vs Origin vs Site ● Domain: サブドメイン + メインドメイ ン + トップレベルドメイン ● Origin: プロトコル + Domain + ポート ● Site: Domain のeTLD+1 ● 例: https://developer.mozilla.org/ja/docs/ と https://support.mozilla.org/ja/ は、Siteが一致していて、Origin は異な る。 便利画像 https://blog.jxck.io/entries/2023-12-21/same-site.html

Slide 46

Slide 46 text

SameSite 属性の挙動 ● None: クロスサイトにクッキーを送れる ● Lax: クロスサイトに送らないが、リンクをたどるときは送れる ● Strict: 同一サイトのみ送れる

Slide 47

Slide 47 text

SameSite=Lax したとき、ローカルホストでの開発が大変 ● http://localhost:8080 から https://ojisan.io へクッキーを使って認証ができ るか ● Siteが違うのでできない

Slide 48

Slide 48 text

SameSite=Lax を使うためには ● アクセスするサイトを揃える ● プロキシーを使う

Slide 49

Slide 49 text

実URLアクセスでローカルホストに繋ぐ ● /etc/hosts ファイルを書き換えると、そのURLでアクセスすると対応するサ イトに飛ぶようになる

Slide 50

Slide 50 text

CORS や secure 属性の制約を考える ● hosts を使ったとしても、実URLは HTTPS なので localhost の名前解 決をしただけでは疎通ができない ● ローカルホストの HTTPS 化が必要

Slide 51

Slide 51 text

ローカルで HTTPS サーバーを起動するためには ● ルート証明書となるオレオレ証明書を発行 ● ルート証明書をマシンに登録する ● ルート証明書を使ってサーバー証明書を作る ● HTTPSをサポートしているWebサーバーにサーバー証明書を読み込む ● ルート証明書を持っているマシンからそのサーバーに対してHTTPS通信ができ る

Slide 52

Slide 52 text

HTTPS通信の仕組み ● ユーザーがサイトにアクセスすると証明書が降ってくる ● 証明書を検証できると、その証明書に書かれている相手と通信できていること が保証される(ドメイン実在の証明) ● 鍵交換(暗号化通信)

Slide 53

Slide 53 text

降ってくる証明書を信用していい理由 ● 証明書は証明書を証明する証明書によって証明されている(ほにゅ?) ● 証明書にはCSR(証明書を発行した人の情報)が含まれており、認証局によっ てそのCSRを秘密鍵で署名されている ● 証明の連鎖の頂点にあるルート証明書は皆さんの手元のマシンの中に入ってい て、このルート証明書を使って検証できる。ルート証明書には認証局が使った 秘密鍵に対する公開鍵が含まれている

Slide 54

Slide 54 text

ルート証明書はなぜ信頼できるか ● 物理的に人手を使って運用する ● see: https://contents.nii.ac.jp/sites/default/files/2020-02/txt5_0.pdf

Slide 55

Slide 55 text

ルート証明書を手に入れる必要がある ● ブラウザからHTTPSにアクセスする以上、これから作るHTTPSサーバーの証 明書を作る・検証するためのルート証明書が必要 ● 幸い、自分で生成して自分でマシンに登録する方法がある(オレオレ証明書)

Slide 56

Slide 56 text

mkcert の使い方 ● mkcert ○ 自己認証局用の証明書・秘密鍵を作る mkcert -install ○ ルート証明書としてマシンに登録 ○ サーバー証明書を作る mkcert example.com ○ サーバーに生成された証明書と秘密鍵を読み込ませる (サーバー側の機能として大体持ってる、 https://nginx.org/en/docs/http/configuring_https_s ervers.html ● コマンドには sudo が必要 ● マシンに mkcert の証明書が作られてしまう ● 意味を分からずに叩いて良いコマンドではない!

Slide 57

Slide 57 text

Next.js の開発サーバーが HTTPS で立つらしい ● `next dev --experimental-https` ● 開発サーバーを立てようとするとマ シンのパスワードを求められる ● mkcert がインストールされる ● ルート証明書が登録される ● サーバーを落としてもルート証明書 は残り続ける ● (あまり触りたくないな...) https://vercel.com/guides/access-nextjs-localhost-https-certificate-self-signed

Slide 58

Slide 58 text

LaxのためにローカルのURL書き換え、絶対ちがう!!! ● 元々 SameSite=Lax で提供されるログインクッキーを使って、ローカルホス トで開発をしたかったのが始まり ● そのために sudo mkcert しますか? ● そのために sudo vim /etc/hosts しますか? ● sudo 使いますか?マシンの設定を直接書き換えますか? ● 開発終了後にちゃんと戻しますか? ● もしうっかりなにかやらかしたらリカバリーできますか? 怖

Slide 59

Slide 59 text

プロキシーを使う ● SameSite による問題は、ブラウザがリクエストする相手のURLと、そのリク エストを発生させるURLでサイトが異なるから ● 双方をどうにかして揃えるためにプロキシーを利用できる。

Slide 60

Slide 60 text

Vite の web proxy が便利 ● /api へのアクセスを全部、https://api.ojisan.dev に変換してくれる。 ● つまり、叩くAPIのURLを全部、localhost:3000/api にしておけば、本番URL にプロキシしてくれる ● VITE_API_ORIGIN=http://localhost:3000/api などとしてAPIの通信箇所を 自分自身のURLにしておけば、SameSiteとなる // vite.config.js export default { server: { proxy: { "/api": { target: "https://api.ojisan.dev", changeOrigin: true, secure: false, rewrite: (path) => path.replace(/^\/api/, ""), }, }, }, };

Slide 61

Slide 61 text

みなさんはどうしています?

Slide 62

Slide 62 text

時間が余ったらコーナー 個人的な疎通の恨みを喋るよ

Slide 63

Slide 63 text

HSTS とその強制 ● HTTP の Strict-Transport-Security レスポンスヘッダー (しばしば HSTS と 略されます) は、ウェブサイトがブラウザーに HTTP の代わりに HTTPS を用 いて通信を行うよう指示するためのものです。 https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Strict-Transport-Security ● .app ドメインと .dev ドメインは HSTS が強制される!!!! ● 何かの IPアドレスに手持ちの appドメインやdevドメインを貼って疎通しよう とすると、疎通相手が HTTPS 対応しないと疎通できない ● ブラウザからは疎通できないのに、cURL だと疎通できる奇妙な状況になるの も芸術点が高い ● 今日の実装例紹介で急に vercel の app ドメインから ojisan.io に変わったの はこれが理由です。(ちなみに cfw 逃げようとしましたが cf も .dev です)

Slide 64

Slide 64 text

Varyを見てくれないCDN ● Varyをちゃんと見てくれるのは Fastly くらいだと思っている ● CDNを通した際、Vary を見てないがために Next.js で Linkをふむと、RSC 丸 見えなんていうバグもあった ○ https://github.com/vercel/next.js/issues/48569

Slide 65

Slide 65 text

cache-control: no-store でもキャッシュするCDN ● 名指しはしませんが、CDN によっては no-store だけならキャッシュするものが ある ● 仕様違反かどうかは Cahce解体新書を読ん で確かめよう ● CDNを使うのは疎通段階、下手したら本番 環境まで使わないかもしれないので、検知 が遅れがち https://zenn.dev/jxck/books/cache-anatomia

Slide 66

Slide 66 text

通らない STUN ● WebRTCを実務で使うには STUN ではなく TURN を使うのが定石だとは思う が、TURN サーバー契約する決済が降りず、偶然 STUN だけで事足りていたの で STUN のまま実装していた ● 客先に納品後しばらくしてとある携帯事業者だけ繋がらないというバグ報告を 受けた ● QAの段階で wifi に繋いでOSごとのQAはしていたが、4G回線の比較はしてい なかった ● TURN 使おう、契約回線もQA事項に入れよう

Slide 67

Slide 67 text

modheader の消し忘れ ● 動作確認のために authorization を書き換える ● これで GitHub, Twitter, ChatGPT を開くと、ログインはできているのにコン テンツのロードができない謎挙動を楽しめる ● 使ったら戻そう || フィルター使おう