Nuxt.jsでKeycloakと連携(OAuth2.0)したログイン機能を実装してみる

# 背景

nuxtでOAuth認証によるログインを試してみる。

今回は、まずOAuth2.0のサーバーとしてKeycloakを自前で用意し、Nuxtを利用して書いたnode.jsのプログラムでそこと連携したログイン機能の実装にチャレンジしてみようと思う。

# Keycloakのセットアップ

まずはKeycloakのセットアップから。

## Keycloakのインストール

前提条件として、Docker環境を用意しておく必要がある。

今回、Docker for Windowsをインストールしようかと思ったが、Windwos10 HomeだとHyper-Vが入らないようなので、仕方なくVirtualBoxでたてたCentOS上にDockerを入れて対応した。

Keycloakのインストールは下記コマンドを実行。

```
docker run -d -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin --name keycloak jboss/keycloak
```

Keycloakのイメージの取得から、起動までこのコマンドだけでOK。

http://localhost:8080
にアクセスして確認してみる。

※今回はVirtualBoxでやっているので、ポートフォワーディング設定で8080を8080に流す設定にして、ホスト側からアクセスできるようにして対応しました。

※2回目以降の起動は「> docker start keycloak 」だけでイケるっぽい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/33179417-aa8f-7c95-3d3c-3ae4d3224f79.png)

Keycloakの画面が表示されました。

## Keycloakの設定

「Administration Console」 から管理画面にログイン。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/3de92578-d97e-853d-6ed9-076aef125c73.png)

起動時のコマンドで指定した「admin」ユーザーでログイン。

最初にレルムの追加。
左上の「Master」と表示されている部分にマウスオーバーすると、「Add realm」のボタンが表示された。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/ed6f8085-bc53-6ac2-63db-4d000ba3e325.png)


今回は「Name」に「test」と入力して作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/6b8100a2-15de-60b2-d9de-c5911df6385f.png)


「Clients」の「account」を開いて、編集。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/4cf1e716-28f7-f217-621c-0e5f655c75ea.png)

今回はテストなので、「Client ID」はデフォルトのまま「account」。

「Client Protocol」は「openid-connect」、「Access Type」は「public」。

※2020/08/03 コメントで指摘していただき、「Access Type」を「confidential」から「public」に変更しました。
 「confidential」を指定するのは、サーバーサイドのクライアント向けで、今回のようなクライアントサイドのクライアント向けでは「confidential」を指定しても意味がない(クライアントシークレットを使わない)ようです。

テスト用ということで、有効にするフローも適当に全部Enabledにしてみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/6eda3383-474e-886b-f06c-caefdbb5777a.png)

「Valid Redirect URIs」にリダイレクト先として許容するURLを指定。

今回はnuxtを使うつもりなので、「 http://localhost:3000/* 」としてみた。

※オープンリダイレクトの脆弱性対策として、ここの設定はとても重要とのこと。実運用時は正しい(信頼できる)クライアントのリダイレクトURIのみを指定すること。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/ecc137b6-130f-b3bd-dabf-40d6de161316.png)

下の方に「save」ボタンがあるので忘れずに押しておく。

確認用のユーザーも作っておく。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/c15af3df-9095-bbca-444f-8d96524fcab9.png)

Usernameを入れて一旦「save」すると、Credentials情報を入力できるようになる。

パスワードを設定して、Temporaryをoffにしておく。
もちろんパスワードは控えておくこと。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/d6a3558c-c8a1-504b-cedf-0fbdf4fa5a87.png)

「Set Password」ボタンを押して保存。

keycloak側の設定はとりあえずこの辺にして利用側を実装してみる。

# nuxtでOAuth2実装

nuxt.jsでoauth-moduleを利用してログイン機能を実装してみる。
ログイン画面は自前ではなく、Keycloakのものを利用するようにしたい。

## nuxt環境の用意。

まずはCLIでnuxt環境の構築。

```console
> yarn create nuxt-app oauth
```

設定は適当。こんな感じにしてみた↓↓。

```console
? Project name: oauth
? Programming language: TypeScript 
? Package manager: Yarn
? UI framework: None
? Nuxt.js modules: Axios
? Linting tools: ESLint, Prettier    
? Testing framework: None
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Server (Node.js hosting)
? Development tools: jsconfig.json
```

とりあえず起動して確認。

