【Auth0】Organization機能を利用してマルチテナントの認証・認可を実現する

弊社プロダクトではユーザーの認証・認可にAuth0を利用しています。

Auth0は多機能なIDaaSで様々なユースケースに対応できる柔軟性を備える一方、

- 自前実装との境界線の設定(ex. 権限管理、ユーザーメタ情報の保存場所)
- 活用すべき機能やAPIの選定

など、ユースケースに適した使いこなしのためにある程度の理解が求められます。（シンプルな認証機構を手軽に実現する、という使い方ももちろんできます）

この記事では、弊社プロダクトにおけるAuth0の利用法、特に最近追加されたOrganization機能を使ってどのようにマルチテナント環境を実現しているかについて書いていきます。

## やりたいこと
弊社プロダクトでは、全てのユーザーアカウントが

- 個人利用するための個人ワークスペースを持つ
- 複数の組織に所属できる（組織は専用ワークスペースを持つ）

といった機能を持っています。QiitaやGithubのようなイメージです。


## 従来の方法・問題点
2021年春頃にOrganization機能が追加されるまで、Auth0のコア機能では上記のような要件を実現するための機構が存在しませんでした。

そこで、完全にバックエンドのDBなどで管理するという方法もありましたが、なるべく認証・認可関連をAuth0側に凝集させたいという思いから、Authorization Extensionという拡張機能を使って対応していました。

https://auth0.com/docs/extensions/authorization-extension

しかし、このExtensionは

- Extension用のAPIや機能が使いにくい
- アクセストークンにRoleを含めるなどの処理一苦労する(複雑なRulesを書く必要がある)

などの問題があり、代替手段を探していました。


## Organizationを使う
そんな中、2021年春頃に追加されたOrganizationは待望の機能であり、これを利用した機構に移行しました。

- ダッシュボード上でマルチテナント管理ができる
- テナントに応じた認証、roleやpermissionの付与が可能

Organizationに関する基本的な説明・デモは公式の動画で見ることができます。

https://www.youtube.com/watch?v=BcUxeThMq74

## 実現方法
弊プロダクトはLaravel + VueのSPA構成です。

ログイン画面を表示し、アクセストークンを取得する認証機構は、フロントのVue側で構築します。

https://auth0.com/docs/quickstart/spa/vuejs/01-login

LaravelのバックエンドAPIにアクセスする際には、取得したアクセストークンを`Authorization: Bearer {token}`ヘッダーに付与してリクエストを投げます。Laravel側ではリクエスト毎にトークンをチェックする認可機構を備えています。

https://auth0.com/docs/quickstart/backend/laravel/01-authorization

ここまでが、このようなSPA構成でAuth0を利用する際に基本的なパターンです。上記ドキュメント通りに実装すれば実現できるので、具体的な説明は省略します。

ここからは、上記実装にOrganization機能を組み合わせてマルチテナント機構を実現するために弊チームが取った方針をご紹介します。

### Organization設定(ダッシュボード)
#### 新規作成

organizationの追加・削除やメンバーのアサインについては、[Management API](https://auth0.com/docs/api/management/v2#!/Organizations/get_organizations)にも専用APIが用意されていますが、まずはダッシュボードから設定してみます。

![スクリーンショット 2021-08-25 14.51.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1909699/5c64feb1-9417-588c-7bab-33ab27e2cd26.png)

ダッシュボード左のOrganizationタブから、Create Organization

![スクリーンショット 2021-08-25 14.52.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1909699/a9cbc64a-50f4-b670-3ea6-9d8f42cc7af7.png)

![スクリーンショット 2021-08-25 14.53.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1909699/c01f11aa-5595-0ffd-599b-531272d034d3.png)

Organizationタブに戻ると、追加されています。
この際、自動的に生成される`Organization ID`がログイン時に必要になってきます。

#### メンバーの追加
作成したOrganizationのメンバーとしてログインできるのは、Organizationのmembersに追加されたユーザーアカウントに限られます。
試しにダッシュボード上からユーザーをOrganization memberとしてアサインします。

![スクリーンショット 2021-08-25 17.49.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1909699/f950e8b7-e723-cf07-fecc-f727459d720f.png)

こちらもManagement APIから行うことも可能なため、組織管理者や開発者がユーザーをOrganizationにアサインする機能をAPIを利用して実現する、といったことも可能です。

また、メンバーには**そのOrganizationにおけるRole**をアサインすることができます。

![スクリーンショット 2021-08-25 17.49.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1909699/1e7267f9-e01d-5a34-a206-9de8ff658ef2.png)

Organizationのmembersタブからユーザー詳細をクリック、さらにAssign Rolesからロールのアサインが可能です。
(ダッシュボード左のUsersタブから、各ユーザーにロールをアサインすることもできますが、その場合ロールはログイン中のOrganizationに関係なく常に付与されます)

#### アプリケーション設定で有効化
Vue側の設定で追加したSingle Page ApplicationのOrganizationsタブでOrganizationログインに関する設定ができます。

![スクリーンショット 2021-08-25 15.43.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1909699/2e6b2a9f-c867-2e2d-5e81-68fe3907b8e7.png)

- Individual for personal use(個人アカウントとしてのみ利用可)
- Team members of organizations(組織メンバーアカウントとしてのみ利用可)
- Both(両方利用可)

の3種類から選ぶことができます。
`個人アカウント`としてログインした場合は取得するアクセストークンのpayloadに`org_id`が含まれず、
`組織メンバーアカウント`としてログインした場合は取得するアクセストークンのpayloadにログイン中の`org_id`が含まれます。
バックエンド側で認可する際には、このpayloadの値をチェックすることになります。
今回は一つのアカウントがどちらの役割も果たせるようにしたいので、`Both`を選択します。

### コードの修正
ログイン時の流れとしては以下のようになります。

1. 最初は個人アカウントとしてログイン
1. 初回マウント時、バックエンドAPIから所属organization一覧情報を受けとりVuexなどに保存しておく
1. テナント(Organizartion)切り替え時は上で保存した情報から対象のorg_idを選び、localStorageに保存し、リロードする
1. vueコードではマウント時のauth0モジュール初期化時にlocalStorageをチェックし、org_idがセットされていればその値をoptionに追加した上で初期化するようにしておく。これで、その対象テナントにログインできる。

このやり方がベストプラクティスかどうか定かではありませんが、公式ドキュメントでもlocalStorageを使った実装法が散見されます。
https://auth0.com/blog/introducing-auth0-organizations/
(こちらはReactのコードベース)

さて、この機構を実現するため、

- 所属organization一覧情報を取得するAPIの追加
- localStorageを使ってログイン時のorg_idを切り替えるためのコード修正
が必要になります。

#### ログイン中ユーザーの所属organization一覧を取得するAPIを追加
Management APIを使って実現しています。

https://auth0.com/docs/api/management/v2#!/Users/get_user_organizations

処理としてはアクセスしてきたユーザーのauth0 idを利用して上記APIを叩き、レスポンスを整形して返すだけです。

```php:App\Controllers\GetCurrentUserInfoController.php
<?php

namespace App\Http\Controllers;

use Log;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Auth0\SDK\API\Management; // https://github.com/auth0/auth0-PHP

class GetCurrentUserInfoController extends Controller
{
    private $mgmt_api;

    /**
     * @param Management $mgmt_api
     */
    public function __construct(Management $mgmt_api)
    {
        $this->mgmt_api = $mgmt_api;
    }

    public function __invoke(Request $request)
    {
        $auth0_user_id = $request->auth0_user_id; // 認可チェックのミドルウェアなどでセットしておく
        $user_info = $this->mgmt_api->users()->get($auth0_user_id);
        $organizations = $this->mgmt_api->users()->getOrganizations($auth0_user_id);

        return [
            'nickname' => $user_info['nickname'],
            'full_name' => $user_info['full_name'],
            'organizations' => $organizations
        ];
    }
}
```

例えばこんな感じ。
実際には、ここにDBに保存したユーザーのメタ情報なども含めてレスポンスしています。

#### localStorageを使ったVue側のコード修正
簡略化した修正コードは以下（ベースは最初の公式ドキュメントのコードです。)

