XXとしてる
2019年3月10日日曜日
Authenticate a Node.js API with JSON Web Tokensを読んだ。
### 初めに ユーザー認証を実装しようと思ったところ、 ログインに成功した後サーバーとクライアントの両側でどのようにログイン状態を確認するのかよくわかっていなかったので、 ネットサーフィンしていたら良さそうな解説サイトがあったので簡単に翻訳してみる。 Nuxt.jsの例を元にできそうだが、認証周りはセキュリティ的に少し慎重に作りたいので解説サイトがあって助かった。 [Authenticate a Node.js API with JSON Web Tokens](https://scotch.io/tutorials/authenticate-a-node-js-api-with-json-web-tokens) 以下、その簡約 ## 概要 予備知識として[The Ins and Outs of Token Based Authentication](https://scotch.io/bar-talk/the-ins-and-outs-of-token-based-authentication)と [JSON Web Tokens](https://scotch.io/tutorials/the-anatomy-of-a-json-web-token)を読んでほしい。 ## 開発環境 NodeとExpress、POSTmanを利用する。 作業手順は以下の通り、 1. 保護されたまたはされないルートを持つ。 1. ユーザーは名前とパスワードを使った認証に成功したらトークンを受け取る 1. ユーザーはクライアント側でそのトークンを格納し、サーバーへリクエストを送るときはそれも一緒に送る。 1. サーバー側は送られてきたトークンを検証し、有効なものであるならJSONデータを返す。 ルートについては以下の種類が出来上がる。 1. 通常のルート 1. トークンを検証するルートミドルウェア 1. 名前とパスワードを認証し、トークンを返すルート 1. 全ユーザーを取得する認証されたルート ## 下準備 プロジェクトルートの`server.js`をサーバーのエントリポイントとして開発していく。 プロジェクトの作成ができたら、以下のパッケージを追加する。 - express - body-parser - morgan - mongoose - jsonwebtoken ### Userモデル `app/models/user.js`にデータベース上のユーザーのテーブルを定義する。 ```js const mongoose = require('mongoose') const Schema = mongoose.Schema module.exports = mongoose.model('User', new Schema({ name: String, password: String, admin: Boolean })) ``` MongoDBを使う時は設定ファイルが必要になるのでそれも作成する。 ちなみにMongoDBのアカウントを作成し、データベースを構築する必要がある。 一応料金フリーのものがあるので試しやすい。 ```js //config.js module.exports = { 'secret': 'ilovescotchyscotch', 'database': 'mongodb://noder:noderauth&54;proximus.modulusmongo.net:27017/so9pojyN' } ``` ## サーバーの実装 ユーザーデータを使う準備ができたので、`server.js`に今回説明するものを全て書いていく。 大体以下の内容を持つようになる。 - アプリケーションの設定 - 基本的なルートの作成: `http://localhost:8080`がホームページになる。 - APIルートの作成 - `POST http://localhost:8080/api/authenticate` : 認証処理を行う - `GET http://localhost:8080/api` : トークンを持っていたらランダムに生成されたテキストを表示する - `GET http://localhost:8080/api/users` : トークンを持っていたら全ユーザー表示する 次のコードを元に機能を追加していく ```js // ======================= // get the packages we need ============ // ======================= var express = require('express'); var app = express(); var bodyParser = require('body-parser'); var morgan = require('morgan'); var mongoose = require('mongoose'); var jwt = require('jsonwebtoken'); // used to create, sign, and verify tokens var config = require('./config'); // get our config file var User = require('./app/models/user'); // get our mongoose model // ======================= // configuration ========= // ======================= var port = process.env.PORT || 8080; // used to create, sign, and verify tokens mongoose.connect(config.database); // connect to database app.set('superSecret', config.secret); // secret variable // use body parser so we can get info from POST and/or URL parameters app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); // use morgan to log requests to the console app.use(morgan('dev')); // ======================= // routes ================ // ======================= // basic route app.get('/', function(req, res) { res.send('Hello! The API is at http://localhost:' + port + '/api'); }); // API ROUTES ------------------- // we'll get to these in a second // ======================= // start the server ====== // ======================= app.listen(port); console.log('Magic happens at http://localhost:' + port); ``` ## テスト用のユーザー作成 `localhost:8080/setup`でテスト用のユーザーを作成する。 サンプルだからそのまま保存しているが、パスワードは決してそのままデータベースに保存しないこと ```js app.get('/setup', function(req, res) { // create a sample user var nick = new User({ name: 'Nick Cerminara', password: 'password', admin: true }); // save the sample user nick.save(function(err) { if (err) throw err; console.log('User saved successfully'); res.json({ success: true }); }); }); ``` ## サンプル用のユーザーを作ったので先にトークンなしで`http://localhost:8080/api`と`http://localhost:8080/api/users`を作る。 ```js // API ROUTES ------------------- // get an instance of the router for api routes var apiRoutes = express.Router(); // TODO: route to authenticate a user (POST http://localhost:8080/api/authenticate) // TODO: route middleware to verify a token // route to show a random message (GET http://localhost:8080/api/) apiRoutes.get('/', function(req, res) { res.json({ message: 'Welcome to the coolest API on earth!' }); }); // route to return all users (GET http://localhost:8080/api/users) apiRoutes.get('/users', function(req, res) { User.find({}, function(err, users) { res.json(users); }); }); // apply the routes to our application with the prefix /api app.use('/api', apiRoutes); ``` それでは本題の認証状況に応じてリクエストを許可したりしなかったりをできるようにしていく。 ## 認証処理 ### 認証とトークン作成 認証は上で書いた通り`POST http://localhost:8080/api/authenticate`で行う。 フォーム用のページは作っていないが、その代わりに[POSTman](https://www.getpostman.com/)というツールを使用する。 なお、バージョンアップなどでところどころ動かないところがあったので元記事より修正している。 (元記事は2015年に書かれている。コメントを見る限り) ```js // API ROUTES ------------------- // get an instance of the router for api routes var apiRoutes = express.Router(); // route to authenticate a user (POST http://localhost:8080/api/authenticate) apiRoutes.post('/authenticate', function(req, res) { const user = await User.findOne({name: req.body.name}) if (!user) { return res.json({ success: false, message: 'Authentication failed. User not found.' }); } // check if password matches if (user.password != req.body.password) { res.json({ success: false, message: 'Authentication failed. Wrong password.' }); } else { // if user is found and password is right // create a token with only our given payload // we don't want to pass in the entire user since that has the password const payload = { admin: user.admin }; var token = jwt.sign(payload, app.get('superSecret'), { expiresIn: 60 * 60 * 24 // expires in 24 hours }); // return the information including token as JSON res.json({ success: true, message: 'Enjoy your token!', token: token }); } }); ... ``` 認証に成功したら(コードの最後あたり)jsonwebtokenを使って認証用のトークンを作成している。 このトークンを使い権限の確認を行っていく。 生成に使用するアルゴリズムは`options.algorithm`で指定する。 何も指定しなければ`HS256`になる。 ```js var token = jwt.sign(payload, app.get('superSecret'), { algorithm: 'HS256', expiresIn: 60 * 60 * 24 // expires in 24 hours }); ``` ## APIルートを保護するためのルートミドルウェア これまでで`/api/authenticate`と`/api`、`/api/users`の3つのAPIを実装してきた。 次はこれらの内`/api`と`/api/users`を認証状況に合わせてアクセスの許可/不許可を行えるようにする。 この記事で一番重要な箇所だ。 リクエスト内のトークンの確認を行うルートミドルウェアは以下のものになる。 トークンがなければHTTPレスポンスコード403(閲覧禁止)を返す。 ```js // route middleware to verify a token apiRoutes.use(function(req, res, next) { // check header or url parameters or post parameters for token var token = req.body.token || req.query.token || req.headers['x-access-token']; // decode token if (token) { // verifies secret and checks exp jwt.verify(token, app.get('superSecret'), function(err, decoded) { if (err) { return res.json({ success: false, message: 'Failed to authenticate token.' }); } else { // if everything is good, save to request for use in other routes req.decoded = decoded; next(); } }); } else { // if there is no token // return an error return res.status(403).send({ success: false, message: 'No token provided.' }); } }); ``` クライアント側では取得したトークンをHTTPヘッダーの`x-access-token`や リクエスト本体、URLパラメータの好きなところに設定すればいい。 サーバー側で受け取ったトークンは作成した時に使用したキーを使って復元している。 また期限切れになっているかは`options.maxAge`を指定してあげたら確認できる。 期限切れになっていたらエラーを返すので、自前でチェックコードを追加する必要はない。 ## 終わり 認証はどう正しく実装したらいいのかわからなかったが、この記事は一つ一つ丁寧に解説してくれていたので理解できたと思う。 さらにトークン作成に使っている[jsonwebtokenの](https://github.com/auth0/node-jsonwebtoken)ドキュメントを見ると、暗号化には秘密鍵とかも利用できるようなので攻撃者による復号化も困難と非常に安心できる手法だと思う。 またMongoDBやPOSTmanといったものにも触れる機会ができたのも非常に良かった。 ### 追記 node.jsで認証を行うなら[Passport](http://www.passportjs.org/)を使用するのがいいみたい。
0 件のコメント:
コメントを投稿
次の投稿
前の投稿
ホーム
登録:
コメントの投稿 (Atom)
0 件のコメント:
コメントを投稿