[npm] Passport 筆記(Learn to Use Passport JS)
- 原本存在 gist 的檔案
- Dive into Passport JS @ PJCHENder Gist
基本觀念
- Strategies 要先定義好之後才能進入 routes
- 當成功登入後可以得到
req.user
這個物件 - 如果定義的 Strategy 驗證失敗,會在 callback 中回傳 err ,後續的路由不會被執行,並回傳 401 Unauthorized 的 response
在路由中使用設定好的 Passport Strategy
設定好的 Passport Strategy 可以直接在 routes 中使用:
在 routes 中使用
將定義好的 passport Strategy 當成 middleware (passport.authenticate('<strategyName>')
)套用在 routes 中即可:
// ./routes/index.js
app.post(
'/login',
// passport as middleware
passport.authenticate('local'),
// routes handler
(req, res) => {
// 如果這個 function 有執行,表示通過驗證
// 在 req.user 中會回傳被認證的使用者
res.redirect(`/users/${req.user.username}`);
},
);
在預設的情況下,如果認證失敗,Passport 會回傳 401 Unauthorized
的狀態,後續的路由都不會在被處理;如果認證成功,則回觸發 next
,並可以在 res.user
中拿到被認證的使用者。
若有需要也可以修改轉址的路徑:
app.get(
'/signin',
passport.authenticate('local', {
failureRedirect: '/signin',
failureFlash: true,
}),
(req, res) => res.redirect(`/users/${req.user.username}`),
);
Session
在成功登入後,Passport 會建立一個 login session。如果不需要可以把它停用(passport.authenticate('<strategyName>', { session: false })
):
// ./routes/index.js
app.get('/api/users/me', passport.authenticate('basic', { session: false }), (req, res) => {
res.json({ id: req.user.id, username: req.user.username });
});
客制化 callback(常用)
如果內建的驗證請求不足夠使用,可以使用客制化的 callback 來處理,passport.authenticate('<strategyName>', callback<err, user, info>)
:
// ./routes/index.js
app.get('/login', function (req, res, next) {
// 在 routes 的 handler 中使用 passport.authenticate
passport.authenticate('local', function (err, user, info) {
if (err) {
return next(err);
}
// 如果找不到使用者
if (!user) {
return res.redirect('/login');
}
// 否則登入
req.logIn(user, function (err) {
if (err) {
return next(err);
}
return res.redirect('/users/' + user.username);
});
})(req, res, next);
});
在這個範例中,passport.authenticate('<strategyName>', callback<err, user, info>)
是在 Express 中的路由中去執行,而不是當成 middleware 使用。因此,這是透過 closure 來在 callback 中取得 req
和 res
。
如果認證失敗,user
會被設成 false
;如果例外發生;err
會被設定;info
則可以拿到 strategy 中 verify callback 所提供的更多訊息。
要注意的是,當使用客制化的 callback 時,需要自己透過 req.login()
來設置 session,並且回傳 response。
req.login('user', callback<err>)
來建立 session,若使用者登入成功,user 會被指定到 req.user(一般情況下,req.login() 會在 passport.authenticate 的 middleware 中被執行,但若是客制化的 callback 則要自己帶)。req.logout()
會移除 req.user 這個屬性,並同時清除 login session(如果有的話)。
使用內建的驗證函式
app.post(
'/login',
passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/login',
session: false,
}),
);
設定 Passport Strategy
透過 passport.use()
可以設定 Strategies:
// ./middleware/passport
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
passport.use(
new LocalStrategy(
// 這是 verify callback
function (username, password, done) {
User.findOne({ username: username }, function (err, user) {
if (err) {
return done(err);
}
// 如果使用者不存在
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
// 如果使用者密碼錯誤
if (!user.validPassword(password)) {
return done(null, false, { message: 'Incorrect password.' });
}
// 認證成功,回傳使用者資訊 user
return done(null, user);
});
},
),
);
驗證用的 callback
return done(<error|null>, <user|false>, {message: 'incorrect reason'})
透過 verify callback 來執行驗證後的結果。在 Passport 驗證一個 request 時,它會解析 request 中的登入資訊(credentials),接著以這些 credentials 當作參數來執行 verify callback(在上面的例子就是 username 和 password)。
有錯誤產生時
這裡的錯誤指的是伺服器的錯誤,這時候 err
會被設為非 null
的值;如果是驗證失敗(伺服器沒有錯誤)應該則此值要設定成null
。
return done(error);
驗證成功
如果 credentials 是有效的,那麼 verify callback 會執行 done
,並提供受驗證後的使用者資訊(user
):
return done(null, user);
驗證失 敗
如果 credentials 無效,則執行 done 中代入第二個參數為 false,表示認證失敗:
return done(null, false);
也可以在第三個參數給認證失敗的理由:
return done(null, false, { message: 'Incorrect password.' });
在 Express 中使用
在 Express 中使用時,要在 middleware 中透過 passport.initialize()
來初始化 Passport。如果有使用 login session,則需要再使用 passport.session()
。
如果有使用 passport.session()
建議要放在 express.session()
來確保執行順序正確。
初始化 Passport
// ./app.js
const express = require('express')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const passport = require('./middleware/passport')
const index = require('./routes/index')
const app = express()
app.use(cookieParser())
app.use(express.bodyParser());
app.use(express.session({ secret: 'keyboard cat' }));
// 💡 passport
app.use(passport.initialize())
app.use(passport.session())
// routes
app.use('/', index)
// catch 404 and forward to error handler
app.use(function (req, res, next) {...})
// error handler
app.use(function (err, req, res, next) {...})
module.exports = app
解析 session
在一個典型的 web 應用中,帳號密碼這類的驗證訊息(credentials)只有在登入的時候會被傳送,如果驗證成功,會在 session 中留下記錄(例如 sessionId)並儲存 cookies 在瀏覽器中。後續的 request 都不會在帶有帳號密碼這些驗證資訊,而是透過 cookie 來辨認 session 。這麼做的好處是可以替 session 保留許多的空間。
預設 Passport 會把整個 user 實例都存放在 session 中,但這麼做佔用了 session 許多不必要的空間。為解決這樣的問題,Passport 可以透過序列化(serialize)的方式,只保存 UserId 在 session 中,當有需要更多使用者資訊時,再透過反序列化(deserialize)的方式,根據 User ID 把整個 user 物件實例取出。
序列化(serialize)簡單來說就是把「物件」轉換成可被儲存在儲存空間的「資料」的這個過程,例如把 JavaScript 中的物件透過
JSON.stringify()
變成字串,就可以存放在儲存空間內;而反序列化則反過來是把「資料」轉換成程式碼中的「物件」,例如把 JSON 字串透過JSON.parse()
轉換成物件。
在下面的例子中,只有 User ID(user.id
)被序列化後存到 session,當後續 requests 近來時,則透過 ID 來找到原本 user 的資訊,並存回 req.user
中:
// ./middleware/passport
const passport = require('passport');
// 只把 UserID 保存在 session 中
passport.serializeUser((user, done) => {
done(null, user.id);
});
// 透過 UserID 找回原本的 User 資料,並存放在 req.user 中
passport.deserializeUser(async (id, done) => {
const user = await User.findByPk(id);
return done(null, user.toJSON());
});
Passport-LocalStrategy
- 使用
passport.use(<strategy>)
來設定 strategy - 在 Strategy 中需要使用
verify callback
,當 passport authenticate 接收到一個 request 時,它會去解析 request 中和認證有關的訊息(credentials),接著它會把這些 credentials 作為代入 verify callback 的參數:- 如果 credentials 有效(valid),則會呼叫
return done(null, user)
- 如果 credentials 無效,則會呼叫並可顯示錯誤訊息
return done(null, false, {message: 'Wrong Password'})
- 如果在過程中發生例外,例如連不上 db ,則會呼叫
return done(err)
- 如果 credentials 有效(valid),則會呼叫
- 預設的情況下
LocalStrategy
會以username
和password
當作驗證的欄位,如果有變更的話,可以透過usernameField
和passwordField
來改變
// ./config/passport.js
// 使用 passport 的 LocalStrategy
const passport = require('passport');
const LocalStrategy = require('passport-local');
const bcrypt = require('bcryptjs');
const db = require('../models');
const { User } = db;
// setup passport strategy
passport.use(
new LocalStrategy(
// customize user field,預設使用 username 和 password 作為驗證的欄位
{
usernameField: 'email',
passportField: 'password',
passReqToCallback: true, // 如果需要在 verify callback 中取得 req
},
// customize verify callback
// 因為上面有註明 passReqToCallback: true,所以第一個參數會是 req
async (req, username, password, done) => {
try {
const user = await User.findOne({ where: { email: username } });
if (!user) {
return done(
null,
false,
// { message: 'Incorrect username.' }
req.flash('error_messages', '帳號或密碼輸入錯誤'),
);
}
if (!bcrypt.compareSync(password, user.password)) {
return done(
null,
false,
// { message: 'Incorrect password.' }
req.flash('error_messages', '帳號或密碼輸入錯誤'),
);
}
return done(null, user, req.flash('success_messages', '登入成功'));
} catch (error) {
return done(error);
}
},
),
);
登出(Logout)
若想要登出,只需要呼叫 req.logout()
即可:
// controller
app.get('/logout', (req, res) => {
req.logout(); // 由 passport 提供
res.redirect('/signin');
});
驗證權限(authenticate)
若要驗證使用者有無登入,可以使用 req.isAuthenticated()
這個方法,之後則可以透過 req.isAuthenticated
判斷使用者的登入狀態:
// controller
// 驗證使用者有無登入,可以使用 req.isAuthenticated() 這個方法
const authenticated = (req, res, next) => {
if (req.isAuthenticated()) {
return next();
}
return res.redirect('/signin');
};
const authenticatedAdmin = (req, res, next) => {
if (req.isAuthenticated()) {
if (req.user.isAdmin) {
return next();
}
return res.redirect('/');
}
return res.redirect('/signin');
};
app.get('/dashboard', authenticated, (req, res, next) => {...});
app.get('/admin/dashboard', authenticatedAdmin, (req, res, next) => {...});
JwtStrategy
new JwtStrategy(options, verify)
- options
secretOrKey
: 必填欄位jwtFromRequest
: 用來代入驗證的函式- verify 是一個 function
verify(jwt_payload, done)
- payload 是解碼後的 JWT payload
- done 是一個
callback<error, user, info>
- 找出 JWT(Extractor)的方式包含
fromHeader(header_name)
: 從指定的 http header name 中找 JWTfromBodyField(field_name)
: 從 body 的欄位中找 JWTfromUrlQueryParameter(param_name)
: 從 URL 的 query parameter 中找 JWTfromAuthHeaderWithScheme(auth_scheme)
: 從 authorization header 中找 JWTfromAuthHeader()
: 以 scheme 'JWT' 尋找 authorization header(HTTP Header 的寫法要是{Authorization: JWT xxx.yyy.zzz}
)fromExtractors([array of extractor functions])
可以用陣列的方式列出所有上述想要使用的方法
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const opts = {
secreteOrKey: jwtConfig.secret,
jwtFromRequest: ExtractJwt.fromExtractors([
ExtractJwt.versionOneCompatibility({ authScheme: 'Bearer' }),
ExtractJwt.fromAuthHeader(),
]),
};
let jwtStrategy = new JwtStrategy(opts, function (payload, done) {
User.findById(payload.sub, function (err, user) {
if (err) return done(err);
if (!user) return done(null, false, { message: 'Wrong JWT Token' });
if (payload.aud !== user.email) return done(null, false, { message: 'Wrong JWT Token' });
const exp = payload.exp;
const nbf = payload.nbf;
const current = ~~(new Date().getTime() / 1000);
if (current > exp || current < nbf) {
return done(null, false, 'Token Expired');
}
return done(null, user);
});
});
passport.use('jwt', jwtStrategy);