LoginSignup
14
12

More than 3 years have passed since last update.

Next.jsでsessionとPassport.js(認証)を使う

Last updated at Posted at 2020-05-17

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

利用しているライブラリ

  • express-session (expressとか書いてるけど問題なくつかえる
  • connect-redis (今回のサンプルではsessionをredisで管理するようにしているので
  • passport (認証
  • passport-local (認証を独自ロジックで行いたいので
yarn add -D express-session connect-redis passport passport-local

Middlewareの前準備

connectするための関数を定義

connect.ts
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を定義

session.ts
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を定義

passport.ts
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を使うための高階関数を定義

index.ts
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
page/api/userInfo.ts
import { withApiMiddlewares } from '../../middlewares'

export default withApiMiddlewares({ requiredAuth: true })(async (req, res) => {
  res.status(200).json({
    user: req.user,
  })
})

ログインのやつ

page/api/loggin.ts
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()
      })
    })
  )
})

ログアウトのやつ

page/api/logout.ts
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

以上。ご査収のほどよろしくお願い致します

14
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
12