Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
【toranoana.deno#11】freshで普通のWebアプリを作る
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
虎の穴ラボ株式会社
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 虎の穴ラボ株式会社
Tailwind CSSとAtomic Designで実現する効率的な Web 開発の事例
toranoana
1
660
Denoについて、同人誌記事を出しました+update
toranoana
0
220
【虎の穴ラボ Tech Talk #2】プロンプトエンジニアリング
toranoana
0
160
20241121_[TechTalk#2]虎の穴ラボでのLLMについて取り組み紹介
toranoana
0
160
社内チャットへRAG導入した話(Tech Talk #2)
toranoana
0
230
Deno Deploy で Web Cache API を 使えるようになったので試した知見
toranoana
1
730
【虎の穴ラボ Tech Talk】虎の穴ラボTech Talk説明資料
toranoana
0
500
虎の穴ラボ Tech Talk_CDKでFargate環境構築
toranoana
1
560
虎の穴ラボスキルアップ支援制度の利用例
toranoana
0
11k
Other Decks in Technology
See All in Technology
2026 TECHFRESH 畢業分享會 - AI-Native 重塑軟體工程與虛擬講師
line_developers_tw
PRO
0
830
Android の公式 Skill / Android skills
yanzm
0
130
Socrates × Looker 〜セマンティックレイヤーで進化するデータ分析エージェント〜
hanon52_
3
2.1k
Disciplined Vibes: Scaling AI-Assisted Engineering
sheharyar
0
130
AI駆動開発を通して感じた、 AI時代のデザイナーの役割変化
whisaiyo
0
260
非定型業務をAI slackbotで自動化する ~ 社内要望を自動壁打ちするbotを作った ~/automating-ad-hoc-work-with-ai-slackbot
shibayu36
0
610
2026TECHFRESH畢業分享會 - AI 時代的人生存檔點
line_developers_tw
PRO
0
830
Building applications in the Gemini API family.
line_developers_tw
PRO
0
3.1k
小さくはじめるSLI/SLO ~育てながら組織に定着させる実践知~ / Starting Small with SLI/SLOs: Building Adoption Through Continuous Growth
nari_ex
6
1.8k
10倍の生産性を実現するAI駆動並列エージェントのすべて
kumaiu
5
1.3k
あなたの AI ワークスペースに、 専門コーダーを連れてくる - Amazon Quick Desktop 最新情報
kawaji_scratch
1
130
【Cyber-sec+】経営層を"動かす"ための考え方
hssh2_bin
0
130
Featured
See All Featured
Speed Design
sergeychernyshev
33
1.8k
Information Architects: The Missing Link in Design Systems
soysaucechin
0
970
Testing 201, or: Great Expectations
jmmastey
46
8.2k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
25
2k
The Pragmatic Product Professional
lauravandoore
37
7.3k
End of SEO as We Know It (SMX Advanced Version)
ipullrank
3
4.2k
How to optimise 3,500 product descriptions for ecommerce in one day using ChatGPT
katarinadahlin
PRO
1
3.6k
Effective software design: The role of men in debugging patriarchy in IT @ Voxxed Days AMS
baasie
0
400
Code Review Best Practice
trishagee
74
20k
Game over? The fight for quality and originality in the time of robots
wayneb77
1
200
Stewardship and Sustainability of Urban and Community Forests
pwiseman
0
220
Navigating the Design Leadership Dip - Product Design Week Design Leaders+ Conference 2024
apolaine
1
350
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