Auth0を触ってみた

本記事はうるる [Advent Calendar 2019](https://adventar.org/calendars/4548) 20日目の記事です。

# はじめに

Webサービスを提供する際には認証・認可の機能は殆どの場合で必須になります。
しかし、自前でこの仕組みを実装しようとなるとけっこう大変です。加えて、セキュアに実装をしなければならなかったり、ソーシャルログインに対応したりなど、、、やることは山積みです。

そこで、今回は[Auth0](https://auth0.com/jp/)というサービスを使って、ソーシャルログインの実装とAPIに対する認証機能を実装したいと思います。

# 開発環境を構築する

## 前提

- API : [NestJS](https://nestjs.com/)
    - ライブラリ
        - `passport`
        - `@nestjs/passport`
        - `passport-jwt`
        - `jwks-rsa`
- クライアント : [Nuxt.js](https://ja.nuxtjs.org/)
    - ライブラリ
        - `@nuxtjs/auth`
- 開発環境 : Docker Compose

今回使用する開発環境はこんな感じです。
フレームワークなどは完全に筆者の好みです。
また、フレームワークのインストールとライブラリのインストールは済ませている状態です。

```yaml:docker-compose.yml
version: '3'
services:
  api:
    build:
      context: ./api
    volumes:
      - ./api:/api
    ports:
      - 3210:3000
  app:
    build:
      context: ./app
    volumes:
      - ./app:/app
    ports:
      - 8888:3000

```

```
# ディレクトリ構成
    .
    ├── api
    │   ├── Dockerfile
    │   ├── README.md
    │   ├── dist
    │   ├── nest-cli.json
    │   ├── node_modules
    │   ├── package-lock.json
    │   ├── package.json
    │   ├── src
    │   ├── test
    │   ├── tsconfig.build.json
    │   ├── tsconfig.json
    │   └── tslint.json
    ├── app
    │   ├── Dockerfile
    │   ├── README.md
    │   ├── assets
    │   ├── components
    │   ├── jest.config.js
    │   ├── jsconfig.json
    │   ├── layouts
    │   ├── middleware
    │   ├── node_modules
    │   ├── nuxt.config.js
    │   ├── package-lock.json
    │   ├── package.json
    │   ├── pages
    │   ├── plugins
    │   ├── static
    │   ├── store
    │   └── test
    └── docker-compose.yml
```

また、NestJSとNuxt.jsそれぞれのDockerfileはこんな感じです。
(Dockerfileを使ってビルドをしましたが、もしかしたらいらなかったかもしれません、、、。)

```dockerfile:api/Dockerfile
FROM node:12-alpine

WORKDIR /api

ADD package.json package-lock.json ./
RUN npm i

ADD . ./

CMD ["npm", "run", "start:dev"]
```

```dockerfile:app/Dockerfile
FROM node:12-alpine

WORKDIR /app

ADD package.json package-lock.json ./
RUN npm i

ADD . ./

CMD ["npm", "run", "dev"]

```

# NestJSでAPIを実装する

## APIの実装

ではまず認証機能を実装するAPIの実装をしていきたいと思います。

NestJSのデフォルト設定では、`src`配下に`*.ts`ファイルを配置して行くことでdistのディレクトリにそれを`*.js`にコンパイルして配置してくれます。

早速、`user`というディレクトリを作成して各種ファイルを配置していきましょう。

```typescript:user/user.controller.ts
import { Controller, Get } from '@nestjs/common';

@Controller('user')
export class UserController {
  constructor() {}

  @Get()
  list() {
    return [
      { id: 1, name: 'Alpha' },
      { id: 2, name: 'Bravo' },
      { id: 3, name: 'Charlie' },
    ];
  }
}
```

```typescript:user/user.module.ts
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';

@Module({
  imports: [],
  controllers: [UserController],
  providers: [],
})
export class UserModule {}
```

```typescript:user/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './use/user.module';

@Module({
  imports: [UserModule], // UserModuleをimport
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

ここでやっていることは簡単で、`http://localhost:3210/api/user`にアクセスするとユーザーのリストが返ってくるAPIを作成しています。

NestJSは少なくとも1つのmoduleを持っている必要があり、moduleをもとにClass間の依存関係を解決します。初期状態では`app.module.ts`のみが配置されており、これをルートモジュールと呼ぶそうです。
基本的にはリソース単位でモジュールを作成することになると思います。

今回は`user.module.ts`を作成して`app.module.ts`のimportsに`UserModule`を記述します。
※ 今回はやりませんでしたが、 cliでserviceやcontrollerを作成すると、自動的にAppModuleの`providers`や`controllers`に挿入してくれるようです。

Postmanなどでアクセスしてみると、下記のような結果を受け取ることができます。
<img width="419" alt="postman result" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129444/00406ba2-3a91-a223-9bd7-8aec0f140304.png">

ここで一旦、APIの実装は終わりです。

# Auth0

# Auth0とは

ユニバーサルログインやSSO、MFAなんかを比較的簡単に実装できる認証・認可のプラットフォームです。

IDaaSとも言うらしいです。類似サービスとして、Firebase Authentication や AWS Cognitoなどが挙げられます。

個人的に嬉しかったのはサンプルコードを公式が用意してくれていることです。
また、SDKやドキュメントも豊富にあって入門しやすい印象でした。

- [https://github.com/auth0-samples](https://github.com/auth0-samples)
- [https://auth0.com/docs/libraries](https://auth0.com/docs/libraries)

## Auth0 Applicationsの作成

Auth0を使ってクライアント側からログインをしたり、jwtトークンの発行などをする場合は、Applicationsの作成が必要になります。
そのためにまず、ダッシュボードでサイドメニューのApplicationsを選択して、`CREATE APPLICATION`というボタンを押します。typeは様々選べるのですが、Nuxt.jsはSPAモードを選択するので、`Single Page Web Application`を選択してください。<img width="795" alt="Auth0 Create Application" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129444/e8465e37-1406-eb90-e6b2-d386de7f3f0e.png">

そして、各種入力欄に下記のように入力して、ページ下部にある`SAVE CHANGE`ボタンを押してください。

```
Allowed Callback URLs : http://localhost:8888/callback
Allowed Web Origins : http://localhost:8888
Allowed Origins (CORS) : http://localhost:3210
```

加えて、Default Audienceの設定を行います。
入力するのは、`Allowed Origins (CORS)`と同じ値です。
こちらは本来であれば設定をしなくてよいのですが、Nuxt.jsの仕様で設定をしないとうまく動きません。

[https://manage.auth0.com/dashboard/us/dev-<自分のID>/tenant/general](https://manage.auth0.com/dashboard/us/dev-l032hs7f/tenant/general) のページで、設定できますので忘れないように設定してください。

これで、Auth0での設定は完了しました。

# APIに認証機能を実装する

Auth0の設定も完了しましたので、先程実装したAPIに認証機能を実装していきたいと思います。
`src`に`auth`ディレクトリを作成して`AuthModule`と`JwtStrategy`というクラスをそれぞれ実装してください。

```typescript:auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtStrategy } from './jwt.strategy';
import { PassportModule } from '@nestjs/passport';

@Module({
  imports: [PassportModule.register({ defaultStrategy: 'jwt' })],
  providers: [JwtStrategy],
  exports: [],
})
export class AuthModule {}

```

```typescript:jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt, VerifiedCallback } from 'passport-jwt';
import { passportJwtSecret } from 'jwks-rsa';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor() {
    super({
      secretOrKeyProvider: passportJwtSecret({
        cache: true,
        jwksRequestsPerMinute: 5,
        jwksUri: `https://dev-<自分のID>.auth0.com/.well-known/jwks.json`,
      }),
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      issuer: 'https://dev-<自分のID>.auth0.com/',
      audience: 'http://localhost:3210', // apiサーバーのドメイン
      algorithms: ['RS256'],
    });
  }

  async validate(payload: any, done: VerifiedCallback) {
    if (!payload) {
      done(new UnauthorizedException(), false);
    }

    return done(null, payload);
  }
}
```

これらを実装したら、`app.module.ts`を下記のように修正します。

```typescript:app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './use/user.module';
import { AuthModule } from './auth/auth.module';

@Module({
  imports: [UserModule, AuthModule], // AuthModuleを追加
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

そして、`user.controller.ts`を下記のように修正します。

```typescript:user.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('users')
@UseGuards(AuthGuard('jwt')) // Guardsを追加
export class UserController {
  constructor() {}

  @Get()
  list() {
    return [
      { id: 1, name: 'Alpha', age: 12 },
      { id: 2, name: 'Bravo', age: 24 },
      { id: 3, name: 'Charlie', age: 36 },
    ];
  }
}
```

`@UseGuards(AuthGuard('jwt'))`
を追加することで、このコントローラー内に作られるエンドポイントのAPIはすべて認証が必須となります。
(これはNestJSの[Guards](https://docs.nestjs.com/guards)という機能で、認証機能を実装する際によく使われるそうです。)


作成したAPIをPostmanなどで叩くと、401が返ってくることが確認できます。
<img width="322" alt="Postma Result Not Auth" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129444/261c5107-7646-8f9c-0c94-f29ecbf608c9.png">

この認証を通すためにはAPIコールの際にJWTトークンを渡してやる必要があります。このJWTトークンはクライアント側でソーシャルログインをしたときに発行され、axiosでAPIコールする際にNuxtのモジュールがヘッダーに自動で差し込んでくれます。
なので、サーバーサイドは受け取ったトークンをどう処理するかだけを実装すればいいのです。

# Nuxt.jsでクライアント側を実装する

## nuxt.config.js 設定

続いて、Nuxt.jsでログイン機能を実装していきたいと思います。

各種設定はnuxt.config.jsに記述していくことになります。今回は`axios`や`auth`などの設定をしています。
`domain`と`client_id`に関しては、Auth0のApplication詳細ページにある`Domain`と`Client ID`を入力してください。

```javascript:nuxt.config.js
...

modules: [
  '@nuxtjs/axios',
	'@nuxtjs/auth' // こいつが重要
],

axios: {
  host: 'localhost',
  port: 3210,
	prefix: '/api'
},

// Auth0 setting
  auth: {
    strategies: {
      auth0: {
        domain: '****************', // Applicationsの詳細ベージで確認
        client_id: '****************' // Applicationsの詳細ベージで確認
      }
    },
    redirect: {
      login: '/', // 未ログイン時のリダイレクト先
      logout: '/', // ログアウト処理を実行した直後のリダイレクト先
      callback: '/callback', // コールバックURL
      home: '/users' // ログイン後に遷移するページ
    }
  },

// 認証済みでないと各種ページにアクセスできないようにする
// index.vueとcallback.vueのページは認証無しでアクセス可
router: {
	middleware: ['auth']
},

...
```
## 各種ページを作成

最後に、各種ページを作成していきたいと思います。
まず、`layout/default.vue`と`pages/index.vue`を下記のように修正します。

```vue:default.vue
<template>
  <v-app>
    <v-app-bar app>
      <v-toolbar-title v-text="title" />
      <v-spacer />
      <v-btn v-if="!this.$auth.$state.loggedIn" color="primary" @click="login">
        Login
      </v-btn>
      <v-btn v-else text @click="logout">Logout</v-btn>
    </v-app-bar>
    <v-content>
      <v-container>
        <nuxt />
      </v-container>
    </v-content>
  </v-app>
</template>

<script>
export default {
  data() {
    return {
      title: 'Auth0 Demo'
    }
  },

  methods: {
    login() {
      // authモジュールを使用してのログイン
      this.$auth.loginWith('auth0')
    },

    logout() {
      this.$auth.logout()
    }
  }
}
</script>
```

```vue:index.vue
<template>
  <v-layout justify-center>
    <v-card width="800px">
      <v-card-title primary-title>
        Let's Login !!!
      </v-card-title>
    </v-card>
  </v-layout>
</template>
```

こうすることで、下記のようなページが出来上がります。
<img width="1677" alt="top page before login" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129444/18ecb740-8189-a94f-9bec-18bd7af06a2b.png">

次に、`pages/callback.vue`を作成します。
こちらのページは、Auth0からコールバックされるページの作成です。
許可コードを発行する際に一瞬だけ表示されるページですので、templateのみにしています。

```vue:callback.vue
<template>
  <div>
    <p>Please Wating</p>
  </div>
</template>
```

最後に、ログイン後に遷移するユーザーの一覧ページです。

```vue:users.vue
<template>
  <v-layout justify-center>
    <v-data-table :headers="headers" :items="users" item-key="id">
    </v-data-table>
  </v-layout>
</template>

<script>
export default {
  data() {
    return {
      headers: [
        { text: 'id', value: 'id' },
        { text: 'name', value: 'name' },
        { text: 'age', value: 'age' }
      ],
      users: []
    }
  },

  created() {
    // APIコールの際に自動でJWTトークンをヘッダーのAuthorizationにつけて送信してくれます。
    this.users = await this.$axios.$get('users')
  }
}
</script>
```

ここまで実装が完了すると、Nuxt.jsでの実装も一通り完了です！

とりあえず、うまく動くか確認してみましょう。
まずは、`http://localhost:8888/`にアクセスしてみましょう。
そして、右上のログインボタンを押してみるとこんな見た目のページに遷移すると思います。

ここで、「Googleで続ける」を選択してみてください。
そうすると、現在のブラウザでログインしているGoogleのアカウント一覧が表示されたと思います。
その中から、ログインしたいアカウントを選択して、ログインが成功すると自分が作ったウェブサイトの`http://localhost:8888/users`に遷移すると思います。
<img width="569" alt="Auth0 login form" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129444/a1a2ae04-ac98-d77a-0fec-e72ce4ab0fa5.png">
ログイン後に、遷移するページは、`http://localhost:8888/users`となります。
このページでは、しっかりとAPIの認証も完了してデータを取ってくるとができています。
<img width="1679" alt="users list" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129444/25aa226f-fdf8-7a0a-716d-db7357ee922a.png">
ちなみに、こんな感じでBearerトークンとして挿入してくれています。
ちゃんと認証も通っていることが確認できます。
<img width="1355" alt="bearer token" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129444/f002a309-32ad-888c-1fd2-4b602f63c6b3.png">

# ひとこと

Auth0を使うことで、こんなに簡単に認証が実装できてしまうのは驚きました。
また、煩わしいソーシャルログインの仕組みもAuth0がすべて肩代わりしてくれるので、開発体験が最高でした。
今回はGoogleのソーシャルログイン機能のみを試しましたが、もっと多くの機能がAuth0にはあるので、気になった方は是非試してみてください！！