Passport.jsを使う機会があったので、また必要になった時用のPassport.jsの使い方個人メモです。
Passport.jsとは
- Passport.jsとは、Node.js+Expressで作ったWebサイトに、ユーザーがログインできる、ユーザー認証を入れるためのライブラリ
- ユーザー認証の種類は、自分でユーザー管理する「ユーザーID+パスワード認証」だけでなく、GoogleやTwitterなどといったSNS認証など、様々な種類がある
- 各種の認証周りの実装はライブラリ化されていて、認証の仕組みを意識しないでユーザー認証を導入することができる
導入
- ライブラリはメイン部分と、Strategyと呼ぶ認証部分に分かれている
- 認証の種類毎にStrategyが分かれていて、使いたい認証のStrategyを都度インストールして使用する
例)Google認証
インストール
npm install --save passport npm install --save passport-google-oauth20
ライブラリ読み込み
import * as passport from 'passport'; import * as GoogleStrategy from 'passport-google-oauth20';
ログイン状態
Passport.jsは一般的に、セッションと組み合わせて使用する。
セッションと組み合わせて使用すると、一度認証が完了すると、ログイン状態はセッションとして保存され、 2回目以降のアクセスでは、セッションIDからユーザーデータが復元され、ログイン状態が維持されるようになる。
2回目以降の、アクセスからユーザーデータ取得までの流れ
- passportをExpressのミドルウェアで使用する。すると、ユーザーがログインしていれば「req.user」にユーザーデータが格納されるようになる
- 「req.user」が存在するかをチェックして、ユーザーがログインしているかを判断し、ログインしていないならログインページにリダイレクトするなどの処理を書く
- 都度ユーザーがログインしているかをチェックするのが面倒なら、ミドルウェア化してもよい
// ログイン状態を直接「req.user」から調べる app.get('/', (req: any, res) => { if (!req.user) { return res.redirect('/login'); } res.render('home.html', { user: req.user }); } ); // ログイン状態を調べるミドルウェア const checkLogin = (req, res, next) => { if (!req.user) { return res.redirect('/login'); } return next(); }; // ログイン状態チェックをミドルウェアに任せる app.get('/userinfo', checkLogin, (req: any, res) => { res.render('userinfo.html', { user: req.user }); } );
passportとStrategyの紐づけ
- passportとStrategyの紐づけは、
passport.use(new Strategy(~))
で行う new Stragety()
の2番目の引数に、認証が成功した後に呼ばれる関数を定義する- ログインしたユーザーが、妥当なユーザーなら、
callback(null, <そのユーザーのユーザーデータ>)
を呼び出す <そのユーザーのユーザーデータ>
は、req.userに格納される値となる- ログインしたユーザーが、妥当なユーザーでないなら、
callback(null, false)
を呼び出す - ログインしたユーザーが、妥当なユーザーかの検証中にエラーが発生したなら、
callback(err)
を呼び出す
- ログインしたユーザーが、妥当なユーザーなら、
new strategy()
の具体的な使い方は、認証方法(Strategyの種類)によってマチマチなので、詳しくは各Strategyのドキュメントを参照する
////////////////////////////// // passportとStrategyの紐づけ passport.use(new LocalStrategy( (username, password, cb: any) => { try { const user = DB_USER.find((v) => { return v.username === username; }); // 妥当なログインではない if (!user) { return cb(null, false); } // 妥当なログインではない if (user.password !== password) { return cb(null, false); } // 妥当なログイン return cb(null, user); } catch (err) { // エラー発生 return cb(err); } } ));
passportとセッションの紐づけ
基本方針
- セッションIDとユニークユーザー識別子を紐づけて、セッションデータとして保存する
- ユーザーデータから、ユニークユーザー識別子を取り出して、セッションIDと紐づけする方法を、
passport.serializerUser()
で定義する - サイトにアクセスがあると、下記の流れで「req.user」にユーザーデータがセットされる
- 1: セッションIDからセッションデータを参照して、ユニークユーザー識別子を取り出す
- 2 : ユニークユーザー識別子から、ユーザーデータを取り出す
- 3 : ユーザーデータを「req.user」に設定する
- ユーザー識別子からユーザーデータを取り出す方法は、
passport.deserializeUser()
で定義する
////////////////////////////// // passportとセッションの紐づけ // ユーザーデータからユニークユーザー識別子を取り出す passport.serializeUser( (user, cb) => { cb(null, user.username); }); // ユニークユーザー識別子からユーザーデータを取り出す passport.deserializeUser( (username, cb) => { const user = DB_USER.find((v) => { return v.username === username; }); if (!user) { return cb(`ERROR : NO USERNAME -> ${username}`); } return cb(null, user); });
認証
- 認証の際に使用するページを、サブディレクトリで用意する
- どのStrategyを使用するかは
passport.authenticate()
の1つ目の引数で指定する - 認証が失敗した時のリダイレクト先を、
passport.authenticate()
の2つ目の引数の「failureRedirect」で指定する - ページの設定は認証方法(Strategyの種類)によってマチマチなので、詳しくは各Strategyのドキュメントを参照する
例)ユーザーID・パスワード認証
// 認証 app.post('/login', passport.authenticate('local', { failureRedirect: '/login', // 認証失敗した場合の飛び先 failureFlash: true }), (req, res) => { // 認証成功した場合の処理 res.redirect('/'); } );
ログイン・ログアウト
- ログインページのURLは、サブディレクトリで用意する
- ログインページの設定は、認証方法(Strategyの種類)によってマチマチなので、詳しくは各Strategyのドキュメントを参照する
- ログアウトは
req.logout()
で行う。req.logout()
はセッションデータを削除するので、それ以降のアクセスはログアウト状態となる
// ログアウト app.get('/logout', (req: any, res) => { req.logout(); // セッション削除 res.redirect('/login'); } );
サンプル
passport-local:ユーザー名・パスワード認証
server.ts
import * as path from 'path'; import * as express from 'express'; import * as bodyParser from 'body-parser'; import * as session from 'express-session'; ////////////////////////////// // Passport.js import * as passport from 'passport'; import { Strategy as LocalStrategy } from 'passport-local'; ////////////////////////////// // sample user DB const DB_USER = [ { username: "test01", password: "pass", email: "test01@foo.bar.com" }, { username: "test02", password: "pass", email: "test02@foo.bar.com" }, { username: "test03", password: "pass", email: "test03@foo.bar.com" }, ]; ////////////////////////////// // passportとStrategyの紐づけ passport.use(new LocalStrategy( (username, password, cb: any) => { try { const user = DB_USER.find((v) => { return v.username === username; }); // 妥当なログインではない if (!user) { return cb(null, false); } // 妥当なログインではない if (user.password !== password) { return cb(null, false); } // 妥当なログイン return cb(null, user); } catch (err) { // エラー発生 return cb(err); } } )); ////////////////////////////// // passportとセッションの紐づけ // ユーザーデータからユニークユーザー識別子を取り出す passport.serializeUser( (user, cb) => { cb(null, user.username); }); // ユニークユーザー識別子からユーザーデータを取り出す passport.deserializeUser( (username, cb) => { const user = DB_USER.find((v) => { return v.username === username; }); if (!user) { return cb(`ERROR : NO USERNAME -> ${username}`); } return cb(null, user); }); ////////////////////////////// // Express const app = express(); const PORT_NO = 3000; // etc app.use(bodyParser.urlencoded({ extended: true })); app.set('views', path.dirname(__dirname) + '/view'); app.set('view engine', 'ejs'); app.engine('html', require('ejs').renderFile); // session // Passport.jsでセッションを使うようにする app.use(session({ secret: 'keyboard_cat', resave: false, saveUninitialized: false })); app.use(passport.initialize()); app.use(passport.session()); ////////////////////////////// // route // ログインページ app.get('/login', (req, res) => { res.render('login.html'); } ); // 認証 app.post('/login', passport.authenticate('local', { failureRedirect: '/login', // 認証失敗した場合の飛び先 failureFlash: true }), (req, res) => { // 認証成功した場合の処理 res.redirect('/'); } ); // ログイン状態を直接「req.user」から調べる app.get('/', (req: any, res) => { if (!req.user) { return res.redirect('/login'); } res.render('home.html', { user: req.user }); } ); // ログイン状態を調べるミドルウェア const checkLogin = (req, res, next) => { if (!req.user) { return res.redirect('/login'); } return next(); }; // ログイン状態チェックをミドルウェアに任せる app.get('/userinfo', checkLogin, (req: any, res) => { res.render('userinfo.html', { user: req.user }); } ); // ログアウト app.get('/logout', (req: any, res) => { req.logout(); // セッション削除 res.redirect('/login'); } ); app.listen(PORT_NO);
home.html
<%- include('head.html'); %> <body> <h3>username</h3> <div><%= user.username %></div> <h3>email</h3> <div><%= user.email %></div> <div><a href="/userinfo">userinfo</a></div> <div><a href="/logout">logout</a></div> </body> </html>
login.html
<%- include('head.html'); %> <body> <form action="/login" method="post"> <h3>username</h3> <div><input type="text" name="username" /></div> <h3>password</h3> <div><input type="password" name="password" /></div> <div> </div> </form> </body> </html>
感想など
セッション周りの実装が手間ですね。 しかし、ややこしい認証周りをやってくれるので助かります。
色々データ変換があって大変なのですが、まとめると、データは下記のような流れで変換されています。
- 「セッションID」>「ユーザー識別子(ユーザーID等)」->「req.user(ユーザーデータ)」
「req.user」に何のユーザーデータを登録するかは実装次第です。
Passport.jsを利用するには、セッションの理解が不可欠なのですが、そもそもそのあたりがよく分かっていなかったので、セッションについてもまとめました。
今はシングルページアプリケーションでサイトを作っているので、このようなユーザー認証するWebサイトを作る必要はなかったのですが、SNSのトークンを取得するのにPassport.jsを使おうと考えていて、まずはPassport.jsの一般的な使い方をまとめてみました。