Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
【toranoana.deno#11】freshで普通のWebアプリを作る
Search
虎の穴ラボ株式会社
February 20, 2023
Technology
1.3k
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
【toranoana.deno#11】freshで普通のWebアプリを作る
toranoana.deno#11での発表資料です
https://yumenosora.connpass.com/event/272012/
虎の穴ラボ株式会社
February 20, 2023
More Decks by 虎の穴ラボ株式会社
See All by 虎の穴ラボ株式会社
関西Ruby登壇資料
toranoana
0
33
Tailwind CSSとAtomic Designで実現する効率的な Web 開発の事例
toranoana
1
710
Denoについて、同人誌記事を出しました+update
toranoana
0
240
【虎の穴ラボ Tech Talk #2】プロンプトエンジニアリング
toranoana
0
170
20241121_[TechTalk#2]虎の穴ラボでのLLMについて取り組み紹介
toranoana
0
180
社内チャットへRAG導入した話(Tech Talk #2)
toranoana
0
250
Deno Deploy で Web Cache API を 使えるようになったので試した知見
toranoana
1
770
【虎の穴ラボ Tech Talk】虎の穴ラボTech Talk説明資料
toranoana
0
530
虎の穴ラボ Tech Talk_CDKでFargate環境構築
toranoana
1
700
Other Decks in Technology
See All in Technology
Mastering Agentic Development: Harness Engineering for Effective Coding Agents
konippi
3
660
Argo CDとAtlantisで実現するインフラ管理のセルフサービス化──小規模SREチームで支えるプラットフォーム
cassius7
0
250
Apache Iceberg が拓く AI 時代のオープンレイクハウス
tomtanaka
0
200
Databricksメトリクスビューはじめてのもくもく会
taka_aki
0
140
React Nativeでの OTA Updateって、 どう説明する?
ichiki1023
0
120
AWS FinOps Agent 結局何が得意なの?
siromi
0
220
猫でもわかるKiro Web
kentapapa
1
160
1人アドミンな私はAWSアカウント申請をSlackで完結したい!
ysuzuki
0
100
あなたの知らないAmazon VPC Route Server/Amazon VPC Route Server you don't know about
masakiokuda
0
160
TiDBファミリーにDWHが新登場!! TiDB最新情報 / TiDB update 202609
yoshiakiyamasaki
0
160
顧客の成果創出とプロダクトの成長を 両立するためのFDE
sansantech
PRO
0
520
セルフサービスのオブザーバビリティ基盤をOpenTelemetryで作る / Building a Self-Service Observability Platform with OpenTelemetry
ymotongpoo
3
490
Featured
See All Featured
Designing Experiences People Love
moore
143
24k
Producing Creativity
orderedlist
PRO
348
41k
How to Think Like a Performance Engineer
csswizardry
28
2.8k
RailsConf 2023
tenderlove
30
1.6k
Building Flexible Design Systems
yeseniaperezcruz
330
41k
svc-hook: hooking system calls on ARM64 by binary rewriting
retrage
2
600
How Software Deployment tools have changed in the past 20 years
geshan
2
34k
Un-Boring Meetings
codingconduct
0
430
Scaling GitHub
holman
464
140k
How to Get Subject Matter Experts Bought In and Actively Contributing to SEO & PR Initiatives.
livdayseo
0
200
Leo the Paperboy
mayatellez
10
2.3k
Designing for humans not robots
tammielis
254
26k
Transcript
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. freshで普通のWebアプリを作る
虎の穴ラボ 藤原 佳顕 1
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. アジェンダ •
概要 • freshでFormを扱う • freshでcookieセッションを扱う • freshでフロントエンドを扱う • DBを扱う 2
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. 自己紹介 3
• 名前 ◦ 藤原佳顕 • 仕事 ◦ FantiaとかCreatiaとか社内アプリとか • 好み ◦ Clojure、Rust • 趣味 ◦ 格闘ゲーム ▪ Melty Blood、Guilty Gear ◦ STG(ダライアスとか)も好き
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. 概要 4
• 新しい言語とかフレームワークを触るときにテンプレとしてWebアプリを作ってみてる のでDeno + Freshでもやってみます ◦ Clojureでやったときのもの ◦ https://gitlab.com/y-fujiwara/earth-clj • 基本的な機能としては以下 ◦ FormでのPOST ◦ ログイン処理及びセッション管理 ◦ フロントエンドで動く地図表示 ◦ (場合によってはWebSocket)
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. freshでFormを扱う 5
• freshのroutesにあるものはssrされるので普通にformタグをかけばそのまま使えるはず function Login() { return ( <form method="post" action="/api/login"> <input type="text" name="username" /> <input type="password" name="password" /> <button type="submit">Submit</button> </form> ); } export default function Home({ data }: PageProps<Data>) { return ( <Layout> <Head> <title>Earth Deno</title> </Head> <div> <div> You currently {data.isAllowed ? "are" : "are not"} logged in. </div> <Login /> </div> </Layout> ); }
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. freshでFormを扱う 6
• CSRF対策をどうするか? ◦ トークン埋め込み ▪ Formにトークンを埋め込み、Cookieやセッションの内容との一致をもってリクエスト成功と する方法 ▪ よく見られる一般的な対応 ◦ 独自ヘッダー ▪ X-Requested-With: XMLHttpRequestなどのカスタムヘッダーを埋め込むことで、preflight requestを強制し、かつ上記カスタムヘッダーがなければ応答しないようにする ▪ APIならこちらでもOK ◦ 今回は古典的な物を作りたかったのでよく使われるトークン埋め込みを採用
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. freshでFormを扱う 7
• どうトークンを生成するか? • いわゆるフルスタックフレームワークには大抵デフォルトで備わっている ◦ 今回はRuby on Rails(RoR)を参考にする ◦ 参考:https://techracho.bpsinc.jp/hachi8833/2021_11_26/46891 • RoRではヘッダーに埋め込むcsrf_meta_tagsヘルパーおよび、controller側でのprotect_from_forgery フィルターでCSRFトークンの生成と検証を行っている • 検証用トークン本体の生成ルール ◦ SecureRandom.base64で生成される安全なランダム文字列をセッションにセット ◦ 何度も生成しないようにセッションに存在すれば生成されない
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. freshでFormを扱う 8
• Formに埋め込むトークンの生成 ◦ セッションに入れたものと同じものを入れてもいいけど・・・ ▪ 実際の実装 ◦ コメント抜粋 ▪ The masking is used to mitigate SSL attacks like BREACH. ▪ SSLに対するBREACH攻撃のためにマスキングされている • https://docs.digicert.com/ja/certcentral/certificate-tools/discovery-user-guide/tls-ssl-endpoi nt-vulnerabilities/breach.html ◦ HTTPの圧縮機能が有効なときに、HTTPSでの暗号化が有効であっても中身の情報が推測できてしまう ◦ 固定値のCSRFトークンを埋め込んでいると値が推測できてしまう ▪ したがって、Formに埋め込むトークは毎回ランダムかつ、セッションの値との整合性をチェックできるもの でなければいけない
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. freshでFormを扱う 9
• RoRではもとのトークンと同じ長さのランダム文字列とのxorを取って暗号化しているので真似する • まずはもとのトークン生成 ◦ 乱数生成後、Base64化 function csrfToken(): string { return encode(crypto.getRandomValues(new Uint8Array(AUTHENTICITY_TOKEN_LENGTH))); }
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. freshでFormを扱う 10
• masked token生成 function maskedToken(token: string): string { const bytes = crypto.getRandomValues( new Uint8Array(AUTHENTICITY_TOKEN_LENGTH), ); const tokenBytes = decode(token); const ret = bytes.map((b, idx) => { return tokenBytes[idx] ^ b; }); const maskedBytes = new Uint8Array(bytes.length + ret.length); maskedBytes.set(bytes); maskedBytes.set(ret, bytes.length); return encode(maskedBytes); }
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. freshでFormを扱う 11
• これらまとめて_middlewareとすることで、各ページロードのたびに新しいトークンが生成できるようにします const AUTHENTICITY_TOKEN_LENGTH = 32; export async function handler( req: Request, ctx: MiddlewareHandlerContext<ContextState>, ) { const cookie = await getDecryptedSessionCookie(req); let realToken = cookie?.csrfToken; let cookieHeaders = null; if (typeof realToken !== "string") { realToken = csrfToken(); cookieHeaders = await buildSessionCookieHeader(req, { csrfToken: realToken, }); } ctx.state.maskedCsrfToken = maskedToken(realToken as string); const resp = await ctx.next(); if (cookieHeaders) { resp.headers.set("Set-Cookie", cookieHeaders.get("Set-Cookie") as string); } return resp; }
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. freshでFormを扱う 12
• 最終的なLogin Form export const handler: Handlers<any, ContextState> = { async GET(req, ctx) { const cookies = await getDecryptedSessionCookie(req); return ctx.render!({ isAllowed: cookies?.userId === "deno", csrfToken: ctx.state.maskedCsrfToken, }); }, }; function Login(props: FormProps) { return ( <form method="post" action="/api/login"> <input type="hidden" value={props.csrfToken} name="csrfToken" /> <input type="text" name="username" /> <input type="password" name="password" /> <button type="submit">Submit</button> </form> ); } export default function Home({ data }: PageProps<Data>) { return ( <Layout> <Head> <title>Earth Deno</title> </Head> <div> <div> You currently {data.isAllowed ? "are" : "are not"} logged in. </div> {!data.isAllowed ? <Login csrfToken={data.csrfToken} /> : ( <> <div> <a href="/auth">User Page</a> </div> <Map /> <a href="/logout">Logout</a> <Counter start={3} /> </> )} </div> </Layout> ); }
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. freshでFormを扱う 13
• チェック処理 ◦ Base64関連でエラーになることもあるので、検証失敗は一括で例外扱い export const handler: Handlers = { async POST(req) { const form = await req.formData(); const maskedToken = form.get("csrfToken") || ""; const cookie = await getDecryptedSessionCookie(req); const sessionToken = cookie?.["csrfToken"] as string; try { validCsrfToken(maskedToken as string, sessionToken); } catch(e) { console.error(e); return new Response(null, { status: 403 }); } // 略 }, };
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. freshでFormを扱う 14
• タイミングアタック(トークンの検証時間でトークンそのものを推測しようとする攻撃)を防ぐために単純な文字比較にはしな い function validCsrfToken(maskedToken: string, sessionToken: string) { const unmaskToken = unmask(maskedToken); return compareToken(unmaskToken, sessionToken); } function unmask(maskedToken: string): Uint8Array { const maskedTokenBytes = decode(maskedToken); const oneTimePad = maskedTokenBytes.subarray(0, 32); const encCsrfToken = maskedTokenBytes.subarray(32); const ret = oneTimePad.map((b, idx) => { return encCsrfToken[idx] ^ b; }); return ret } function compareToken(unmaskBytes: Uint8Array, sessionToken: string) { const sessionTokenBytes = decode(sessionToken); if (unmaskBytes.byteLength !== sessionTokenBytes.byteLength) { throw new Error("CSRFトークンの長さが異なります。"); } const ret = sessionTokenBytes.every((byte, idx) => { return !(byte ^ unmaskBytes[idx]); }); if (!ret) { throw new Error("CSRFトークンが異なります。"); } }
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. ログイン処理及びセッション管理 15
• 基本的に以前の発表でお話したことと同様のことを行います ◦ https://www.slideshare.net/toranoana-lab/github-apifresh • 基本的にはCookieセッションを扱います • Cookieの内容はcryptoを使って暗号化します(AES-GCM) • 今回は、ログインIDの他、先述のCSRFトークンを保持する必要があるので、単一の値での上書きは NGです ◦ 取得→merge→再追加 • 2022/11/4のDenoブログに似たような内容があったりする ◦ https://deno.com/blog/setup-auth-with-fresh • 今回の実装は基本的に上記+Cookieの暗号化をしているだけ
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. ログイン処理及びセッション管理
16 • ほぼサンプル通り export const handler: Handlers = { async POST(req) { // TODO: csrfトークン検証処理 const form = await req.formData(); if ( form.get("username") === USERNAME && form.get("password") === PASSWORD ) { const headers = await buildSessionCookieHeader(req, { userId: "deno" }); headers.set("location", "/"); return new Response(null, { status: 303, headers }); } else { return new Response(null, { status: 403, }); } }, };
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. フロントエンドで動く地図表示 17
• ログイン後の画面に地図を表示したい • 利用ライブラリ ◦ leaflet.js https://leafletjs.com/ • Deno用のライブラリはなさそう • 基本的にDOM関連ライブラリなのでクライアントサイドだけで動く • 実装どうするか ◦ islandsで全部やる(dynamic importをうまく使う) ◦ ライブラリは使わずに、cdn経由でファイル読み込んで使う ▪ グローバル変数に対して型定義が必要になる
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. フロントエンドで動く地図表示
18 • islandsでdynamic importした import { useEffect, useRef, useState } from "preact/hooks"; export function MapContainer() { const mapContainer = useRef<HTMLDivElement>(null); const [map, setMap] = useState<L.Map | null>(null); useEffect(() => { if (typeof document === "object" && mapContainer && mapContainer.current) { import("leaflet").then((L) => { const m = L.map("map").setView([51.505, -0.09], 13); L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>', }).addTo(m); setMap(map); }); } }, []); return <div id="map" style={{ height: "180px" }} ref={mapContainer}></div>; }
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. フロントエンドで動く地図表示
19 • dynamic importしたライブラリはfreshがよしなに制御してくれる
Copyright (C) 2021 Toranoana Inc. All Rights Reserved. まとめ •
denoで普通のWebアプリを作ってみました • セキュリティ関連の実装はできれば自作したくないので、FWに混同されていると嬉しいなと 思いました • DOM依存系のnpmモジュールは扱いに注意が必要だなと思いました ◦ islandsであってもエラーになるパターンがある 20