[TypeScript]express-sessionを使ったセッション維持

前回は、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');
});
スポンサーリンク

シェアする

  • このエントリーをはてなブックマークに追加

フォローする

スポンサーリンク