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

# Next.jsでsessionとPassport.jsを使う

Next.jsでsessionとかPassport.js(認証)を使う方法を書いてみる


`next.js session` とか `next.js 認証` とかでググるとNextにexpressを入れる感じでのサンプルは結構あったけど、デフォルトで用意されているAPI Routeを利用したやつの実装のサンプルがあまり見つけれなかったので書いてみる

Next.js では [connect](https://nextjs.org/docs/api-routes/api-middlewares#connectexpress-middleware-support) という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 （認証を独自ロジックで行いたいので

```sh
yarn add -D express-session connect-redis passport passport-local
```


## Middlewareの前準備

#### connectするための関数を定義

```ts: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を定義

```ts: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を定義

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

```ts: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 の第一引数はライブラリのオプションなどで利用。第２引数はメインの処理を書く感じ

下記が例

```ts
export default withApiMiddlewares({ requiredAuth: true, methods: ['GET', 'POST'] })(async (req, res) => {
  res.status(200).json({
    message: 'ok'
  })
})
```

こんな感じで使える。第一引数のmethodsみたいな感じで利用できるHttpMethodを限定するみたいな拡張をしてもいいかも
高階関数にすることで、req resの型定義も勝手にされるのでサボれる

## ミドルウェアを使ったAPIの例

##### ユーザーの情報を返すAPI

```ts:page/api/userInfo.ts
import { withApiMiddlewares } from '../../middlewares'

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

```


#### ログインのやつ

```ts: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()
      })
    })
  )
})
```

#### ログアウトのやつ

```ts:page/api/logout.ts
import { withApiMiddlewares } from '../../middlewares'

export default withApiMiddlewares({ requiredAuth: false })(async (req, res) => {
  req.logout()
  res.status(200).end()
})

```

結構コードがメインの記事になりましたが、これで利用可能です
[next-connect](https://www.npmjs.com/package/next-connect) というライブラリもあり、これを使うのもありかも。
Next.jsのサンプルでも使われてたりはします https://github.com/zeit/next.js/tree/canary/examples

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