```console
> cd oauth
> yarn dev
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/e7ddbdd5-0876-b533-576e-2180ea1e4982.png)

問題なく環境ができたっぽい。
やりたいこととしては、この画面にアクセスしようとした場合に、未認証であれば先にログイン画面を表示させて、認証後に画面を表示させる感じ。

## 認証機能の実装

auth-moduleをインストール。

```
> yarn add @nuxtjs/auth
```

nuxt.config.jsに下記設定を追加。

```nuxt.config.js
export default {
  ...
  modules: [
    // Doc: https://axios.nuxtjs.org/usage
    '@nuxtjs/axios',
    '@nuxtjs/auth',
  ],
  /*
   ** Axios module configuration
   ** See https://axios.nuxtjs.org/options
   */
  axios: {},
  auth: {
    redirect: {
      login: '/login',
      logout: '/logout',
      callback: '/callback',
      home: '/',
    },
    strategies: {
      keycloak: {
        _scheme: 'oauth2',
        authorization_endpoint:
          'http://localhost:8080/auth/realms/test/protocol/openid-connect/auth',
        userinfo_endpoint: false,
        access_type: 'offline',
        access_token_endpoint:
          'http://localhost:8080/auth/realms/test/protocol/openid-connect/token',
        response_type: 'token',
        token_type: 'Bearer',
        token_key: 'access_token',
        client_id: 'account',
      },
    },
  },
  router: {
    middleware: ['auth'],
  },
  ...
```

authとrouterの設定を追加する。あと、modulesに「@nuxtjs/auth」を追加するのを忘れずに。

keycloakの場合、「authorization_endpoint」は下記のURLになる。
**http://<サーバー>:<ポート>/auth/realms/<レルム>/protocol/openid-connect/auth**
また、「access_token_endpoint」は下記のURLになる。
**http://<サーバー>:<ポート>/auth/realms/<レルム>/protocol/openid-connect/token**

「client_id」は、keycloak側で設定した値を指定する。

「response_type」は「token」にしないと正しく動かなかった。

routerに「middleware」として「auth」を指定しておくことで、「/」などのページにアクセスした際に、auth-moduleのmiddlewareが実行されて、未ログインだったらログインのパスに飛ばしてくれるような処理を自動でやってくれる(自分でmiddleware以下にauth.jsを作る必要はない模様)。

以降で、「auth」の「redirect」に指定した「/login」と「/logout」と「/callback」のページを作成する。
なお、「home」で指定しているのは「/」なので、既存のindex.vueそのまま。これは書き換えない。

まずは、login.vueを作成してpages/の下に配置。

```login.vue
<template>
  <div></div>
</template>

<script>
export default {
  mounted() {
    this.$auth.loginWith('keycloak')
  },
}
</script>
```

html的な要素は空にして、マウント時にログイン処理が呼ばれるようにしただけ。

次にlogout.vue。

```logout.vue
<template>
  <div></div>
</template>

<script>
export default {
  mounted() {
    this.$auth.logout()
  },
}
</script>
```

ログインのやつとほぼ同じ。マウント時にログアウト処理が呼ばれるようにしている。

ただ、この処理ではローカルで保持している認証情報がクリアされるだけの模様(少なくともkeycloakでは)。
keycloak側で認証済みの情報が残ってしまうので、そちらもクリアしてあげる必要がある。
その辺は後で対応するとして放置。


最後にcallback.vue。

```callback.vue
<template>
  <div></div>
</template>
```

これは完全に空。
「nuxt.config.js」で「callback」に指定するページを「/」にしてしまえば、このページはいらないんじゃないかとも思ったが、「/」だとダメだった。空でも必要らしい。

実装はこれで完了。

ただ、auth-moduleを動作させるためには、「store/」の下に「index.js」ファイルを置いておく必要があるらしいので、それを作成。

```index.js
```

これは正真正銘の空。


## 動作確認

起動

```console
> yarn dev
```

http://localhost:3000/
にアクセスしてみる。

一瞬、
http://localhost:3000/login/
にリダイレクトされたあと、すぐに下記ログイン画面(keycloakのログイン画面)が表示された。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/8c646ec2-14e4-69e8-f90c-5f061890e78f.png)

事前に登録しておいたユーザー(user1)でログイン。

一瞬、
http://localhost:3000/callback/
にリダイレクトされたあと、すぐに下記ホーム画面が表示された。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/21f73160-7c4d-47e9-6320-f77908161fa8.png)



http://localhost:3000/logout
でログアウトしてみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/143ae056-4b43-3653-1fe9-0af6960fd2fb.png)

真っ白なログアウト画面になる。この時点でローカルに保持されている認証情報はクリアされているはず。

そのまま下記にアクセスする。
http://localhost:3000/

一瞬リダイレクトされるが、下記の画面が表示されてしまう。
これはkeycloak側のセッションがクリアされていないから。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/432dede9-5b88-8bdb-1ed0-229dc528476e.png)


再度、
http://localhost:3000/logout
でログアウトした後に下記のようにkeycloak側の管理画面でセッションを破棄する。
（画像には映ってないが、左側のメニューの下のほうにある「Sessions」を選択すると、セッション一覧の画面が表示される。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/6e4fff57-4f69-b0d3-5ec7-543bd01a593c.png)

その後下記にアクセスする。
http://localhost:3000/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/31db7739-816d-d96e-70f3-cb5f34607d47.png)

今度は正しくログイン画面に遷移した。


## keycloak側のログアウト処理

実運用において、keycloak側の管理画面でセッションを破棄するのは現実的ではないため、ログアウト処理でkeycloak側もクリアするようにしてみる。

この辺り(↓)を見る限り、auth-moduleのv5だと、logoutのendpointが指定できるっぽいけど、現状(v4)だとできないっぽい。
https://github.com/nuxt-community/auth-module/issues/559

現状、auth-moduleはv4しか公開されていないようだが、@nuxtjs/authの代わりに@nuxtjs/auth-nextを使うことで、開発中のv5を利用することができそうな雰囲気。

ということで、@nuxtjs/auth-nextをインストールして試してみる。

```console
> yarn add @nuxtjs/auth-next
```

nuxt.config.jsを下記のように変更。

```nuxt.config.js
export default {
  ...
  modules: [
    // Doc: https://axios.nuxtjs.org/usage
    '@nuxtjs/axios',
    '@nuxtjs/auth-next',
  ],
  /*
   ** Axios module configuration
   ** See https://axios.nuxtjs.org/options
   */
  axios: {},
  auth: {
    redirect: {
      login: '/login',
      logout: '/logout',
      callback: '/callback',
      home: '/',
    },
    strategies: {
      keycloak: {
        scheme: 'oauth2',
        endpoints: {
          authorization:
            'http://localhost:8080/auth/realms/test/protocol/openid-connect/auth',
          token:
            'http://localhost:8080/auth/realms/test/protocol/openid-connect/token',
          logout:
            'http://localhost:8080/auth/realms/test/protocol/openid-connect/logout',
        },
        responseType: 'id_token token',
        clientId: 'account',
        scope: ['openid'],
      },
    },
  },
  router: {
    middleware: ['auth'],
  },
  ...
```

modulesの「@nuxtjs/auth」を「@nuxtjs/auth-next」に変更。

authのstrategiesをv5の形式に変更。
v5の形式だと、logoutのエンドポイントも指定できる。

## 動作確認(part 2)

起動

```console
> yarn dev
```

http://localhost:3000/
にアクセスしてみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/a98b303e-a270-0202-94ec-4a6d6c0f63f7.png)

さきほどと同様にログイン画面が表示されたので、また、ユーザー(user1)でログイン。

こちらも先ほど同様、ホーム画面が表示された。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/ad103b56-d427-e930-4281-4ac52f68beb9.png)

http://localhost:3000/logout
でログアウトしてみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/256f6ba0-5682-35ca-397d-d5b3b1e610ed.png)

画面は先ほどと同じだけど、アドレスバーには、keycloakのログアウト用エンドポイントが入っている。

そのまま下記にアクセスする。
http://localhost:3000/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/724e3063-41a0-1f18-5ed3-da669a68c29b.png)

ちゃんとログイン画面が表示された。
keycloak側も認証情報がクリアされているっぽい。


## 動作確認(part 3)

トークンの保存場所についても確認しておく。

トークン(JWT)は、ユーザー情報を含んだ秘密情報で、Base64urlエンコードされているだけなので、簡単に解析できてしまうみたい。また、トークンをコピーされると、他の人が対象Webサイトにリクエストできてしまうので、他から盗られたり、コピーされたりすると問題ありな情報と思われる。

ブラウザで確認したら、Local StorageとCookieにauth-module関連の情報を持っていた。
Local Storage側の情報はどうでもよい情報のみで、Cookieにはトークンが保持されていた。

とりあえず、アンチパターンらしいLocalStorageへの秘密情報の保存はやっていないっぽい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/83fddbce-3608-5ec0-6731-ae0aacf8bb50.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/20d97615-cb19-7964-5aba-b8e322684a5a.png)

# さいごに

今回はnuxtのauth-moduleを使って、oauth2のログイン機能を実装してみた。

それっぽいログイン機能は実現できたけど、auth-moduleの開発中のバージョンを使わないと、ログアウトが中途半端な状態になってしまうことが分かった。
早くv5がリリースされるといいけど。。。
(開発中だからか、稀に@nuxtjs/auth-nextの中で発生したエラーのログが出ていたりした。)