Node.jsの認証はJWT(JSON Web Token)だったのでDenoも同じようにできるみたいなのでやってみる。認証用のDBはMongoDBになります。
前回のMongoDBに認証を加える形になっています。認証とデータをおこなうために毎回、MongoDBの設定を書くのはおかしいのでMongoDBの接続などを外だししました。
前提
このプログラムはログインと認証をおこなっているがユーザーの作成はやっていない。 ユーザー情報を作成する時にはハッシュ化してパスワードが必要になる。以下で作成できる
import * as bcrypt from "https://deno.land/x/bcrypt/mod.ts"; const hash = await bcrypt.hash("password"); console.log(hash)
実行
$ deno run --allow-net --unstable hash.ts $2a$10$PJ5sSNKKCWnEXJgcmk6Xou7F76G3WqzKYjXSFn1G.O.Ww/U.oeCPC
JWTとは
JWTはJSON Web Tokenの略で、特徴は下記の2点です。
- コンパクト:tokenという形でURLのクエリパラメーターやPOSTのパラメーター、HTTP Headerに含む事ができる等、非常に小さいサイズで情報をやり取りすることができます。
- 自己完結:必要な情報を全て、tokenという形で含めることができます。
JWTは3つの要素から成り立ちます。
- Header
- Payload
- Signature
この要素を.で繋ぐことにより1つのtokenとして扱います
詳しくは
を見てほしいです
プログラム構成
├─routes.ts (ルーティング用) ├─server.ts (mainの起動プログラム) ├─denon.json (denonという監視プログラムの設定ファイル) ├─.env (DBの接続情報) ├─mongodb.ts (MongoDBの接続プログラム) │ └─controllers ├─ auth.ts (ユーザーの認証) └─ products.ts(productsのデータ取得プログラム)
全プログラムを見たい場合はgithubを参照してください
事前作業
denon install
変更時に自動的に再起動してくれるツールのインストール。環境設定とかも楽です。
deno install --allow-read --allow-run --allow-write -f --unstable https://deno.land/x/denon/denon.ts
実行
denon start
Routes
//認証なし GET /api/v1/products GET /api/v1/products/:id POST /api/v1/products PUT /api/v1/products/:id DELETE /api/v1/products/:id //認証 POST /login POST /auth //認証あり GET /api/v2/products GET /api/v2/products/:id POST /api/v2/products PUT /api/v2/products/:id DELETE /api/v2/products/:id
認証する場合はAuthorization: BearerというヘッダーにJWTのトークンをセットします。
ソース解説
server.ts
import {Application,Router} from "https://deno.land/x/oak/mod.ts"; import router from './routes.ts' const port = 5000 const app = new Application() app.use(router.routes()) app.use(router.allowedMethods()) console.log(`Server Running on port ${port}`) await app.listen({port })
起動用のプログラムなのでルーティング用のファイルを読んでいて適用させているだけです。
mongodb.ts
import { config } from "https://deno.land/x/dotenv/mod.ts"; import { MongoClient } from "https://deno.land/x/mongo@v0.7.0/mod.ts"; //環境変数 const ENV_PATH = '.env';//deno runしたところをカレントディレクトリになるみたい const config_env:any = config({ path: ENV_PATH }); const user:string = config_env.mongo_user const password_data:string = config_env.mongo_password const host:string = config_env.mongo_host const port_num:number = config_env.mongo_port const db_name:string = config_env.mongo_db //mongodb://<dbuser>:<dbpassword>@<host>:<port>/<db> const url:string = 'mongodb://' + user + ':' + password_data + '@' + host + ':' + port_num + '/' + db_name //mongoDB接続 console.log("mongodb connection start") const client_mongodb = new MongoClient() client_mongodb.connectWithUri(url) //DBの設定 const mongodb = client_mongodb.database(db_name) export default mongodb;
- MongoDBの接続用のプログラムでexportする事で他のプログラムでも使えるようにしています。
- コレクションは使う前に設定する方式にしています
denon.json
{ "$schema": "https://deno.land/x/denon/schema.json", "scripts": { "start": { "cmd": "deno run server.ts", "allow": [ "env", "read", "write", "net", "plugin" ], "unstable": true } }, "logger": { "debug": true }, "watcher": { "interval": 350, "exts": ["js", "ts", "json"], "match": ["*.*"], "skip": ["*/.git/*" ,"README.md" ], "legacy": false } }
- 起動時の設定や監視ファイルの無視ファイルを設定しています。allowの設定とか多くて困る・・・この設定があると本当に楽~♪
auth.ts
長いの分割になります。全文をみたい場合はgithubのソースを参照してください。
ライブラリーの読み込み
import { config } from "https://deno.land/x/dotenv/mod.ts"; import { makeJwt, setExpiration, Jose, Payload } from "https://deno.land/x/djwt/create.ts" import { validateJwt } from "https://deno.land/x/djwt/validate.ts" import { Context } from "https://deno.land/x/oak/mod.ts"; import * as bcrypt from "https://deno.land/x/bcrypt/mod.ts"; import mongodb from '../mongodb.ts'
- "https://deno.land/x/dotenv/mod.ts"は環境変数を取得するためのプログラムです。
- "https://deno.land/x/djwt/create.ts"はJWTを作成するためのプログラムです。
- "https://deno.land/x/djwt/validate.ts"はJWTを認証するためのプログラムです。
- "https://deno.land/x/oak/mod.ts"はHTTPリクエストを処理するためのプログラムです。
- "https://deno.land/x/bcrypt/mod.ts"はパスワードをHASH化してくれるプログラムです。
- "../mongodb.ts"はMongoDBの接続情報です。
DBの設定と環境変数の設定
//DBの設定 const db = mongodb //環境変数 const ENV_PATH = '.env';//deno runしたところをカレントディレクトリになるみたい const config_env:any = config({ path: ENV_PATH }); const collection_name:string = config_env.mongo_users_collection const datas = db.collection(collection_name) const key = "xxxxxxsome-random-secret-keyxxxxxxxxx"; const header: Jose = { alg: "HS256", typ: "JWT", }
- DBの接続を取得してそれをもとにコレクションに接続しています
Keyはサーバサイド側でしか知らないsecret keyになります。これも普通は環境変数とかに入れると思う。
Headerは最低限下記の2つの要素から成り立ちます。
ログイン
// @desc login // @route POST /login const login = async ({ request, response }: { request: any , response: any }) => { if (!request.hasBody){ response.status = 400 response.body = { success: false, msg: 'No Data' } }else { const body = await request.body() //console.log(body) //console.log(body.value.username) //console.log(body.value.password) //const hash = await bcrypt.hash(body.value.password); //console.log(hash) const data = await datas.find({ "username": body.value.username }); //判定が微妙な感じ・・・ if ( data.toString() ===""){ console.log("mongodb no data") response.status = 422; response.body = { message: 'Invalid username or password' }; } else { //ループでもってくる以外のやり方がわからずこの形になった for (var it in data) { const item = data[it] var db_password = item.password var db_id = item.id } if (await bcrypt.compare(body.value.password, db_password)) { const payload: Payload = { iss: body.value.username, exp: setExpiration(new Date().getTime() + 3600000), } // Create JWT and send it to user const jwt = makeJwt({key, header, payload}); if (jwt) { response.status = 200; response.body = { id: db_id, username: body.value.username, jwt, } } else { response.status = 500; response.body = { message: 'Internal server error' } } return; } else { console.log("password compare false") response.status = 422; response.body = { message: 'Invalid username or password' }; } } } }
- request.hasBodyはデータの有無をチェックしています。
- data = await datas.find({ "username": body.value.username }) はMongoDBからUser情報を取得しています。
- お恥ずかしいのですがMongoDBからデータを取得して変数にいれる方法がわからず・・・・ループで取得するという形にしています。たぶん違う
for (var it in data) { const item = data[it] var db_password = item.password var db_id = item.id }
- await bcrypt.compare(body.value.password, user.password)は入力されたパスワードとハッシュ化されたパスワードが一致(compare)しているのかチェックしています。
- Payloadはサーバサイドとクライアントサイドで共有したい情報になります。今回はユーザー情報と有効時間です
const payload: Payload = { iss: user.username, exp: setExpiration(new Date().getTime() + 3600000), }
- const jwt = makeJwt({key, header, payload});はシークレットキーとヘッダーとペイロードをもとにJWTを作成しています。ちなみにこの変数はお決まりらしくてkeyをSecretKeyに変更するとエラーになりました。
- リクエストデータのJSONのサンプルは以下の形です
{ "username": "masalib", "password": "password" }
認証
2つのプログラムで構成されています。authに関しては実はなんでもいいです。 2つに分かれている理由としては認証する部分は他のプログラムでも使われるので分けています。これは次にやる予定のMongoDBでの認証で記載します
// @desc auth // @route POST /auth const auth = async ({ request, response }: { request: any , response: any }) => { response.status = 200 response.body = { success: true, data:"auth success" } } const authMiddleware = async (ctx: Context, next: any) => { const headers: Headers = ctx.request.headers; // Taking JWT from Authorization header and comparing if it is valid JWT token, if YES - we continue, // otherwise we return with status code 401 const authorization = headers.get('Authorization') console.log("authMiddleware authorization:" + authorization) if (!authorization) { console.log("authorization Noting" ) ctx.response.status = 401; return; } const jwt = authorization.split(' ')[1]; console.log("jwt:" + jwt ) if (!jwt) { console.log("jwt Noting" ) ctx.response.status = 401; return; } if (await validateJwt(jwt, key, {isThrowing: false})){ await next(); return; } console.log("validateJwt false" ) ctx.response.status = 401; ctx.response.body = {message: 'Invalid jwt token'}; }
- 認証はヘッダーにあるという形にしています。POSTの中にいれてもいいと思う。
curl --location --request POST 'http://localhost:5000/auth' \ --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtYXNhbGliIiwiZXhwIjoxNTkxNTIyODM3NTgwfQ.-UNfi94e2kzuMq9BlIiwVww2masIiF5ax0fgCyW3oPM' \ --data-raw ''
- const jwt = authorization.split(' ')[1];は0番は違うデータが入っているので無視しています。
- if (await validateJwt(jwt, key, {isThrowing: false})){ でJWTの認証をおこなっています。
products.ts
//DBの設定 const db = mongodb //コレクションの設定 const ENV_PATH = '.env';//deno runしたところをカレントディレクトリになるみたい const config_env:any = config({ path: ENV_PATH }); const collection_name:string = config_env.mongo_products_collection const datas = db.collection(collection_name)
DBコレクションの設定の部分以外は下記の記事と同じです
感想
- MongoDBから取得して変数にいれる所が間違えていると思うが動くのはできた。
- 認証付きのデータ取得が思いのほか簡単にできた。認証はアプリでは一般的に普通にあるので今後に使えそう~♪