Nuxt2 + express + express-openid-connect の認証について

# はじめに
Nuxt.jsでSSRを使ったアプリケーションを作成する時に、express-openid-connectを使った認証を実装する際につまづいた部分があったので、自分自身の備忘録として、また同じ所でつまづいた人に対して、何かのヒントになればと思い記事を書きます
備忘録で大事な部分は「🔑」注意する部分は「🚨」を記載します

記事の内容としては実際にNuxtのアプリケーションを作りながら説明をしていく流れになります
🚨 Nuxt2でのアプリケーション作成となります

## 主な技術
node : v16.14.2
npm : v8.5.0
yarn : 1.22.18
Nuxt : 2.15.8
Express : 4.18.1

## ここでは書かない事
Nuxtの細かい設定・操作
Expressの細かい設定・操作
OAuth自体の設定

## 認証までのフロー
Nuxtで認証を行うのはバックエンドとフロントエンドそれぞれ、認証させている事を認識させる必要があります。
🚨 フローの流れはあくまでイメージになります

![スクリーンショット 2022-05-09 15.07.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211927/b0d5ee74-b2d7-c1bf-70ee-7de7e40d7cf1.png)


## Nuxtプロジェクトの作成
作りたいディレクトリ上でプロジェクトを作成します
今回は「oauth-app」というアプリケーション名とします

公式：https://nuxtjs.org/docs/get-started/installation/

yarn実行後、色々と質問をされるので、 基本は全て、Enterで良いですが、以下の質問は下記のように選択して下さい
+ Nuxt.js modules: Axios - Promise based HTTP client　（スペースキーで選択）
+ Rendering mode: Universal (SSR / SSG)
+ Deployment target: Server(Node.js hosting)

``` 
yarn create nuxt-app oauth-app
```

処理が終わりましたら、問題なく起動できるか確認をします。
🔑 dockerで環境を作っている場合、「nuxt.config.js」のserverの追記を忘れずに
```
cd oauth-app
yarn dev
```

起動が、ブラウザでアクセスをして画面が表示されればプロジェクト作成は完了です
説明上は「localhost:3000」とします
![スクリーンショット 2022-05-09 13.22.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211927/de59a8cd-5220-37ec-8dfa-02729776bb00.png)

## BFFの設定
まずはバックエンドの設定、準備を行います

### Expressの導入
BFFを使用するために Express をインストールします

https://www.npmjs.com/package/express

```
yarn add express
```

「nuxt.config.js」に「serverMiddleware」の設定を追加
https://nuxtjs.org/docs/configuration-glossary/configuration-servermiddleware/

``` nuxt.config.js

serverMiddleware: [
  '~/server'
]
...
```

プロジェクト内にフォルダと以下のファイルを作成
``` server / index.js
import express from 'express'
const app = express();

//動作確認用 確認できたら消しても良いです。
app.get("/test", function(req, res){
  res.json({"value" : 1});
});

module.exports = {
  path: '/api',
  handler: app,
}
```

再度、Nuxtを起動して以下のURLをブラウザアクセスし、表示されればExpressの導入は完了です
http://localhost:3000/api/test
```
{"value":1}
```

### express-openid-connect の導入
今回の本命である express-openid-connect をインストールしていきます
https://github.com/auth0/express-openid-connect
```
yarn add express-openid-connect
```

インストール後、「server / index.js」に下記を追記
🔑 configの値は環境変数から取得する方法が良い

``` server / index.js
import express from 'express'

const {auth, requiresAuth } = require('express-openid-connect'); //追加

//追加
//値等は公式ドキュメントを参考に設定
//https://auth0.github.io/express-openid-connect/interfaces/configparams.html
const config = {
  authRequired: false,
  baseURL: '<baseURL>',
  clientID: '<clientID>',
  issuerBaseURL: '<issuerBaseURL>',
  secret: '<secret>',
  clientSecret: '<clientSecret>',
  idpLogout: true,
  authorizationParams: {
    response_type: '<response_type>',
    scope: '<scope>',
    audience: '<audience>',
    prompt: '<prompt>',
    redirect_uri: '<redirect_uri>/api/auth/callback',
  },
  routes: {
    login: false,
    callback: '/auth/callback',
  },
  afterCallback: async (req, res, session, decodedState) => {
    return {
      ...session,
    };
  }
};

const app = express();

app.use(auth(auth_config)); //追加

//追加
app.get('/login', (req, res) => {
  res.oidc.login({
    returnTo: '/callback'
  });
});

//追加
app.get('/callback', requiresAuth(), (req, res) => {
  const token = req.oidc.accessToken.access_token
  res.json({
    token: token,
    login: true
  });
});

//動作確認用
app.get("/test", function(req, res){
  res.json({"value" : 1});
});

module.exports = {
  path: '/api',
  handler: app,
}
```

🔑 認証後のパスの変更方法
OAuthで認証後、でフォルデのリダイレクトするパスは「/callback」になります。Expressのみでしたら、問題ないのですが、今回はNuxtでのアプリケーションなので「/callback」の場合フロントを表示するパスになります。その場合、認証情報を「express-openid-connect」がいい感じに保持してくれません。このパスを変更する場所が、公式ドキュメントも、Gitを見ても分からず結構時間を要しました。結論、「authorizationParams.redirect_uri」で指定することで変更することができました。

設定後、Nuxtを起動して以下のURLをブラウザアクセスし、認証画面の表示
http://localhost:3000/api/login

