概要
Passport、passport-localを使ってごく簡単な認証フローを確認してみます。
Passportの公式サイト
ソースコード
今回の実装内容のソースコードは以下にあります。
環境
ソフトウェア | バージョン |
---|---|
Node.js | 14.15.0 |
npx | 6.14.8 |
Express | 4.16.1 |
Passport | 0.4.1 |
passport-local | 1.0.0 |
Expressアプリを作成する
express-generatorを使って今回認証機能を実装するためのExpressアプリを作ります。
npx express-generator --view=pug express-passport
cd express-passport
yarn install
yarn start
ログインページを作成する
/login
に対してPOSTリクエストできるページを作成します。
extends layout
block content
h1 ログイン
form(action="/login" method="POST")
div
label(for="username") ユーザ名
input(type="text" name="username")
div
label(for="password") パスワード
input(type="password" name="password")
div
button(type="submit") ログイン
Passportを使って認証APIを作成する
最初に必要なライブラリをインストールします。
yarn add passport passport-local express-session
Passportの基本
Passportでは、認証を行うのに以下の3つを設定する必要があります。
(セッションは任意ですが、リクエストの度に認証するAPIでもなければ基本的に必要となります)
- ストラテジー(Authentication strategies)
- ミドルウェア(Application Middleware)
- セッション(Sessions)
ストラテジーを設定する
Passportでは、ストラテジーという方法でリクエストを認証します。
ストラテジーは今回実装するユーザ名・パスワードの組み合わせの検証(passport-local)のほか、OAuthを使った委任認証、OpenIDを使った連携認証などいろいろあります。
また、ストラテジーはリクエストの認証に使われるよりも前に設定しておく必要があります。
今回はapp.js
に以下のような感じで実装しました。
const passport = require('passport');
const LocalStorategy = require('passport-local').Strategy;
const users = [
{ username: 'alice', password: 'alice', age: 22 },
{ username: 'bob', password: 'bob', age: 21 },
{ username: 'carol', password: 'carol', age: 30 }
]
const User = {
findOne({ username }) {
return users.find(user => user.username === username) || null
}
}
passport.use(new LocalStorategy(function(username, password, done) {
try {
const user = User.findOne({ username })
if (user == null) {
return done(null, false)
}
if (user.password !== password) {
return done(null, false)
}
delete user.password
return done(null, user)
} catch (err) {
done(err)
}
}))
LocalStorategy
の引数として渡された関数はverify callbackと呼ばれ、ここでは与えられた認証情報(credentials)を有するユーザを検索して認証情報が妥当かどうか検証しています。
認証の結果に応じてdone()
関数に対して以下のように引数を渡しています。
認証結果 | 第1引数 | 第2引数 |
---|---|---|
成功 | null | 認証されたユーザオブジェクト |
失敗 | null | false |
サーバエラー | Errorオブジェクト | - |
サーバ側でエラーが発生した(例えばDBに接続できないなど)場合、上記のように第1引数にエラーを入れておきます。クライアント側のエラーであれば基本的にエラーは入れず、認証失敗としてdone(null, false)
とします。
passport.initialize()
ミドルウェアで初期化する
永続的なセッションが必要ならexpress-sessionミドルウェア及びpassport.session()
を使う必要があります。
app.use(session({
secret: "cats",
resave: false,
saveUninitialized: false
}));
app.use(passport.initialize());
app.use(passport.session());
ちなみに公式サイトでは、以下のようにbody-parserをインストールしていますが、
app.use(bodyParser.urlencoded({ extended: false }));
v4.16.0以降のExpress.jsでは標準搭載されているそうです。
参考: Body-ParserがExpressにexpress.json()として標準搭載されている話 - Qiita
今回はexpress-generatorで作成しているので既に必要な記述はされています。
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
ユーザのシリアライズ・デシリアライズ
永続的なセッションが必要な場合、セッションに対してユーザインスタンスをシリアライズして保存し、リクエストが来たときにセッションからユーザインスタンスをデシリアライズして保存できるようにします。
passport.serializeUser(function(user, done) {
done(null, user.username);
});
passport.deserializeUser(function(username, done) {
const user = User.findOne({ username })
done(null, user);
});
なるべく保存されるセッション内のデータ量を小さくする為にここではuser.username
だけ入れています。
認証APIを作成する
router.post('/login', passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/login',
}))
router.post('/logout', function(req, res, next) {
req.logOut();
res.redirect('/login');
})
認証チェックミドルウェアを作成する
認証チェックミドルウェアを作って、アクセスを制限したいパスに対して付与します。
module.exports = function (req, res, next) {
if (!req.isAuthenticated()) {
res.redirect('/login')
return
}
next()
}
こんな感じのミドルウェアを作って、例えば/
に付与すると、認証されていなければアクセスすることができないようになります。
const authenticated = require('../middleware/authenticated');
router.get('/', authenticated, function(req, res, next) {
res.render('index', { title: 'Express', username: req.user.username });
});
動作確認
yarn start
http://localhost:3000 にアクセスし、認証されていないので/login
に遷移することを確認。
適当なユーザでログインしてクッキーがセットされることを確認します。
HTTP/1.1 302 Found
Set-Cookie: connect.sid=*****; Path=/; HttpOnly
補足
password-localではユーザ名とパスワードのプロパティ名がデフォルトでusername
、password
ですが、変更する場合はストラテジーを設定する際に以下のように設定を追加します。
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'passwd'
},
function(username, password, done) {
// ...
}
));