はじめに
本記事の目的
- Node.jsで一般的に利用される認証ミドルである Passport.js の動作を理解すること
- 公式サイトのドキュメントがわかりづらいので、Stepごとに理解できるように進めていく。
Passportの動作の基本的な概念
- まずPassportをミドルウェアとして動作させる場合、HTTPリクエストの body にしかるべきラベルで IDとパスワードを入れてやる必要がある。デフォルトでは以下の通り。
body: { username: 'andre', password: '12345' }
- これが仮に
{ name: 'andre', pass: '12345'}
のようになっていると、デフォルトでは動作しない。 - LocalStrategyのコールバック関数は、
done()
のコールバックを返すようにしていれば実装方法は任意でよく、とにかく与えられたIDとパスワードが正しいか(IDがある、パスワードが正しい、など)判断し、done()
を返せばいい。 - 一般的に
app.post
でのミドルウェアの例が多いが、router.post
でのミドルウェアとしても動作する。
前提環境
ソフトウェア | バージョン |
---|---|
Node.js | v10.15.3 |
express | 4.16.4 |
Passport | 0.4.0 |
その1. 最小限の実装
- 極小の実装(セッション非利用、ユーザデータは埋め込み)で作成。
-
app.post
ではなく、router.post
でやっている。 - Passportに渡すパラメータが正しいか確認するため、リクエストボディをコンソールに表示する。
挙動の解説
- express から、
http://localhost:3000/
へのリクエストを indexルータ で処理。 - passportにパラメータを渡すため、
app.use(express.urlencoded( { extended: false }))
が必要。 - 認証が成功した場合、passportミドルウェアはリクエストヘッダに
req.user
をセットし、次のコールバックに処理を渡す。 - 動作を確認するのが目的なので、認証失敗時のページは実装していない。
実装
index.js
const express = require('express');
const router = express.Router();
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const User1 = {
name: "Taro",
password: "Taro123"
};
passport.use(new LocalStrategy(
(username, password, done) => {
if(username !== User1.name){
// Error
return done(null, false);
} else if(password !== User1.password) {
// Error
return done(null, false);
} else {
// Success and return user information.
return done(null, { username: username, password: password});
}
}
));
router.use(passport.initialize());
router.get('/', (req, res) => res.render('index'));
router.post('/',
(req, res, next) => {
console.log(req);
next();
},
passport.authenticate('local',
{
session: false,
failureRedirect : '/failure'
}
),
(req, res) => {
console.log(req.user);
res.send('Success');
}
);
module.exports = router;
- 参考に app.js と index.hbs も掲載する。
app.js
const express = require('express');
const app = express();
app.set('view engine', 'hbs');
app.use(express.urlencoded( { extended: false }));
app.use('/', require('./routes/index'));
app.listen(3000, console.log('Server listening port 3000...'));
index.hbs
<!DOCTYPE html>
<body>
<form action="/" method="post">
<input type="text" name="username" id="">
<input type="password" name="password" id="">
<input type="submit" value="Try Auth">
</form>
</body>
</html>
以上となる。
その2. セッションを利用する
- 次に、
express-session
を導入し一般的な利用方法に近づける。
動作概念
- Passport は認証が成功すると、セッション内にオブジェクト
passport: { user : obj }
を作成する。この動作をシリアライズという。 - シリアライズ時のコールバックに渡される第1引数は、認証成功時に
done()
で返却したユーザ情報がそのまま渡る。 - httpリクエストがあった際に、セッション内のオブジェクトが
undefined
でなければ、req.userにオブジェクトを格納しルータに引き渡す。この動作をデシリアライズという。 - オブジェクトの形式は任意でよく、StringでもJSONでもいい。
実装
- シリアライズするオブジェクトは、
{ username: String, password: String }
としている。 - ログアウトの動作として、セッション内のユーザ情報を
undefined
にしている。
app.js
const express = require('express');
const session = require('express-session');
const app = express();
app.set('view engine', 'hbs');
app.use(express.urlencoded( { extended: false }));
app.use(session({
secret: 'keyboard cat',
resave: true,
saveUninitialized: false,
}));
app.use('/', require('./routes/index'));
app.listen(3000, console.log('Server listening port 3000...'));
index.js
const express = require('express');
const router = express.Router();
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const User1 = {
name: "Taro",
password: "Taro123"
};
passport.use(new LocalStrategy(
(username, password, done) => {
if(username !== User1.name){
// Error
return done(null, false);
} else if(password !== User1.password) {
// Error
return done(null, false);
} else {
// Success and return user information.
return done(null, { username: username, password: password});
}
}
));
passport.serializeUser((user, done) => {
console.log('Serialize ...');
done(null, user);
});
passport.deserializeUser((user, done) => {
console.log('Deserialize ...');
done(null, user);
});
router.use(passport.initialize());
router.use(passport.session());
router.get('/', (req, res) => {
console.log(req.session);
res.render('index', {user: req.user});
});
router.get('/failure', (req, res) => {
console.log(req.session);
res.send('Failure');
});
router.get('/success', (req, res) => {
console.log(req.session);
res.redirect('/');
});
router.post('/',
passport.authenticate('local',
{
failureRedirect : '/failure',
successRedirect : '/success'
}
)
);
router.post('/logout', (req, res) => {
req.session.passport.user = undefined;
res.redirect('/');
});
module.exports = router;
index.hbs
<!DOCTYPE html>
<body>
{{#if user}}
<h2>Hi {{user.username}}!! You are now logged in.</h2>
<form action="/logout" method="post">
<input type="submit" value="LOGOUT">
</form>
<hr>
{{/if}}
<form action="/" method="post">
<input type="text" name="username" id="">
<input type="password" name="password" id="">
<input type="submit" value="Try Auth">
</form>
</body>
</html>
その3. flashを利用する
動作概念
- flashは一度表示、厳密には一度
req.flash( '' )
が実行されるまで、セッション内にメッセージを蓄積できる機能。 - LocalStrategy内の認証失敗時の
done()
の第3引数に{ message : String }
を渡すと、flashメッセージが蓄積され、req.flash('error')
で呼び出せる。- メッセージのラベルは自動的に
error
になる点に注意。
- メッセージのラベルは自動的に
実装
- 再掲部分が多いので、差分にコメントを入れる。app.js は変更なし。
- 認証失敗時のページでflashメッセージを表示するように変更した。
index.js
const express = require('express');
const router = express.Router();
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const flash = require('connect-flash'); //変更
const User1 = {
name: "Taro",
password: "Taro123"
};
passport.use(new LocalStrategy(
(username, password, done) => {
if(username !== User1.name){
// Error
return done(null, false, { message : 'User does not exist' }); //変更
} else if(password !== User1.password) {
// Error
return done(null, false, { message : 'Password incorrect' }); //変更
} else {
// Success and return user information.
return done(null, { username: username, password: password});
}
}
));
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((user, done) => {
done(null, user);
});
router.use(passport.initialize());
router.use(passport.session());
router.use(flash()); //変更
router.get('/', (req, res) => {
console.log(req.session);
res.render('index', {user: req.user});
});
router.get('/failure', (req, res) => {
console.log(req.session);
res.render('index', { message: req.flash( 'error' )}); //変更
});
router.get('/success', (req, res) => {
console.log(req.session);
res.redirect('/');
});
router.post('/',
passport.authenticate('local',
{
failureRedirect : '/failure',
successRedirect : '/success',
failureFlash: true //変更
}
)
);
router.post('/logout', (req, res) => {
req.session.passport.user = undefined;
res.redirect('/');
});
module.exports = router;
index.hbs
<!DOCTYPE html>
<body>
{{#if user}}
<h2>Hi {{user.username}}!! You are now logged in.</h2>
<form action="/logout" method="post">
<input type="submit" value="LOGOUT">
</form>
<hr>
{{/if}}
{{#if message}}
<p>{{message}}</p>
<hr>
{{/if}}
<form action="/" method="post">
<input type="text" name="username" id="">
<input type="password" name="password" id="">
<input type="submit" value="Try Auth">
</form>
</body>
</html>
以上!
更新
- ログアウト処理で、
req.session.passport.user = undefined;
としたが、req.logout();
が標準で提供される