Passport.js を使ってみる回その4です。
以前の記事はこちら:
前回時点での課題
前回までで、認証したユーザー情報を MongoDB に保存することができました。
これで一件落着かと思われたのですが、よくよく考えるとこの後認証先のサービス(ここでは Google)の API を利用するとなると
アクセストークンが必要になります。これも保存しておかないといけませんね。
また、アクセストークンには有効期限が定められているものもあります。
その場合は「アクセストークンが期限切れになったら、リフレッシュトークンを使って新しいアクセストークンを取得し、再度 DB に保存する」という処理も入れなければいけません。
調べてみたところ Passport.js ではそのためのライブラリとして passport-oauth2-refresh というものがあるようです。
(あんまスターついてなくて心配。。。)
というわけで今回はまず、アクセストークンとリフレッシュトークンも DB に保存するようにした後、
passport-oauth2-refresh を使ったアクセストークンの更新処理も実装してみます。
PR は https://github.com/zaki-yama/passport-express-oauth2/pull/2
本旨とは関係ありませんが、先に今回用意した API のコードを載せておきます。
内部的に Google Tasks API を叩いて Tasklist を取得します。
import express from 'express';
import fetch from 'node-fetch';
const router = express.Router();
router.get('/tasklists', (req, res) => {
console.log('/tasklists', req.user);
fetch(`https:
.then((response) => {
return response.json();
})
.then((data) => {
console.log(data);
return res.json(data.items);
});
});
export default router;
アクセストークンが切れなければこのままでも問題ありませんが、時間が経つと Google Tasks API のレスポンスが 401 になってしまいます。
アクセストークン、リフレッシュトークンを DB に保存する
これは簡単です。
認証時に accessToken, refreshToken は取得できていたので、ユーザーモデルにこれらを追加して一緒に保存してしまいます。
// models/user.js
const userSchema = mongoose.Schema({
_id: String,
displayName: String,
+ accessToken: String,
+ refreshToken: String,
image: String,
});
// routes/auth.js
- User.findByIdAndUpdate(profile.id, extractProfile(profile), {
+ const user = {
+ accessToken,
+ refreshToken,
+ ...extractProfile(profile),
+ };
+
+ User.findByIdAndUpdate(profile.id, user, {
passport-oauth2-refresh のインストールと設定
$ yarn add passport-oauth2-refresh
でインストールします。
// routes/auth.js
// [START setup]
import passport from 'passport';
import passportGoogleOauth2 from 'passport-google-oauth20';
+import refresh from 'passport-oauth2-refresh';
import User from '../models/user';
...
// along with the user's profile. The function must invoke `cb` with a user
// object, which will be set at `req.user` in route handlers after
// authentication.
-passport.use(new GoogleStrategy({
+const strategy = new GoogleStrategy({
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
// FIXME: Enable to switch local & production environment.
...
console.log(err, user);
return done(err, user);
});
-}));
+});
+passport.use(strategy);
+refresh.use(strategy);
ここもそんなにやることはないですね。元々 passport.use()
にストラテジーを渡していたのと同じことを refresh.use()
に対してもやります。
アクセストークンが有効期限切れだったら更新する、という処理を実装
最初に紹介した /routes/api.js にアクセストークンが有効期限切れだったときの処理を入れると、以下のようになります。
これは、 passport-oauth2-refresh の Issue に書かれていたコードを参考にしたものです。
Mongoose でのモデルの save 方法などは↑のコードとは違うようなので、そこは直してます。
import express from 'express';
import fetch from 'node-fetch';
+import refresh from 'passport-oauth2-refresh';
const router = express.Router();
router.get('/tasklists', (req, res) => {
- console.log('/tasklists', req.user);
- fetch(`https://www.googleapis.com/tasks/v1/users/@me/lists?access_token=${req.user.accessToken}`)
- .then((response) => {
- return response.json();
- })
- .then((data) => {
- console.log(data);
- return res.json(data.items);
- });
+ const user = req.user;
+ const makeRequest = () => {
+ console.log('/tasklists', user.accessToken);
+ fetch(`https://www.googleapis.com/tasks/v1/users/@me/lists?access_token=${user.accessToken}`)
+ .then((response) => {
+ if (response.status === 401) {
+ console.log('response status 401. Retry');
+ // Access token expired (or is invalid).
+ // Try to fetch a new one.
+ refresh.requestNewAccessToken('google', user.refreshToken, (err, accessToken) => {
+ // TODO: Error handling
+ console.log('new access_token', accessToken);
+
+ // Save the new accessToken
+ user.accessToken = accessToken;
+ user.save().then(() => {
+ makeRequest();
+ });
+ });
+ }
+
+ console.log(response.status);
+ return response.json();
+ })
+ .then((data) => {
+ console.log(data);
+ return res.json(data.items);
+ });
+ };
+
+ return makeRequest();
});
export default router;
fetch を使った場合、ネットワークエラーでもない限りレスポンスは then()
に渡されるので、 response.status
の値で 401 が返ってきていないかチェックします。
401 で返ってきた場合はアクセストークン有効期限切れの可能性があるので、
refresh.requestNewAccessToken()
で新しいアクセストークンを取得します。
レスポンスの第二引数には新しいアクセストークンが渡されてくるので、あとはこのアクセストークンでユーザーモデルの内容を上書きして保存( user.save() => { ...
)すればいいですね。
ただしリトライ数には上限がないので、Issue のサンプルコードと同じように
const retry = 2
などを用意する
makeRequest()
時にデクリメントする
retry <= 0
の場合は 401 画面を出す
とした方がいいと思います。
また、今回は簡単なサンプルコードのため API が一つしかなく、その中にアクセストークン更新処理&リトライを書いてますが、
これはおそらくすべての API で共通な処理となると思うので、独立した関数にする?など工夫が必要そうです。
TODO
エラーハンドリングを諸々省略してるので、もうちょっと安全にするためにはそういった処理も必要そうです。
なんか動作検証してたら UnhandledPromiseException がちょいちょい出てた気がする(けどリロードしたら出なくなる)んですが、 user.save()
に失敗しているとかかな...