※イメージ
![スクリーンショット 2022-05-09 18.30.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211927/9262f23d-2d8d-42bf-ab29-8ff5ea714e25.png)

認証後に以下のURLに遷移して画面が表示されればオッケー！
http://localhost:3000/callback

![スクリーンショット 2022-05-09 18.30.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211927/8b694028-5ad7-74bf-0559-15125f3ff60b.png)

これで、認証とBFF側の認証保持ができている状態になります

確認として、以下のURLにアクセスしてみて下さい
以下のように表示されれば認証の保持はできています
http://localhost:3000/api/callback
```
{"token":".....","islogin":true}
```

📍 ログアウトする場合は以下のURL
http://localhost:3000/api/logout

## フロントエンドの設定
フロントエンドで認証していなければ表示できない画面を作っていきます

### store の作成
フロントエンドの状態を保持するためのstoreを作成します
https://vuex.vuejs.org/ja/

プロジェクト内にフォルダと以下のファイルを作成

``` store / index.js
export const state = () => ({
  auth: false,
});

export const mutations = {
  login(state) {
    state.auth = true;
  },
  logout(state) {
    state.auth = false;
  },
};

```

### middleware の作成
表示したいページが認証をしていないと表示できないページがどれかを設定していきます
指定したページ('/','/callback')以外は全て認証が必要という設定にします

「nuxt.config.js」に以下を追加
``` nuxt.config.js
router: {
    middleware: 'session'
  },
...
```


プロジェクト内にフォルダと以下のファイルを作成

``` middleware / session.js
export default ({ store, route, redirect }) => {
  if (!store.state.auth && !['/', '/callback'].includes(route.path)) {
    return redirect('/')
  }
}
```

### page の作成
認証情報を取得するページ、認証後に表示するページを作っていきます

下記二つのファイルを作成

``` pages / callback.vue
<template>
  <p>Please Wating・・・・</p>
</template>
<script>
export default {
  async mounted() {

    await this.$axios.get("/api/callback")
    .then( responce => {
      console.log("responce",responce);
      this.$store.commit("login");
      this.$router.push("/home")
    })
    .catch(error => this.$router.push("/"));
  },
};
</script>
```

``` pages / home.vue
<template>
  <div>
    <p>認証画面</p>
    <button @click="logout">Logout</button>
  </div>
</template>
<script>
export default {
  methods: {
    logout(){
      console.log('logout')
      this.$store.commit("logout");
      window.location.href = "api/logout";
    }
  }
}
</script>
```

以下のファイルを編集
``` page/index.vue
<template>
  - <Tutorial/> //削除
  <div>
    <p>ログイン画面</p>
    <button @click="login">Login</button>
  </div>
</template>
<script>
export default {
  name: 'IndexPage',
  methods: {
    login(){
      window.location.href = "api/login";
    }
  }
}
</script>

```

作成後、Nuxtを起動して以下のURLをブラウザアクセスし、「login」ボタンからログイン後、「/home」のパスに遷移すればオッケーです
http://localhost:3000

また、「/home」の画面にある、「logout」ボタンをクリックすると「/」パスに遷移すればオッケーです

ログインしていない状態で、以下のURLにアクセスしても「/」に移動することも確認してください
http://localhost:3000/home

### 認証の永続化
この状態だと、認証後に「/home」パスでブラウザをリロードすると「/」に遷移してしまいます
これは、フロントで保持している store が session で保持しているのでリロードをすると値が初期化してしまいます
そのため、毎回「/」パスに遷移してしまっているという事です
なので、リロードしても store の値を保持するようにしていきます

#### vuex-persistedstate の導入
store の保存先を session ではなく、 localstrage や cokkie に保存するためのパッケージになります
ブラウザをリロードしてもデータを保持するパッケージをインストールします
https://www.npmjs.com/package/vuex-persistedstate

```
yarn add vuex-persistedstate
```

#### js-cookie の導入
今回は、 cookie に保存するため以下のパッケージもインストールします
https://www.npmjs.com/package/js-cookie

```
yarn add js-cookie
```

プロジェクト内にフォルダと以下のファイルを作成
```plugins/persistedstate.js
import createPersistedState from 'vuex-persistedstate';
import * as Cookies from 'js-cookie';
import cookie from 'cookie';

export default ({ store, req }) => {
    createPersistedState({
        key: 'oaut_data',
        storage: {
            getItem: (key) => {
                // See https://nuxtjs.org/guide/plugins/#using-process-flags
                if (process.server) {
                    const parsedCookies = cookie.parse(req.headers.cookie);
                    return parsedCookies[key];
                } else {
                    return Cookies.get(key);
                }
            },
            // Please see https://github.com/js-cookie/js-cookie#json, on how to handle JSON.
            setItem: (key, value) =>
                Cookies.set(key, value, { expires: 365, secure: false }),
            removeItem: key => Cookies.remove(key)
        }
    })(store);
};
```

「nuxt.config.js」に以下を追加
``` nuxt.config.js
plugins: [{ src: "~plugins/persistedstate.js", ssr: true },]
...
```

これで、ブラウザをリロードしてもフロントも認証情報は保持したままになります

これでバックエンドとフロントエンドの認証の設定は完了になります

## さいごに
OAuthのつまづいた部分もそうなんですけど、同時にNuxt全体の仕様などの振り返りにもなったと思っています

以上