Deno 用の CSRF トークンの検証モジュールを作ったのが約 1 カ月前。
「アプリで実際に使うことほとんど試していなかったな」と気が付いたので、作ってみます。
簡単に、POST するだけのアプリです。
参考
実装 - その前に -
今回使うdeno.land/x/sessions@v1.5.4、実は上手く動かないのを確認しています。
(今度プルリクを送っておきたい。プルリクは承認されたんですが、deno.land/x のモジュールはまだ更新されず。[2021/09/03])
なので、モンキーパッチ(という言い方で正しいのか?)的な方法で対応します。
deno.land/x/sessions@v1.5.4/src/frameworks/OakSession.js を見ると以下のようになっています。
deno.land/x/sessions@v1.5.4/src/frameworks/OakSession.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import Session from "../Session.js";
export default class OakSession extends Session { constructor(oakApp, store = null) { super(store || null);
oakApp.use(async (ctx, next) => { const sid = ctx.cookies.get("sid");
if (sid && (await this.sessionExists(sid))) { ctx.state.session = this.getSession(sid); } else { ctx.state.session = await this.createSession(); ctx.cookies.set("sid", ctx.state.session.id); }
ctx.state.session.set("_flash", {});
await next(); }); } }
|
書き足したコメントの通り、await が足りていないです,
なので、次のようにして使います。
./PatchedOakSession.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import Session from "https://deno.land/x/sessions@v1.5.4/src/Session.js";
export class PatchedOakSession extends Session { constructor(oakApp, store = null) { super(store || null);
oakApp.use(async (ctx, next) => { const sid = await ctx.cookies.get("sid");
if (sid && (await this.sessionExists(sid))) { ctx.state.session = this.getSession(sid); } else { ctx.state.session = await this.createSession(); ctx.cookies.set("sid", ctx.state.session.id); }
ctx.state.session.set("_flash", {});
await next(); }); } }
|
以降のアプリ本体では、この ./PatchedOakSession.js を使用します。
実装
deps.ts
./deps.ts1 2 3 4 5 6 7 8 9 10 11 12
| export { Application, Router } from "https://deno.land/x/oak/mod.ts"; export { RedisStore } from "https://deno.land/x/sessions@v1.5.4/mod.ts" export { render } from "https://deno.land/x/mustache/mod.ts"; export { computeHmacTokenPair, computeVerifyHmacTokenPair, } from "https://deno.land/x/deno_csrf@0.0.4/mod.ts"; export { getCookies, setCookie, } from "https://deno.land/std@0.106.0/http/cookie.ts"; export type { Cookie } from "https://deno.land/std@0.106.0/http/cookie.ts";
|
app.ts
ベタ書きですが、動作確認なので許してください。
./app.ts1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| import "https://deno.land/x/dotenv/load.ts"; import { Application, Router, render, computeHmacTokenPair, computeVerifyHmacTokenPair, getCookies, setCookie, RedisStore, } from "./deps.ts"; import { Cookie } from "./deps.ts";
import { PatchedOakSession } from "./PatchedOakSession.js";
const app = new Application(); const store = new RedisStore({ host: "redis", port: 6379, });
await store.init();
new PatchedOakSession(app, store);
const router = new Router();
const key = Deno.env.get("CSRF_KEY") as string;
router.get("/", async (context) => { console.log("get /"); const tokenPair = computeHmacTokenPair(key, 360);
const flash = await context.state.session.get("_flash"); const name = !flash.name ? "" : flash.name;
const body = render(Deno.readTextFileSync("./page/form.html"), { token: tokenPair.tokenStr, name, }); const cookie: Cookie = { name: "cookies_token", value: tokenPair.cookieStr }; setCookie(context.response, cookie);
context.response.body = body; });
router.post("/", async (context) => { console.log("post /");
const value = await context.request.body({ type: "form" }).value; const csrf_token = value.get("csrf_token"); const name = value.get("name"); const cookies = getCookies(context.request);
if (!csrf_token) { const body = render(Deno.readTextFileSync("./page/error.html"), {}); context.response.body = body; return; }
if (!computeVerifyHmacTokenPair(key, csrf_token, cookies.cookies_token)) { const body = render(Deno.readTextFileSync("./page/error.html"), {}); context.response.body = body; return; }
await context.state.session.flash("name", name);
context.response.redirect("/"); });
app.use(router.routes()); app.use(router.allowedMethods()); app.listen({ port: 8080 });
|
page/form.html
page/form.html1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <!DOCTYPE html> <html> <head> </head> <body> <form action="/" method="POST"> <input type="hidden" name="csrf_token" value="{{token}}" /> <input type="text" name="name" /> <button type="submit">送信</button> </form> {{#name}} <div>入力されたのは、{{name}}</div> {{/name}} </body> </html>
|
page/
page/error.html1 2 3 4 5 6 7 8 9
| <!DOCTYPE html> <html> <head>
</head> <body> <h1>Error</h1> </body> </html>
|
起動
以下コマンドで起動します。
1
| deno run --allow-net --allow-env --allow-read app.ts
|
アクセスするとフォームが表示されるの入力/送信。フォームの下に入力結果が表示されます。
開発者ツールでCookieを削除したり、フォーム中のトークンを書き換えなどしてフォーム送信すると、「ERROR」の表示に移ります。
作ったツールを使ったアプリの動作確認をしました。
動作確認したものの、可能なら oak 用の拡張として公開してみたいので、近いうちにチャレンジする予定です。
ではでは。