Next.jsでsessionとPassport.jsを使う
Next.jsでsessionとかPassport.js(認証)を使う方法を書いてみる
next.js session
とか next.js 認証
とかでググるとNextにexpressを入れる感じでのサンプルは結構あったけど、デフォルトで用意されているAPI Routeを利用したやつの実装のサンプルがあまり見つけれなかったので書いてみる
Next.js では connect というmiddlewareレイヤーに対応していて、これに対応しているライブラリなどであれば利用することが出来ます
サンプルは下記の配置で書いていきます
- middleweres
- index.ts
- connect.ts
- passport.ts
- session.ts
- page
- api
- userInfo.ts
- login.ts
- logout.ts
- api
利用しているライブラリ
- express-session (expressとか書いてるけど問題なくつかえる
- connect-redis (今回のサンプルではsessionをredisで管理するようにしているので
- passport (認証
- passport-local (認証を独自ロジックで行いたいので
yarn add -D express-session connect-redis passport passport-local
Middlewareの前準備
connectするための関数を定義
import { NextApiRequest, NextApiResponse } from 'next'
export const connectMiddleware = (req: NextApiRequest, res: NextApiResponse, middleware: Function) => {
return new Promise((resolve, reject) => {
middleware(req, res, (result: any) => {
if (result instanceof Error) {
return reject(result)
}
return resolve(result)
})
})
}
https://nextjs.org/docs/api-routes/api-middlewares#connectexpress-middleware-support
これを関数化しただけ
sessionを定義
import { NextApiRequest, NextApiResponse } from 'next'
import session from 'express-session'
import { createClient } from 'redis'
import { connectMiddleware } from './connect'
const RedisStore = require('connect-redis')(session)
export type Session = { session: { [key: string]: any } }
// 関数の外で初期化しないと、毎回redisにconnection を貼るので関数外に書く
const redis = createClient({
host: '0.0.0.0',
port: 6380,
prefix: 'fuga:',
})
// このへんよしなに。開発用の設定になってる
const config = {
saveUninitialized: true,
secret: 'keyboard cat',
resave: false,
store: new RedisStore({
client: redis,
}),
cookie: {
httpOnly: true,
sameSite: true,
secure: !isDev,
},
proxy: true // これがないと本番時に cookie.secure: true でCookieがセットできない
}
export const withSession = async (req: NextApiRequest, res: NextApiResponse) => {
await connectMiddleware(req, res, session(config))
}
Passport.jsを定義
import { NextApiRequest, NextApiResponse } from 'next'
import { connectMiddleware } from './connect'
const passport = require('passport')
const LocalStrategy = require('passport-local').Strategy
// passport.d.ts の上書きがめんどいのでこれで(さぼり)
export type PassportFunctions = {
authInfo?: any
user?: User
login(user: {id: number, name: string}, done: (err: any) => void): void
login(user: {id: number, name: string}, options: any, done: (err: any) => void): void
logIn(user: {id: number, name: string}, done: (err: any) => void): void
logIn(user: {id: number, name: string}, options: any, done: (err: any) => void): void
logout(): void
logOut(): void
isAuthenticated(): boolean
isUnauthenticated(): boolean
}
passport.use(
new LocalStrategy(
{
usernameField: 'id',
passwordField: 'passport',
},
async (username: string, password: string, done: Function) => {
// なんか認証してユーザーを取得するやつ
const user: {id: number, user: string} | null = await authUser({username, password})
// 取得エラー
if (!result) {
done(null, false)
return
}
// IDと名前
done(null, user)
}
)
)
passport.serializeUser((user: User, done: Function) => {
done(null, user)
})
passport.deserializeUser((user: User, done: Function) => {
done(null, user)
})
export const withPassport = async (req: NextApiRequest, res: NextApiResponse) => {
await connectMiddleware(req, res, passport.initialize())
// Passport.jsでセッションを使いたいので
await connectMiddleware(req, res, passport.session())
}
Passport.js(認証) & sessionを使うための高階関数を定義
import { NextApiRequest, NextApiResponse } from 'next'
import { Session, withSession } from './session'
import { PassportFunctions, withPassport } from './passport'
// デフォルトのNextApiRequestとライブラリで拡張されたのをくっつけて再定義
type Request = NextApiRequest & Session & PassportFunctions
type Response = NextApiResponse & Session
type Options = {
requiredAuth: boolean // 認証がされているかどうか、されていなければ403を返す
}
export const withApiMiddlewares = (options: Options) => (fn: (req: Request, res: Response) => void) => {
return async (req: Request, res: Response) => {
await withSession(req, res)
await withPassport(req, res)
// 認証していなければ403を返す
if (options.requiredAuth && !req.isAuthenticated()) {
res.status(403).json({ message: 'Forbidden' })
return
}
fn(req, res)
}
}
あとは withApiMiddlewares を使うことでAPIの中でsessionやPassport.js(認証)を使うことが可能になった
withApiMiddlewares の第一引数はライブラリのオプションなどで利用。第2引数はメインの処理を書く感じ
下記が例
export default withApiMiddlewares({ requiredAuth: true, methods: ['GET', 'POST'] })(async (req, res) => {
res.status(200).json({
message: 'ok'
})
})
こんな感じで使える。第一引数のmethodsみたいな感じで利用できるHttpMethodを限定するみたいな拡張をしてもいいかも
高階関数にすることで、req resの型定義も勝手にされるのでサボれる
ミドルウェアを使ったAPIの例
ユーザーの情報を返すAPI
import { withApiMiddlewares } from '../../middlewares'
export default withApiMiddlewares({ requiredAuth: true })(async (req, res) => {
res.status(200).json({
user: req.user,
})
})
ログインのやつ
import passport from 'passport'
import { withApiMiddlewares } from '/middlewares'
import { connectMiddleware } from '/middlewares/connect'
export default withApiMiddlewares({ requiredAuth: false })(async (req, res) => {
await connectMiddleware(
req,
res,
passport.authenticate('local', (err: any, user: {id: number, name: string} | null) => {
if (err || !user) {
res.writeHead(302, { Location: '/' }).end()
return
}
req.logIn(user, (error: any) => {
if (error) {
res.writeHead(302, { Location: '/' }).end()
return
}
res.writeHead(302, { Location: '/loggedInPage' }).end()
})
})
)
})
ログアウトのやつ
import { withApiMiddlewares } from '../../middlewares'
export default withApiMiddlewares({ requiredAuth: false })(async (req, res) => {
req.logout()
res.status(200).end()
})
結構コードがメインの記事になりましたが、これで利用可能です
next-connect というライブラリもあり、これを使うのもありかも。
Next.jsのサンプルでも使われてたりはします https://github.com/zeit/next.js/tree/canary/examples
以上。ご査収のほどよろしくお願い致します