前回は、passport-localだけを用いて認証を行いました。
これだと、認証ができるのはID/PWを入力した直後だけで、他のページに遷移した場合にまた認証が必要になります。
そこで、セッションを用いて認証を継続するように拡張してみましょう。
まず、保持するユーザー型ですが、単にIDだけだと非常に不便です。
作成するWebアプリで必要な権限などのユーザーのさまざまな情報が格納できると、それを用いた処理が行えるようになります。
今回は、MyUser型を作成しました。
type MyUser = { id: number username: string role: string // 管理者か一般ユーザーかなどの独自の権限情報を入れる想定 }
作成したMyUserをセッションに格納できるように、SessionDataの拡張を行います。
declare module 'express-session' { interface SessionData { userSession: MyUser } }
また、ExpressのRequestオブジェクトに生えているUserに、追加した独自のMyUserのプロパティを追加しておきます。
これにより、req.user.role の形で独自プロパティが利用可能になります。
declare global { namespace Express { interface User { id: number; username: string; role: string; } } }
前回も書いたように、これらのファイルは別ファイルにして、tsconfigのtypesやtypeRootsで読み込むようにすると便利です。
sessionの利用宣言や、passportとの紐付けはコードに書いてある通りです。
重要なのは、sessionへの書き込みと読み込みの部分です。
serializeUserのコールバックに渡る第一引数userは、Express.User型です。
これには、最初の設定で独自プロパティを生やしているので、user.roleといった形で型安全にアクセスが可能になります。
passport.serializeUser((user, done) => { const myUser = {id: user.id, username: user.username, role: user.role}; done(null, {passport: {user: myUser}}); })
セッションの読み込み部分では、ジェネリックとしてSessionDataを指定します。
すると、SessionData.userSessionに型安全にアクセスできるので、そこに格納した独自ユーザーを取り出すことができるようになります。
passport.deserializeUser((sessionData, done) => { const myUser = sessionData.userSession done(null, myUser); })
いちど/loginで認証後に/profileにアクセスすると、セッションデータからユーザーが復元されて、認証が続いているのがわかります。
import express from 'express' import passport from 'passport' import {Strategy as LocalStrategy} from 'passport-local' import session, {SessionData} from "express-session"; type MyUser = { id: number; username: string; role: string; } declare module 'express-session' { interface SessionData { userSession: MyUser } } declare global { namespace Express { interface User { //user: MyUser; id: number; username: string; role: string; } } } const app = express(); app.use(express.urlencoded({extended: false})); app.use(session({ secret: 'your-secret-key', resave: false, saveUninitialized: false })) passport.use(new LocalStrategy( (username, password, done) => { if (username === 'testuser' && password === 'testpass') { const myUser = {id: 1, username: 'testuser', role: 'admin'}; return done(null, myUser); } return done(null, false); } )); app.use(passport.initialize()); app.use(passport.session()); passport.serializeUser((user, done) => { const myUser = {id: user.id, username: user.username, role: user.role}; console.log('serializeUser', myUser); done(null, {userSession: {user: myUser}}); }) passport.deserializeUser<SessionData>((sessionData, done) => { const myUser = sessionData.userSession done(null, myUser); }) app.post('/login', (req, res, next) => { console.log(req.body); next(); }, passport.authenticate('local', { session: true, failureMessage: 'Login failed', }), (req, res) => { res.send({LoginUser: req.user}) } ) app.get('/profile', (req, res) => { if (req.isAuthenticated()) { res.send({user: req.user}); } else { res.sendStatus(401); } }) app.listen(3000, () => { console.log('Server is running on port 3000'); });