Zeit Now + Next.js のページにFirebase認証を導入した際、環境変数にハマったメモ

## 概要
* Zeit Now + Next.jsの開発環境ができた([*1][*1])([*2][*2])ので、認証を試してみた。
  *  [試してみたソースコード](https://github.com/hibohiboo/create-now/tree/19af4015f5b4f693b5991f0417e251ad5031fe84/) 
* [▲🔥 Getting started with Next.js, ZEIT Now, and Firebase](https://dev.to/benzguo/getting-started-with-next-js-now-firebase-4ejg)の記事を参考に作成
* firebaseの認証は以前に行った([*3][*3])ものを参考にして構築したので割愛。
  * Twitter認証で使用するため、`/infos/agreement`と`/infos/privacy-policy`のページを作成
* 環境変数まわりでハマったため、そのあたりのメモ

## フォルダ構成

```yml
- .env # 環境変数設定ファイル
- .env.build
- now.json # now設定ファイル
- next.config.js # next設定ファイル
- api
  - login.ts # セッション用API (cookieを使用。トークンをデコードしてユーザ情報を返却）
  - logout.ts # セッション終了API
- src
  - pages
    - index.tsx # トップページ
    - _document.tsx # テンプレート
    - auth
      - login.tsx # ログインページ
    - mypage
      - index.tsx # ログイン時のみ閲覧可能なページ
    - infos
      - agreement.tsx # 利用規約
      - privacy-policy.tsx # プライバシーポリシー
  - utils
    - auth
       - firebaseAdmin.ts # トークン検証（API用）
       - firebaseSessionHandler # セッション用APIを叩く
       - hooks.ts # コンテクスト作成用
       - initFirebase.ts # firebase初期化
       - logout.ts # ログアウト処理
       - user.ts # ユーザ。firebaseの認証ユーザから必要な情報のみを抽出
     - middleware
       - commonMiddleware.ts # middlewareとして登録(API用)
       - cookieSession.ts # セッションを追加する（API用）
       - cookiSessionRefresh.ts # cookie初期化(API用)
     - pageWrappers
       - withAuthUser.tsx # サーバ側・クライアント側それぞれでユーザ情報を取得
       - withAuthUserInfo.tsx # コンテクスト作成関数を呼び出して、コンテクストをNextのコンポーネントに追加
```

## 環境変数について
* ローカルの開発環境と、デプロイ先の環境で設定方法が違ってけっこうハマった
  * デプロイ先の環境変数が設定できていないときに出力されたエラーは以下。

```
Error occurred prerendering page "/auth/login". Read more: https://err.sh/next.js/prerender-error:
Error: Failed to render serverless page
> Build error occurred
Error: Export encountered errors
    at _default (/zeit/2856d89b/node_modules/next/dist/export/index.js:19:1086)
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (internal/process/task_queues.js:94:5)
    at async build (/zeit/2856d89b/node_modules/next/dist/build/index.js:36:218)
````

### ローカル開発環境での環境変数について

.envファイルに記述、next.config.jsファイルでnextに適用する。

```conf:.env
FIREBASE_API_KEY=xXxXxX
FIREBASE_AUTH_DOMAIN=XXX.firebaseapp.com
FIREBASE_PROJECT_ID=XXX
# == Firebase admin keys (from serviceAccount-staging.json) ==
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-■■■■@■■■■■■■■.iam.gserviceaccount.com
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n■■■■■■■■\n-----END PRIVATE KEY-----\n"
# Cookie
SESSION_SECRET=secretHOGEHOGE
SESSION_SECRET_PREVIOUS=secretFUGAFUGA
```

```diff:next.config.js
/* eslint-disable @typescript-eslint/no-var-requires */
const { resolve } = require('path')
const withOffline = require('next-offline')
+ require('dotenv').config()

const nextConfig = {
  generateInDevMode: true,
  workboxOpts: {
    swDest: 'static/service-worker.js',
  // 省略
  },
+  // .envファイルから読み込んだ環境変数をnextで使用できるように定義
+  env: {
+    FIREBASE_API_KEY: process.env.FIREBASE_API_KEY,
+    FIREBASE_AUTH_DOMAIN: process.env.FIREBASE_AUTH_DOMAIN,
+    FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID,
+    FIREBASE_CLIENT_EMAIL: process.env.FIREBASE_CLIENT_EMAIL,
+    FIREBASE_PRIVATE_KEY: process.env.FIREBASE_PRIVATE_KEY,
+    SESSION_SECRET: process.env.SESSION_SECRET,
+    SESSION_SECRET_PREVIOUS: process.env.SESSION_SECRET_PREVIOUS,
+  },
}

// PWA に対応
module.exports = withOffline(nextConfig)
```

### デプロイ時の環境変数について

* secretsに登録して、now.jsonで設定する( [*](https://zeit.co/docs/v2/build-step#environment-variables) ) ( [**](https://zeit.co/blog/environment-variables-secrets) ) ( [***](https://zeit.co/blog/environment-variables-secrets) )

```json:now.json
{
  "version": 2,
  "routes": [
    {
      "src": "^/service-worker.js$",
      "dest": "/_next/static/service-worker.js",
      "headers": {
        "cache-control": "public, max-age=43200, immutable",
        "Service-Worker-Allowed": "/"
      }
    }
  ],
  "functions": {
    "api/pdf.ts": {
      "includeFiles": "fonts/**"
    }
  },
  "env": {
    "FIREBASE_API_KEY": "@firebase-api-key",
    "FIREBASE_AUTH_DOMAIN": "@firebase-auth-domain",
    "FIREBASE_PROJECT_ID": "@firebase-project-id",
    "FIREBASE_CLIENT_EMAIL": "@firebase-client-email",
    "FIREBASE_PRIVATE_KEY": "@firebase-private-key",
    "SESSION_SECRET": "@session-secret",
    "SESSION_SECRET_PREVIOUS": "@session-secret-previous"
  },
  "build": {
    "env": {
      "FIREBASE_API_KEY": "@firebase-api-key",
      "FIREBASE_AUTH_DOMAIN": "@firebase-auth-domain",
      "FIREBASE_PROJECT_ID": "@firebase-project-id",
      "FIREBASE_CLIENT_EMAIL": "@firebase-client-email",
      "FIREBASE_PRIVATE_KEY": "@firebase-private-key",
      "SESSION_SECRET": "@session-secret",
      "SESSION_SECRET_PREVIOUS": "@session-secret-previous"
    }
  }
}

```

* secretsへの登録はコマンドを使用する

```
now secrets add firebase-api-key xXxXxX
now secrets add firebase-auth-domain XXX.firebaseapp.com
now secrets add firebase-project-id XXX
now secrets add firebase-client-email firebase-adminsdk-■■■■@■■■■■■■■.iam.gserviceaccount.com
now secrets add firebase-private-key -- "-----BEGIN PRIVATE KEY-----\n■■■■■■■■\n-----END PRIVATE KEY-----\n"
now secrets add session-secret secretHOGEHOGE
now secrets add session-secret-previous secretFUGAFUGA
```


  * 秘密鍵の登録には、 ` -- ` オプションを使わないと以下のようなエラーとなる

```
Now CLI 18.0.0
Error! Invalid number of arguments. Usage: `now secret add <name> <value>`
If your secret has spaces or starts with '-', make sure to terminate command options with double dash and wrap it in quotes. Example:
  $ now secret add -- "test"
```


#### 設定したsecretsの確認
`now secrets ls`コマンドで登録したsecretsを確認できる。

```
root@6ad82688728c:/app# now secrets ls
Now CLI 18.0.0
x secrets found under xxx[527ms]

  name                                     created
  firebase-private-key                     2h ago
  session-secret                           3h ago
  session-secret-previous                  14h ago
  firebase-client-email                    14h ago
  firebase-project-id                      14h ago
  firebase-auth-domain                     14h ago
  firebase-api-key                         14h ago
```

#### 秘密鍵の改行コードについて
* `now secrets add`で登録した秘密鍵は、`\n`が`\\n`として登録されてしまう。
  * ローカルで`.env`ファイルから読み込んだときには発生しない状況。本番だけなのでハマった。 
* 使用時には、以下のように`FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n')`で置換する必要があった。
  * 改行込みで環境変数に登録できないものか。。。

```ts
import * as admin from 'firebase-admin'

export const verifyIdToken = (token: string) => {
  if (!admin.apps.length) {
    const cert = {
      projectId: process.env.FIREBASE_PROJECT_ID,
      clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
      // zeit now の環境変数だと\nが\\nにエスケープされてしまっているので元に戻す
      privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
    }
    admin.initializeApp({
      credential: admin.credential.cert(cert),
    })
  }

  return admin
    .auth()
    .verifyIdToken(token)
    .catch((error) => {
      throw error
    })
}
```

## 環境

- Windows 10 Home
- virtualbox 6.1.6
- Vagrant 2.2.7
- ubuntu 18.04 LTS
- docker-ce Docker version 19.03.5, build 633a0ea838
- docker-compose version 1.25.3, build d4d1b42b
- node 13.13
  - now-cli: 18.0 
  - next: 9.3.4
  - react: 16.13.1
  - react-dom: 16.13.1
  - @reduxjs/toolkit: 1.3.4
  - react-redux: 7.2.0
  - firebase: 7.14.0
  - firebase-admin: 8.10.0
  - react-firebaseui: 4.1.0
  - typescript: 3.8.3


## 参考

[[RS256] JWTでRSA秘密鍵を環境変数で処理したい [Javascript]](https://qiita.com/sho7650/items/1dd65a1db785f902a2d6)
[Zeit Nowの具体的なTips集](https://qiita.com/mt0m/items/3e58d6185a5335729ccc)
[▲🔥 Getting started with Next.js, ZEIT Now, and Firebase](https://dev.to/benzguo/getting-started-with-next-js-now-firebase-4ejg)
[Next.js プロジェクトに Firebase Authentication を組み込み](https://nekorokkekun.hatenablog.com/entry/2019/09/25/000041)
[firebase](https://qiita.com/gagagaga_dev/items/a8dd490114c315329279)
[next example](https://github.com/zeit/next.js/blob/canary/examples/with-firebase-authentication/pages/index.js)
[React で Firebase Authentication を使う](https://qiita.com/zaburo/items/801bd288cec47bd28764)
[Next.js, Material-UI v4-alpha And React Hook With Firebase Hosting example](https://github.com/scarlet-sage/React-Hooks-Nextjs-Material-Ui-next-With-Firebase-Hosting)
[react-redux-firebase](http://react-redux-firebase.com/)
[Storing complex secrets ](https://github.com/zeit/now/issues/749)
[Docker で zeit nowのデプロイ環境とNext.jsの開発環境を作ったメモ][*1]
[PdfMakeを使ったPDF作成APIをZeit Nowで動かしたメモ][*2]



[*1]:https://qiita.com/hibohiboo/items/e6c30e76e26dde0e8611
[*2]:https://qiita.com/hibohiboo/items/9123fc6ecb546c5820e3
[*3]:https://qiita.com/hibohiboo/items/11768cb10bf1e7025090