JSONWebトークンとパスポートを使用してAPI認証を実装する方法
序章
多くのWebアプリケーションとAPIは、認証形式を使用してリソースを保護し、検証済みのユーザーのみにアクセスを制限します。
JSON Web Token(JWT)は、当事者間で情報をJSONオブジェクトとして安全に送信するためのコンパクトで自己完結型の方法を定義するオープンスタンダードです。
このガイドでは、JWTと Passport 、Nodeの認証ミドルウェアを使用してAPIの認証を実装する方法について説明します。
ここに、構築するアプリケーションの概要を示します。
- ユーザーがサインアップすると、ユーザーアカウントが作成されます。
- ユーザーがログインすると、JSONWebトークンがユーザーに割り当てられます。
- このトークンは、特定の安全なルートにアクセスしようとしたときにユーザーによって送信されます。
- トークンが確認されると、ユーザーはルートにアクセスできるようになります。
前提条件
このチュートリアルを完了するには、次のものが必要です。
- Node.jsはローカルにインストールされます。これは、Node.jsのインストール方法とローカル開発環境の作成に従って実行できます。
- MongoDB がローカルにインストールおよび実行されています。これは、公式ドキュメントに従うことで実行できます。
- APIエンドポイントをテストするには、Postmanなどのツールをダウンロードしてインストールする必要があります。
このチュートリアルは、ノードv14.2.0、npm v6.14.5、およびmongodb-communityv4.2.6で検証されました。
ステップ1—プロジェクトの設定
プロジェクトを設定することから始めましょう。 ターミナルウィンドウで、プロジェクトのディレクトリを作成します。
mkdir jwt-and-passport-auth
そして、その新しいディレクトリに移動します。
cd jwt-and-passport-auth
次に、新しいpackage.jsonを初期化します。
npm init -y
プロジェクトの依存関係をインストールします。
npm install --save [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected]
ユーザーパスワードのハッシュにはbcrypt、トークンの署名にはjsonwebtoken、ローカル戦略の実装にはpassport-local、JWTの取得と検証にはpassport-jwtが必要です。
この時点で、プロジェクトは初期化され、すべての依存関係がインストールされています。 次に、ユーザー情報を保存するデータベースを追加します。
ステップ2—データベースのセットアップ
データベーススキーマは、データのタイプとデータベースの構造を確立します。 データベースには、ユーザー用のスキーマが必要です。
modelディレクトリを作成します。
mkdir model
この新しいディレクトリにmodel.jsファイルを作成します。
nano model/model.js
mongooseライブラリは、MongoDBコレクションにマップされるスキーマを定義するために使用されます。 スキーマでは、ユーザーに電子メールとパスワードが必要になります。 mongooseライブラリはスキーマを取得し、それをモデルに変換します。
model / model.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const UserSchema = new Schema({
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
}
});
const UserModel = mongoose.model('user', UserSchema);
module.exports = UserModel;
攻撃者がデータベースにアクセスできた場合、パスワードが読み取られる可能性があるため、パスワードをプレーンテキストで保存することは避けてください。
これを回避するには、bcryptというパッケージを使用して、ユーザーパスワードをハッシュし、安全に保存します。 ライブラリと次のコード行を追加します。
model / model.js
// ...
const bcrypt = require('bcrypt');
// ...
const UserSchema = new Schema({
// ...
});
UserSchema.pre(
'save',
async function(next) {
const user = this;
const hash = await bcrypt.hash(this.password, 10);
this.password = hash;
next();
}
);
// ...
module.exports = UserModel;
UserScheme.pre()関数のコードはプリフックと呼ばれます。 ユーザー情報がデータベースに保存される前に、この関数が呼び出され、プレーンテキストのパスワードを取得してハッシュし、保存します。
thisは、保存しようとしている現在のドキュメントを指します。
await bcrypt.hash(this.password, 10)は、パスワードと salt round (または cost )の値を10に渡します。 コストが高くなると、より多くの反復でハッシュが実行され、より安全になります。 これには、アプリケーションのパフォーマンスに影響を与える可能性があるという点で、より計算集約的であるというトレードオフがあります。
次に、プレーンテキストのパスワードをハッシュに置き換えて保存します。
最後に、完了したことを示し、next()を使用して次のミドルウェアに進む必要があります。
また、ログインしようとしているユーザーが正しい資格情報を持っていることを確認する必要があります。 次の新しいメソッドを追加します。
model / model.js
// ...
const UserSchema = new Schema({
// ...
});
UserSchema.pre(
// ...
});
UserSchema.methods.isValidPassword = async function(password) {
const user = this;
const compare = await bcrypt.compare(password, user.password);
return compare;
}
// ...
module.exports = UserModel;
bcryptは、ログインのためにユーザーから送信されたパスワードをハッシュし、データベースに保存されているハッシュされたパスワードが送信されたパスワードと一致するかどうかを確認します。 一致する場合はtrueを返します。 それ以外の場合、一致するものがない場合はfalseを返します。
この時点で、MongoDBコレクション用に定義されたスキーマとモデルがあります。
ステップ3—登録およびログインミドルウェアの設定
Passportは、リクエストの認証に使用される認証ミドルウェアです。
これにより、開発者は、ローカルデータベースの使用や、APIを介したソーシャルネットワークへの接続など、ユーザーを認証するためのさまざまな戦略を使用できます。
このステップでは、ローカル(電子メールとパスワード)戦略を使用します。
passport-local戦略を使用して、ユーザー登録とログインを処理するミドルウェアを作成します。 これは特定のルートに接続され、認証に使用されます。
authディレクトリを作成します。
mkdir auth
この新しいディレクトリにauth.jsファイルを作成します。
nano auth/auth.js
passport、passport-local、および前の手順で作成したUserModelを要求することから始めます。
auth / auth.js
const passport = require('passport');
const localStrategy = require('passport-local').Strategy;
const UserModel = require('../model/model');
まず、ユーザー登録を処理するPassportミドルウェアを追加します。
auth / auth.js
// ...
passport.use(
'signup',
new localStrategy(
{
usernameField: 'email',
passwordField: 'password'
},
async (email, password, done) => {
try {
const user = await UserModel.create({ email, password });
return done(null, user);
} catch (error) {
done(error);
}
}
)
);
このコードは、ユーザーから提供された情報をデータベースに保存し、成功した場合はユーザー情報を次のミドルウェアに送信します。
それ以外の場合は、エラーを報告します。
次に、ユーザーログインを処理するPassportミドルウェアを追加します。
auth / auth.js
// ...
passport.use(
'login',
new localStrategy(
{
usernameField: 'email',
passwordField: 'password'
},
async (email, password, done) => {
try {
const user = await UserModel.findOne({ email });
if (!user) {
return done(null, false, { message: 'User not found' });
}
const validate = await user.isValidPassword(password);
if (!validate) {
return done(null, false, { message: 'Wrong Password' });
}
return done(null, user, { message: 'Logged in Successfully' });
} catch (error) {
return done(error);
}
}
)
);
このコードは、提供された電子メールに関連付けられている1人のユーザーを検索します。
- ユーザーがデータベース内のどのユーザーとも一致しない場合、
"User not found"エラーが返されます。 - パスワードがデータベース内のユーザーに関連付けられているパスワードと一致しない場合、
"Wrong Password"エラーが返されます。 - ユーザーとパスワードが一致すると、
"Logged in Successfully"メッセージが返され、ユーザー情報が次のミドルウェアに送信されます。
それ以外の場合は、エラーを報告します。
この時点で、サインアップとログインを処理するためのミドルウェアがあります。
ステップ4—サインアップエンドポイントを作成する
Express は、ルーティングを提供するWebフレームワークです。 このステップでは、signupエンドポイントのルートを作成します。
routesディレクトリを作成します。
mkdir routes
この新しいディレクトリにroutes.jsファイルを作成します。
nano routes/routes.js
expressとpassportを要求することから始めます。
ルート/routes.js
const express = require('express');
const passport = require('passport');
const router = express.Router();
module.exports = router;
次に、signupのPOSTリクエストの処理を追加します。
ルート/routes.js
// ...
const router = express.Router();
router.post(
'/signup',
passport.authenticate('signup', { session: false }),
async (req, res, next) => {
res.json({
message: 'Signup successful',
user: req.user
});
}
);
module.exports = router;
ユーザーがこのルートにPOSTリクエストを送信すると、Passportは以前に作成されたミドルウェアに基づいてユーザーを認証します。
これで、signupエンドポイントができました。 次に、loginエンドポイントが必要になります。
ステップ5—ログインエンドポイントの作成とJWTへの署名
ユーザーがログインすると、ユーザー情報がカスタムコールバックに渡され、カスタムコールバックがその情報を使用して安全なトークンを作成します。
このステップでは、loginエンドポイントのルートを作成します。
まず、jsonwebtokenが必要です。
ルート/routes.js
const express = require('express');
const passport = require('passport');
const jwt = require('jsonwebtoken');
// ...
次に、loginのPOSTリクエストの処理を追加します。
ルート/routes.js
// ...
const router = express.Router();
// ...
router.post(
'/login',
async (req, res, next) => {
passport.authenticate(
'login',
async (err, user, info) => {
try {
if (err || !user) {
const error = new Error('An error occurred.');
return next(error);
}
req.login(
user,
{ session: false },
async (error) => {
if (error) return next(error);
const body = { _id: user._id, email: user.email };
const token = jwt.sign({ user: body }, 'TOP_SECRET');
return res.json({ token });
}
);
} catch (error) {
return next(error);
}
}
)(req, res, next);
}
);
module.exports = router;
ユーザーのパスワードなどの機密情報をトークンに保存しないでください。
idとemailをJWTのペイロードに保存します。 次に、秘密またはキー(TOP_SECRET)を使用してトークンに署名します。 最後に、トークンをユーザーに送り返します。
注:ユーザーの詳細をセッションに保存したくないため、{ session: false }を設定します。 ユーザーは、リクエストごとにトークンを安全なルートに送信する必要があります。
これはAPIに特に役立ちますが、パフォーマンス上の理由からWebアプリケーションには推奨されないアプローチです。
これで、loginエンドポイントができました。 正常にログインしたユーザーはトークンを生成します。 ただし、アプリケーションはまだトークンに対して何もしません。
ステップ6—JWTを確認する
これで、ユーザーのサインアップとログインを処理できました。次のステップは、トークンを持つユーザーが特定の安全なルートにアクセスできるようにすることです。
このステップでは、トークンが操作されておらず、有効であることを確認します。
auth.jsファイルに再度アクセスしてください。
nano auth/auth.js
次のコード行を追加します。
auth / auth.js
// ...
const JWTstrategy = require('passport-jwt').Strategy;
const ExtractJWT = require('passport-jwt').ExtractJwt;
passport.use(
new JWTstrategy(
{
secretOrKey: 'TOP_SECRET',
jwtFromRequest: ExtractJWT.fromUrlQueryParameter('secret_token')
},
async (token, done) => {
try {
return done(null, token.user);
} catch (error) {
done(error);
}
}
)
);
このコードは、passport-jwtを使用して、クエリパラメーターからJWTを抽出します。 次に、このトークンがログイン中に設定されたシークレットまたはキーで署名されていることを確認します(TOP_SECRET)。 トークンが有効な場合、ユーザーの詳細は次のミドルウェアに渡されます。
注:トークンで使用できないユーザーに関する追加の詳細または機密情報が必要な場合は、トークンで使用可能な_idを使用して、データベースからそれらを取得できます。
これで、アプリケーションはトークンへの署名とトークンの検証の両方が可能になります。
ステップ7—安全なルートを作成する
それでは、検証済みのトークンを持つユーザーだけがアクセスできる安全なルートをいくつか作成しましょう。
新しいsecure-routes.jsファイルを作成します。
nano routes/secure-routes.js
次に、次のコード行を追加します。
ルート/secure-routes.js
const express = require('express');
const router = express.Router();
router.get(
'/profile',
(req, res, next) => {
res.json({
message: 'You made it to the secure route',
user: req.user,
token: req.query.secret_token
})
}
);
module.exports = router;
このコードは、profileのGETリクエストを処理します。 "You made it to the secure route"メッセージを返します。 また、userおよびtokenに関する情報も返します。
目標は、検証済みのトークンを持つユーザーのみにこの応答が表示されるようにすることです。
ステップ8—すべてをまとめる
ルートと認証ミドルウェアの作成がすべて完了したので、すべてをまとめることができます。
新しいapp.jsファイルを作成します。
nano app.js
次に、次のコードを追加します。
app.js
const express = require('express');
const mongoose = require('mongoose');
const passport = require('passport');
const bodyParser = require('body-parser');
const UserModel = require('./model/model');
mongoose.connect('mongodb://127.0.0.1:27017/passport-jwt', { useMongoClient: true });
mongoose.connection.on('error', error => console.log(error) );
mongoose.Promise = global.Promise;
require('./auth/auth');
const routes = require('./routes/routes');
const secureRoute = require('./routes/secure-routes');
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use('/', routes);
// Plug in the JWT strategy as a middleware so only verified users can access this route.
app.use('/user', passport.authenticate('jwt', { session: false }), secureRoute);
// Handle errors.
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.json({ error: err });
});
app.listen(3000, () => {
console.log('Server started.')
});
注: mongooseのバージョンによっては、WARNING: The 'useMongoClient' option is no longer necessary in mongoose 5.x, please remove it.というメッセージが表示される場合があります。
useNewUrlParser、useUnifiedTopology、およびensureIndex(createIndexes)の廃止通知が表示される場合もあります。
トラブルシューティング中に、mongoose.connectメソッド呼び出しを変更し、mongoose.setメソッド呼び出しを追加することで、これらを解決することができました。
mongoose.connect("mongodb://127.0.0.1:27017/passport-jwt", {
useNewUrlParser: true,
useUnifiedTopology: true,
});
mongoose.set("useCreateIndex", true);
次のコマンドを使用してアプリケーションを実行します。
node app.js
"Server started."メッセージが表示されます。 アプリケーションを実行したままにして、テストします。
ステップ9—Postmanでのテスト
すべてをまとめたので、Postmanを使用してAPI認証をテストできます。
注:リクエストのためにPostmanインターフェースをナビゲートするための支援が必要な場合は、公式ドキュメントを参照してください。
まず、電子メールとパスワードを使用して、アプリケーションに新しいユーザーを登録する必要があります。
Postmanで、routes.jsで作成したsignupエンドポイントへのリクエストを設定します。
POST localhost:3000/signup Body x-www-form-urlencoded
そして、リクエストのBodyを介してこれらの詳細を送信します。
| 鍵 | 価値 |
|---|---|
| Eメール | [email protected]
|
| パスワード | password
|
それが完了したら、送信ボタンをクリックして、POSTリクエストを開始します。
Output{
"message": "Signup successful",
"user": {
"_id": "[a long string of characters representing a unique id]",
"email": "[email protected]",
"password": "[a long string of characters representing an encrypted password]",
"__v": 0
}
}
パスワードはデータベースに保存される方法であるため、暗号化された文字列として表示されます。 これは、model.jsでbcryptを使用してパスワードをハッシュするために作成したプリフックの結果です。
次に、資格情報を使用してログインし、トークンを取得します。
Postmanで、routes.jsで作成したloginエンドポイントへのリクエストを設定します。
POST localhost:3000/login Body x-www-form-urlencoded
そして、リクエストのBodyを介してこれらの詳細を送信します。
| 鍵 | 価値 |
|---|---|
| Eメール | [email protected]
|
| パスワード | password
|
それが完了したら、送信ボタンをクリックして、POSTリクエストを開始します。
Output{
"token": "[a long string of characters representing a token]"
}
トークンを取得したので、安全なルートにアクセスするときはいつでもこのトークンを送信します。 後で使用するためにコピーして貼り付けます。
/user/profileにアクセスすると、アプリケーションがトークンの検証をどのように処理するかをテストできます。
Postmanで、secure-routes.jsで作成したprofileエンドポイントへのリクエストを設定します。
GET localhost:3000/user/profile Params
そして、secret_tokenと呼ばれるクエリパラメータでトークンを渡します。
| 鍵 | 価値 |
|---|---|
| secret_token | [a long string of characters representing a token]
|
それが完了したら、送信ボタンをクリックして、GETリクエストを開始します。
Output{
"message": "You made it to the secure route",
"user": {
"_id": "[a long string of characters representing a unique id]",
"email": "[email protected]"
},
"token": "[a long string of characters representing a token]"
}
トークンが収集され、検証されます。 トークンが有効な場合、安全なルートへのアクセスが許可されます。 これは、secure-routes.jsで作成した応答の結果です。
このルートへのアクセスを試みることもできますが、トークンが無効な場合、リクエストはUnauthorizedエラーを返します。
結論
このチュートリアルでは、JWTを使用してAPI認証を設定し、Postmanを使用してテストしました。
JSON Webトークンは、APIの認証を作成するための安全な方法を提供します。 トークン内のすべての情報を暗号化することでセキュリティの層を追加できるため、トークンの安全性がさらに高まります。
JWTのより深い知識が必要な場合は、次の追加リソースを使用できます。