SQLite をデータベースに使用し、アプリケーションを作ってみたくなった(単純にお安く済みそうだから)。
SQLite は、ネットワーク機能は持っていないので、そこは API サーバーとしてフォローする必要があった。
認証機能として、OAuth 2.0 Client Credentials Grant を実現する簡単なアプリケーションを書いたメモ。
参考
実装
ディレクトリの中身は次の通り。
1 2 3 4 5 6 7 8 9
| . |-- .env |-- app.ts |-- clients.ts |-- config.ts |-- deps.ts |-- orm.ts |-- test.db `-- validates.ts
|
ソースコードは次の通り。
./.env1 2
| BASIC_SECRET = <文字列> SALT = <文字列>
|
./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
| import { Application, Router } from "./deps.ts"; import { orm, Token } from "./orm.ts"; import { accessResourceRequestCheck, accessTokenRequestCheck, checkBasicAuth, } from "./validates.ts";
const router = new Router(); router.get("/resource/users", (context) => { const result = accessResourceRequestCheck(context.request); if (!result.status) return;
context.response.body = { users: [ { id: 1, name: "A" }, { id: 2, name: "B" }, ], }; }); router.post("/oauth/token", async (context) => { if (!checkBasicAuth(context.request)) return; const result = await accessTokenRequestCheck(context.request); if (!result.status) return;
const token = new Token(); token.clientId = result.clientId; token.token = crypto.randomUUID(); orm.save(token);
context.response.body = { access_token: token.token, token_type: "bearer", expires_in: 3600, }; });
const app = new Application();
app.use(router.routes()); app.use(router.allowedMethods());
await app.listen({ port: 8080 });
|
./clients.ts1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { createHash } from "./deps.ts"; import { config } from "./config.ts";
const clientSecret = "qwertyuiopasdfghjklzxcvbnm"
const hash = createHash("sha256"); hash.update(`${config.salt}-${clientSecret}-${config.salt}`);
interface Clients { [key: string]: { secret: string }; }
export const clients = { "1": { secret: hash.toString() }, } as Clients;
|
./config.ts1 2 3 4 5 6
| import "https://deno.land/std@0.139.0/dotenv/load.ts";
export const config = { basicSecret: Deno.env.get("BASIC_SECRET")!, salt: Deno.env.get("SALT")!, };
|
./deps.ts1 2 3 4 5 6 7 8 9 10
| export { Application, Request, Router, } from "https://deno.land/x/oak@v10.5.1/mod.ts"; export { createHash } from "https://deno.land/std@0.139.0/hash/mod.ts"; export { SSQL, SSQLTable, } from "https://deno.land/x/smallorm_sqlite@0.2.1/mod.ts";
|
./orm.ts1 2 3 4 5 6 7 8 9 10 11 12 13
| import { SSQL, SSQLTable } from "./deps.ts";
export class Token extends SSQLTable { clientId = ""; token = ""; expirationAt = new Date().getTime();
expire() { return new Date(this.expirationAt); } }
export const orm = new SSQL("test.db", [Token]);
|
./validates.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 78 79 80
| import { createHash, Request } from "./deps.ts"; import { config } from "./config.ts"; import { clients } from "./clients.ts"; import { orm, Token } from "./orm.ts";
interface AccessTokenRequestCheckResultFailure { status: false; } interface AccessTokenRequestCheckResultSuccess { status: true; clientId: string; }
type AccessTokenRequestCheckResult = | AccessTokenRequestCheckResultFailure | AccessTokenRequestCheckResultSuccess;
type AccessResourceRequestCheckResultFailure = AccessTokenRequestCheckResultFailure; type AccessResourceRequestCheckResultSuccess = AccessTokenRequestCheckResultSuccess; type AccessResourceRequestCheckResult = | AccessResourceRequestCheckResultFailure | AccessResourceRequestCheckResultSuccess;
export function accessResourceRequestCheck( req: Request ): AccessResourceRequestCheckResult { const auth = req.headers.get("authorization"); if (!auth) return { status: false }; const token = auth.split("Bearer ")[1]; if (!token) return { status: false };
const d = new Date(); d.setMinutes(d.getMinutes() - 10);
const t = orm.findMany(Token, { where: { clause: "token = ? AND expirationAt > ?", values: [token, d.getTime()], }, });
if (t.length !== 1) return { status: false };
return { status: true, clientId: t[0].clientId }; }
export async function accessTokenRequestCheck( req: Request ): Promise<AccessTokenRequestCheckResult> { const body = await req.body({ type: "form" }); const clientId = (await body.value).get("client_id"); const clientSecret = (await body.value).get("client_secret"); const grantType = (await body.value).get("grant_type");
if (grantType !== "client_credentials") return { status: false }; if (!clientId) return { status: false }; if (typeof clientId !== "string") return { status: false }; if (!clientSecret) return { status: false };
const client = clients[clientId];
const h = createHash("sha256"); h.update(`${config.salt}-${clientSecret}-${config.salt}`); const clientSecretHash = h.toString();
if (clientSecretHash !== client.secret) return { status: false };
return { status: true, clientId }; }
export function checkBasicAuth(req: Request): boolean { const auth = req.headers.get("authorization"); if (!auth) return false; const s = auth.split("Basic ")[1]; if (!s) return false; if (s !== config.basicSecret) return false; return true; }
|
SQLite で、時刻で where をするのが少し難しく Integer で処理するようにしてある。
たまには RFC 見ながら実装するのもいいなと思うなど。
ちゃんと実装できてるのか確認したい。
追々アプリを作っていきます。
ではでは。