```javascript:main.js
import Vue from "vue";
import App from "./App.vue";
import router from './router'

// Import the plugin here
import { Auth0Plugin } from "./auth";

// Import the Auth0 configuration
import { domain, clientId, audience } from "../auth_config.json";

const options = {
  domain,
  clientId,
  audience,
  onRedirectCallback: (appState) => {
    router.push(appState && appState.targetUrl ? appState.targetUrl : window.location.pathname);
  },
};
if (localStorage.getItem('currentOrganizationId')) {
  options.organization = 
  localStorage.getItem('currentOrganizationId')
}

// Install the authentication plugin here
Vue.use(Auth0Plugin, options);

Vue.config.productionTip = false;

new Vue({
  router,
  render: h => h(App)
}).$mount("#app");
```

```vue:App.vue
<template>
  <div id="app">
    <router-view />
  </div>
</template>

<script>
export default {
  async mounted() {
    // ログイン済みなら所属organization一覧を保存しておく
    if (this.$auth.isAuthenticated) {
      const token = await this.$auth.getTokenSilently();

      const res = await fetch('http:localhost/8080/current_user_info', // 前節で作ったAPI
      {
        headers: { Authorization: 'Bearer ' + token },
      });
      const data = await res.json();
      this.$store.dispatch('setOrganizations', data.organizations); // ここでstoreに保存
    }
  },
};
</script>
```

```vue:pages/SwitchOrganization.vue
<template>
  <div>
    <button v-for="organization in organizations" :key="organization.id">
      {{ organization.display_name }}
    </button>
  </div>
</template>

<script>
export default {
  name: 'SwitchOrganization',
  computed: {
    organizations() {
      return this.$store.getters.organizations;
    },
  },
  methods: {
    switchOrg(orgId) {
      localStorage.setItem('currentOrganizationId', orgId);
      this.reload();
    },
    reload() {
      this.$router.go({ path: this.$router.currentRoute.path, force: true });
    },
  },
};
</script>
```

簡略化していますが、これで最低限のマルチテナント向け認証・認可機構が実現できたと思います。

実際のプロダクトにおいては、

- Roleを使ってOrganization内権限の管理
- バックエンド側DBにusersテーブル、organizationsテーブル等を用意してメタ情報や利用プランの管理
- Management APIを利用した
  - Organization管理者が新規ユーザーを招待、アサインする機能
  - ユーザー、組織を管理するための管理用ツール
 
なども実装しています。

## 残る問題点
- Organization内でネストした権限管理などを行うには、Auth0のCore機能だけでは難しそう
  - 結局自前の実装が必要になってくる?
- 組織ごとの制限(ex.IPアドレス制限)の実現方法
  - バックエンドAPI側か? Rulesなどを利用して認証時に行えるかどうか?

## まとめ
本記事では、Auth0を利用したアプリケーションにおいて、今春頃に追加されたOrganization機能を利用しつつマルチテナント認証・認可を実現するために模索した方法を簡単に紹介しました。

- Auth0の多機能さ
- Organizationが新機能であり情報が出揃っていない
- (おそらく)Auth0はまだまだ発展途上

なためまだまだ我々も活用を模索中ですが、今後より期待ができる点でもあります。

同じような活用をされている、見込んでいる方を含め、意見やダメ出し等お待ちしております。