dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

[Node.js]ExpressとPassport.jsでOAuth2 (2)認証済みユーザー情報をセッションに保存する

はじめに

Passport.js を使ってみる回その2です。
前回はこちら:[Node.js]ExpressとPassport.jsでOAuth2 (1)インストールと認証までのフローを作る - dackdive's blog

前回まででなんとか認証先のサービス(今回は Google)で認証した後、ユーザー情報を取得してアプリに戻ってくるところまでできました。
今回は取得したユーザー情報をセッションに保存し、使い回せるようにします。

リポジトリ

該当のコミットは e0ecd4 あたり


express-sesion のインストール

セッションを利用するために express-session が必要になるのでインストールしておきます。

$ yarn add express-session


コード

今回新たに追加するコードです。

// routes/auth.js(抜粋)
 // Configure the Google strategy for use by Passport.js.
 //
 // OAuth 2-based strategies require a `verify` function which receives the
 // credential (`accessToken`) for accessing the Google API on the user's behalf,
 // 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({
   clientID: process.env.CLIENT_ID,
   clientSecret: process.env.CLIENT_SECRET,
   // FIXME: Enable to switch local & production environment.
   callbackURL: 'http://localhost:8080/auth/google/callback',
   accessType: 'offline',
 }, (accessToken, refreshToken, profile, cb) => {
   // Extract the minimal profile information we need from the profile object
   // provided by Google
   cb(null, extractProfile(profile));
 }));

+passport.serializeUser((user, done) => {
+  console.log('serializeUser', user);
+  done(null, user);
+});
+passport.deserializeUser((obj, done) => {
+  console.log('deserializeUser', obj);
+  done(null, obj);
+});
// app.js(抜粋)
 import express from 'express';
+import session from 'express-session';
 import path from 'path';
 import passport from 'passport';
 
 import auth from './routes/auth';

 (略)

+app.use(session({
+  secret: 'keyboard cat',
+  resave: false,
+  saveUninitialized: true,
+  cookie: {
+    maxAge: 1000 * 60 * 60 * 24 * 30,
+  },
+}));

 app.use(passport.initialize());
+app.use(passport.session());

-// Serve static files
-// app.use(express.static(path.join(__dirname, 'public')));
 
+// Application Root
+app.get('/', (req, res) => {
+  console.log('user', req.user);
+  res.sendFile(path.join(__dirname, 'public', 'index.html'));
+});

routes/auth.js には GoogleStrategy の初期化処理の下に serializeUserdeserializeUser という2つのメソッドを定義しました。
これらは、ユーザー情報をセッションに保存するときや取り出すときにそれぞれ実行します。

参考:http://passportjs.org/docs#sessions

In order to support login sessions, Passport will serialize and deserialize user instances to and from the session.

ドキュメントの例では、

passport.serializeUser(function(user, done) {
  done(null, user.id);
});

セッションにはユーザーオブジェクトそのものではなくユーザーの ID だけを保存するようにしています。

また、app.js では上から順に以下を行っています。

express-session ミドルウェアの設定

app.use(session({ ... }) の箇所です。各オプションについてはこちらを見るといいです。
https://expressjs.com/en/resources/middleware/session.html

なお、サイトによっては cookie-parser を有効にしているコードも見られますが、express-session のドキュメントを読むと

Note Since version 1.5.0, the cookie-parser middleware no longer needs to be used for this module to work.

とあるので、最新版では不要のようです。


Passport.js 側のミドルウェアも有効化

app.use(passport.session()); で Passport.js のセッション用ミドルウェアも有効にしています。
express-session の初期化処理は passport.session() だけでなく passport.initialize() よりも前に行わないといけないので注意


アプリのエントリーポイント(/)に対するミドルウェアを定義

express.static() を指定してしまうと / に来たアクセスをミドルウェアで処理できなかったので書き方を変えました。
今回は / でも req.user をログに出力しているだけですが、ここで

if (!req.user) {
  return res.redirect('/auth/login');
}

などとしてやれば「アクセス時、認証済みかどうかを判定し、未認証の場合は認証画面にリダイレクト」が実現できそうです。


Passport.js や Express におけるセッションの扱いについて

一応ここまでで動くようになりましたが、Passport.js および Express でセッションがどう扱われているのかわかってなかったのでメモ。


Passport.js でセッションを使う/使わないという設定はどうなっているの?

http://passportjs.org/docs#disable-sessions

After successful authentication, Passport will establish a persistent login session. This is useful for the common scenario of users accessing a web application via a browser. However, in some cases, session support is not necessary

という記載から、デフォルトで認証したユーザー情報をセッションに保存するような動きになっていると理解しました。
その上で

http://passportjs.org/docs#middleware

If your application uses persistent login sessions, passport.session() middleware must also be used.

Note that enabling session support is entirely optional, though it is recommended for most applications. If enabled, be sure to use express.session() before passport.session() to ensure that the login session is restored in the correct order.

なので、passport.session() および express.session() は有効にする必要があると。


セッション情報はどこに保存されているの?

express-session 側のドキュメントを読むと
https://github.com/expressjs/session#store

The session store instance, defaults to a new MemoryStore instance.

なので、オプションを特に指定しなかったときの保存先は MemoryStore つまりサーバー側のメモリ上(なので再起動のたびにクリアされる)、
オンメモリではなくデータベース上に永続的に保存したい場合は store オプションで指定する必要があるみたいです。


セッション情報はどのように保存されているの?

こちらも express-session 側のドキュメントを読むと
https://github.com/expressjs/session#sessionoptions

Note Session data is not saved in the cookie itself, just the session ID. Session data is stored server-side.

というわけで、セッション情報は基本的にサーバー側に保存され、その ID だけをブラウザの Cookie に保存しているようです。
実際に今回のコードで Cookie を見ると、ログイン後に connect.sid という名前の Cookie が保存されていることがわかります。

f:id:dackdive:20170827165044p:plain

試しにこちらを削除してリロードすると、再度ログイン画面にリダイレクトされます。


まとめと TODO

というわけで前回取得した認証済みユーザー情報をセッションに保存するところまでできました。
しかしながら express-session#session(options) の項を読むとわかるように

Warning The default server-side session storage, MemoryStore, is purposely not designed for a production environment. It will leak memory under most conditions, does not scale past a single process, and is meant for debugging and developing.

セッションの保存先としてサーバーのメモリを使用するのは本番環境では推奨されないようです。
次回はこれを MongoDB に保存する方法を調べます。

(追記)

書きました。

リファレンス