Auth0のCustom SignUpをLaravelで実装してみた

## はじめに

最近Auth0を使い始めまして、久しぶりに素晴らしいサービスに出会えたと感謝している毎日です。
その中で、Auth0を使って少々詰まったユースケースがあったのでご紹介しようと思います。

実装例にはLaravelを用いていますが、ポイントはどの言語でも変わらないと思われます(たぶん)。

## 概要

[Auth0](https://auth0.com/jp/)は認証を中心としたエコシステムを提供しているマネージドサービスになります。
ユーザーの登録、ログイン、ログアウト、メール検証、二段階認証など様々なユースケースに対応しています。

僕がAuth0を使って実現したかったのは、SignUp時の追加情報入力フローです。
というのも、Auth0のデフォルトのSignUpでは、Emailとパスワードか、Social Providerに登録済みの情報しか取得することができません。
アプリケーションによっては足りない情報の入力を促したり、SignUp時に必須で取得したい情報が存在すると思います。
そのようなSignUp時の追加情報入力フローをAuth0 Rulesを使って実装してみました。

## Custom SignUpのフロー

Auth0でCustom SignUpを実現するにはいくつか方法が存在します。
[公式ドキュメントに記載されてはいる](https://auth0.com/docs/libraries/custom-signup)方法だと以下の3つです。

1. Auth0のSignUpページをカスタマイズする
2. 自分でログインページを作成しホスティングする
2. 別ページにRedirectさせる
3. Progressive profilingをする

各方法について見ていきます。

### 1. Auth0のSignUpページをカスタマイズする

1.のやり方はAuth0がホスティングしているセントラル認証サーバー上で入力する項目をカスタマイズすることができます。
Auth0の提供しているLockというライブラリを使えば簡単に項目を追加することができます。
しかし、複数アプリケーションから利用されるようなユースケースの場合だと、アプリケーション固有の項目を入れるとなると、かなり複雑な処理を書く必要が出てきます。
また、Social ProviderでSignUpされた場合には、対応することができないので注意が必要です。

### 2. 自分でログインページを作成しホスティングする

Lockでは、求めるUI/UXを実現できない！となった場合は、自分でログインページを作成することも可能です。その場合、Auth0のAPIを用いて、認証を行うことになります。
自分でUIを作成してしまえば、複雑な認証プロセスをいくらでも実現することが可能となります。

しかし、Auth0が用意している認証エコシステムが一部利用不可能になったり、実装コストがかかってしまうというデメリットも抱えています。
ちなみに、LockとCustom UIについては、[公式ドキュメントで比較されている](https://auth0.com/docs/libraries/when-to-use-lock)ので、ご興味がある方は参照してみてください。

### 3. 別ページにRedirectさせる

今回採用したのはこの方法です。

Auth0のホスティングするログインページでSignUp後、自分がホスティングする追加情報入力ページにリダイレクトさせ、情報を入力してもらうという方法です。
この方法を用いると、Auth0の機能をふんだんに使いつつ、自分のアプリケーション用にカスタマイズされた追加項目の入力をユーザーに求めることが可能です。
また、Social Provider経由のSignUpでもEmail/PWでのSignUpでも、同じフローで追加項目の入力を求めることが可能になります。

これの実現方法についても。[公式ドキュメントにかかれています](https://auth0.com/docs/rules/guides/redirect)。

### 4. Progressive profilingをする

SignUp時には最低限の情報しか求めず、次回ログイン時や、アクセス時に追加で情報を求めるというのが、Progressive profilingです。
名前もかっこいいし、登録率も増えそうな方法ですが、UX設計のハードルが少々高そうだったので、今回は見送りました。

Progressive profilingの例も[Auth0の公式ドキュメントで提供されています](https://auth0.com/docs/rules/guides/redirect#progressive-profiling-example)。
ご興味がある方はご参照ください。

## RedirectでCustom SignUp

Auth0でSignUp中にRedirectする場合には、Auth0 Rulesというサービスを使います。
[Auth0 Rules](https://auth0.com/docs/rules)は認証トークンを発行する前に呼び出されるサーバレスな関数だと思って頂ければよいかと思います。

Rulesを使ったRedirectのSignUpのフローを図にすると以下のような感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120358/0d842847-4b08-994c-17fc-e7c273062a0b.png)

図の`7.`でAuth0 Rulesを使って、Custom SignUpページにRedirectされています。
この時、認証は一度中断されるため、ここでユーザーが離脱した場合には、認証状態はなりません。
(実際には`6.`の時点で、Auth0上にユーザーが作成されるようですが)

認証フローを再開する場合には、`https://{YOUR_AUTH0_DOMAIN}/continue?state={STATE}` にリクエストを送る必要があります。
(`state`パラメーターについては、`7.`でRedirectされる際に、URLのクエリパラメーターにくっついてきます。)

認証フローが再開し、その後の処理が無事終了すると、認証済みになり、トークンが発行され、ログイン状態になります。

### Rulesの作成

実際にRulesを作成してみます。
既にAuth0のアカウントは作成済みという前提で進めます。

1. Auth0のDashboardのサイドメニューにある[Rules](https://manage.auth0.com/#/rules)をクリックします。
2. `CREATE RULE` をクリックし、`Empty rule`を選択します
3. Scriptに以下のコードを貼り付けます

```js
function (user, context, callback) {
  const hasConsented = user.app_metadata && user.app_metadata.has_consented;

  if (!hasConsented && context.protocol !== 'redirect-callback') {
    // urlはアプリケーションのURLに差し替える
    context.redirect = { url: 'http://localhost:3000/signup' };
  }
  
  if (!hasConsented && context.protocol === 'redirect-callback') {
    user.app_metadata = { has_consented:  true };
    user.user_metadata = context.request.body;
    auth0.users.updateAppMetadata(user.user_id, user.app_metadata);
    auth0.users.updateUserMetadata(user.user_id, user.user_metadata);
  }
  
  callback(null, user, context);
}
```

Saveすると準備完了です。
6行目の`context.redirect = {url }`でRedirect先のURLを指定しています。

2つ目のifの中身は `https://{YOUR_AUTH0_DOMAIN}/continue?state={STATE}` にリクエストが送られた後の処理になります。
認証プロセスが再開されると、 `context.protocol`に`redirect-callback`という値が挿入されます。
ここで、Custom Signupが完了しているユーザーなのか判別するために、`app_metadata`に`{ has_consented: true }`という値を入れています。

### Laravel Application側

Redirectの受け先のLaravel Applicationを作成します。
既にLaravelはセットアップ済みという前提で進めます。
(セットアップがまだの方は[こちら](https://auth0.com/docs/quickstart/backend/laravel/01-authorization)を参考にしてください。)

```php
<?php

# routes/web.php

Route::get('/signup', SignupController@main)->name('signup');
Route::post('/signup', SignupController@signup);
```

```php
<?php

# app/Http/Controllers/SignupController.php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class SignupController extends Controller
{
    public function main(Request $request) {
        $state = $request->input('state');

        // formにstateの値をhiddenで入れる
        return view('signup', ['state' => $state]);
    }

    public function signup(Request $request) {
        $state = $request->input('state');
        $config = config('laravel-auth0');
        $url = 'https://'.$config['domain'].'/continue?state='.$state;
        return redirect($url, '307');
    }
}
```

ここでの注意点は、 `/continue`にRedirectさせる時に、 `redirect($url, '307')` とすることです。
第2引数はHttpステータスコードを指定しています。

laravelの`redirect()`はデフォルトでは`302`でリダイレクトされるのですが、`302`だとリダイレクトする際にブラウザ側で勝手にGETリクエストに変換されてリクエストが送信されてしまうので、POSTのBodyが全てなくなってしまいます。
そこで、Httpステータスコードを`307`でリダイレクトさせることで、POSTのままリダイレクト先にリクエストを送信でき、Bodyの情報も無事渡すことが可能になります。

`signup()` でリダイレクトされた後は、再びAuth0のRulesが発火されるといった寸法です。

ここで、無事認証が終了すると指定したコールバックURLにトークン付きでリダイレクトされ、ログイン状態になるといった寸法です。

## まとめ

Laravelを用いてAuth0のCustom Signupを実装してみました。
Custom Signupを利用することで、アプリケーション固有の入力項目にも対応することができ、かつ、Redirectの場合であれば、Social ProviderでSignupしたユーザーにも対応することができるようになりました。
少しの手間で、Custom Signupを実現することができ、Auth0の凄さには驚きを隠せません。

ただ、認証再開時の実装については、あまり文献がなかったので、見事にハマってしまいましたが、良さげな解決法が見つかってよかったです。
しかし、Httpステータスコードの`307`に対応していないブラウザ(特にモバイルが心配)もある可能性もあり、そこについてはまだ調査していないので、実際に利用する際にはご注意ください...。
(RFCで定められているから、大丈夫だと思うんだけど...一応)。