📌

(EN) Express+Passport with multiple user sessions

2023/10/30に公開

sample use case

Let's say you have one backend and multiple front ends and each front end has its own user type.
The backend is going to have one path for each user like this:

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

Each of them will need to have a different session, with different cookies, end even if you log in for /admin it is necessary for you to be "logged out" of the other paths.

splitting the passport

The first issue you are going to face is the fact that when you import the passport you will be importing an instantiated Passport object and not a passport class.

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

All you need to fix this is to import the class and create your own instance.

import { Passport } from 'passport'

const passport = new Passport()

you will also need to split the passport.use() and give a different key for each one.

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

After you've done this your file will probably look something like this:

/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

And you will finally be able to split your passport sessions on the app

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())

splitting the routers

The first thing to do is give each path its own router

/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)

Each router will have to then import its own passport, and use its authentication mehod

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 }

splitting up the middleware

After all this your index file will probably be something along these lines:

/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())

Because you are still using the same middleware if you are loggen in as an "admin" it will still think you are logged in when you access /developer.
There is only two spots you have to change when splitting the middleware, and that is the session's key and name

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

In my case I decided to use the generate-api-key library.

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

When you split the middleware it will change to something like this:
####/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' }),
  ...
})

You can also manually add a different api key for each file.

Finally

After splitting the middleware you should be good to go!

/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}.`)
})

And with this even if you log in as an admin, all the other paths will still return unauthenticated.

Discussion