📌

(JP) Express+Passport複数のユーザーSessionで起動

2023/10/30に公開

ユーズケース

ひとつのバックエンドと複数のフロントがあり、各フロントが独自のユーザーを持っているとします
バックエンドにはユーザーごとに別々ののPATHが設定されます

https://something/admin
https://something/developer
https://something/a-type-user
https://something/b-type-user

それぞれのセッションは異なるCookieと異なるSessionを持つ必要があり、/adminにログインしてたとしても、他のパスからは「ログアウト」されてる必要があります。

パスポートを分ける

最初に直面するべき問題はPassportをインポートするときにクラスではなく、インスタンス化されたPassportオブジェクトをインポートされてることです

// returns the same object every time
import passport from 'passport'

修正するには、クラスをインポートして独自のインスタンスを作成することです

import { Passport } from 'passport'

const passport = new Passport()

またpassport.use() を分割し、それぞれに異なるキーを与える必要があります。

// src/admin/passport.ts
passport.use('admin', new Strategy(verifyUserFunction))
// src/developer/passport.ts
passport.use('developer', new Strategy(verifyUserFunction))

完了したらファイルがこのようになります:

/src/admin/passport.ts

import { Passport } from 'passport'
import { Strategy, VerifyCallback } from 'passport-custom'

const passport = new Passport()

const verifyUser: VerifyCallback = async (req, done) => {
    ...
}

passport.serializeUser<UserInSession>((user, done) => {
  done(null, user as UserInSession)
})

passport.deserializeUser<UserInSession>(async (userInSession, done) => {
  done(null, userInSession)
})

passport.use('admin', new Strategy(verifyUser))

export passport

そしてついにアプリ上でPassportのSessionを分割できるようになります

src/index.ts

import express from 'express'
import { passport as adminPassport } from './admin/passport'
import { passport as developerPassport } from './developer/passport'

const app = express()

app.use('/admin', adminPassport.session())
app.use('/developer', developerPassport.session())

ルーターを分ける

最初に必要なのは、各パスに独自のルーターを与えることです

/src/index.ts

import express from 'express'
import { Router as AdminRouter } from './admin/router'
import { Router as DeveloperRouter } from './developer/router'

const app = express()

app.use('/admin', AdminRouter)
app.use('/developer', DeveloperRouter)

各ルーターは独自のパスポートをインポートし、その認証方法を使用する必要があります。

src/admin/router

import { Router } from 'express'
import { passport } from './passport'

const router = Router()

// the name 'admin' was set up with passport.use()
router.post('/login', passport.authenticate('admin'), (req, res) => {
  return res.json(req.user)
})

...

export { router }

ミドルウエアを分ける

ここまでくると、index.tsがこのようなものになってるはずです

/src/index.ts

import express from 'express'
import { sessionMiddleware } from './middleware'
import { Router as AdminRouter } from './admin/router'
import { passport as adminPassport } from './admin/passport'
import { Router as DeveloperRouter } from './developer/router'
import { passport as developerPassport } from './developer/passport'

const app = express()

app.use(sessionMiddleware)

app.use('/admin', AdminRouter)
app.use('/admin', adminPassport.session())

app.use('/developer', DeveloperRouter)
app.use('/developer', developerPassport.session())

Adminとしてログインしていても同じMiddlewareを使用しているため、/developerのpathにアクセスしようとしてもログインしていると認識されます
Middlewareを分割するに変更するのは二箇所のセッションキーと名前だけです

export const sessionMiddleware = session({
  // add the name parameter
  store: new RedisStore({ client: redisClient }),
  secret: 'some-random-session-key', // change here
  ...
})

僕はこの場合generate-api-keyのライブラリを使うことにしました.

yarn add generate-api-key
// or
npm install generate-api-key

Middlewareを分割するとこのようになります
####/src/admin/middleware

import generateApiKey from 'generate-api-key';

export const sessionMiddleware = session({
  name: 'admin-session',
  store: new RedisStore({ client: redisClient }),
  secret: generateApiKey({ prefix: 'admin' }),
  ...
})

ファイルごとに手入力でセッションキーを手動で追加することも可能です

Finally

Middlewareを分割し終わったら、準備完了のはずです!

/src/index.ts

import express from 'express'
import { sessionMiddleware as AdminMiddleware } from './admin/middleware'
import { Router as AdminRouter } from './admin/router'
import { passport as adminPassport } from './admin/passport'

import { sessionMiddleware as DeveloperMiddleware } from './developer/middleware'
import { Router as DeveloperRouter } from './developer/router'
import { passport as developerPassport } from './developer/passport'

const app = express()

app.use('/admin', AdminMiddleware)
app.use('/admin', AdminRouter)
app.use('/admin', adminPassport.session())

app.use('/developer', DeveloperMiddleware)
app.use('/developer', DeveloperRouter)
app.use('/developer', developerPassport.session())

app.listen(port, () => {
    console.log(`Server is listening at port ${port}.`)
})

これで管理者としてログインしてても、他のすべてのPATHはUnauthenticatedと返します。

Discussion