【Auth0】LaravelでAuth0を使って認証する：アクセストークン編【Laravel】

Auth0は公式のドキュメントやチュートリアル、Q&Aがとても充実しているのですが、どれも英語で微妙に検索しにくいので自分でメモ代わりに記事を書くことにしました。

今回はLaravelでのJWT形式のアクセストークンの認証のチュートリアルに関してまとめます。
[Auth0公式チュートリアル](https://auth0.com/docs/quickstart/backend/laravel/01-authorization)

## 関連記事
[【Auth0】LaravelでAuth0を使って認証する：IDトークン編【Laravel】](https://qiita.com/akkino_D-En/items/9c2a8c0d13ac802b04e7)

# 環境
PHP: 7.2.5
laravel: 7.0
Auth0/login: 6.0


## Auth0の設定

まずはAuth0にフリートライアルで登録して、APIを作成しましょう。
そうしたら`APIs`タブを開き、`CREATE API`から新規Auth0 APIを作成します。

<img width="1110" alt="スクリーンショット 2020-06-03 23.48.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391127/ba0dde3b-fa26-f2ec-74b0-a3bbf289a671.png">

API名と識別子は先例に習って`Quickstarts API`と`https://quickstarts/api`にします。またアルゴリズムは特に理由がなければ`RS256`にしましょう。
`RS256`を使用することで`JWKs`を使用したトークン検証を行えるようになります。

<img width="685" alt="スクリーンショット 2020-06-03 23.54.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391127/cf346442-fb1a-73cb-fbcd-1090f8908c5b.png">

ものすごく丁寧なことに今作成したアプリケーションにアクセスするためのクイックスタートを記載したページに飛ばしてくれます。

ですが今は一旦無視して`Permissions`タブを開きましょう。
ここではアクセストークンを使用してAuth0からどの情報まで取得できるかの権限を設定することができます。
とりあえず今は`read:messages`, `read:email`, `read:users`あたりを設定しておきましょう。

<img width="1150" alt="スクリーンショット 2020-06-04 0.10.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391127/ecf2609b-fde8-2eff-0ad4-f67dc84fa7bd.png">


## ライブラリの導入と設定

Auth0側の準備が完了したので、次はLaravelの実装に移ります。
まずはアクセストークンを検証するためのライブラリを導入します。

```
$ composer require auth0/login
```

この時、auth0/auth0-phpという似た名前のライブラリがあるので注意しましょう。

以下のコマンドで設定ファイルを生成します。

```
$ php artisan vendor:publish --provider "Auth0\Login\LoginServiceProvider"
```

これにより`config/laravel-auth0.php`に設定ファイルが生成されるので、以下のように認証に必要な値を設定します。

```php:config/laravel-auth0/php
return [
	// 許可されたトークン発行者のドメイン（テナントURL）
	'authorized_issuers' => [ env('AUTH0_DOMAIN') ],
	// Auth0 APIの識別子
	'api_identifier' => env('AUTH0_API_IDENTIFIER'),
	// 署名アルゴリズム
	'supported_algs' => [ 'RS256' ],
];

```

```:.env
AUTH0_DOMAIN=dev-hogehoge.auth0.com
AUTH0_CLIENT_ID=hogehogeClientId
AUTH0_CLIENT_SECRET=hogehogeClientSecret
AUTH0_API_IDENTIFIER=ttps://quickstarts/api
```

`.env`には`AUTH0_DOMAIN`は`Settings`の`Custom Domains`から、`
AUTH0_API_IDENTIFIER`は`APIs`の`Quickstarts`の`Identifier`の値をそれぞれ入力しましょう。

<img width="1107" alt="スクリーンショット 2020-06-04 0.40.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391127/c3180092-6271-6cc3-1a75-0ddeb1badba4.png">

<img width="956" alt="スクリーンショット 2020-06-04 0.33.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391127/556d98c5-faa7-dd13-d628-f5a3cbb659fd.png">

`AUTH0_CLIENT_ID`と`AUTH0_CLIENT_SECRET`には`Applications`からQuickstarts APIを選択し、そのClient IDとClient Secretを使用します。

<img width="1056" alt="スクリーンショット 2020-06-07 18.08.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391127/94b7e05b-573d-ec62-0a14-d7ef4203b7bd.png">




Apacheを使用している場合、デフォルトではHTTPリクエストからの`Authorization`ヘッダーを解析しないので`.htaccess`に以下の設定を追加する必要がある場合があります。

```
RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
```


## Larabvelでの実装

今回は以下の三つのエンドポイントを作成します。

* GET `/api/public`：認証されていないリクエストで利用可能
* GET `/api/private`：追加のスコープのないアクセストークンが含まれたリクエストでの認証により使用可能
* GET `/api/private-scoped`：read:messagesスコープが付与されたアクセストークンが含まれたリクエストでの認証により使用可能

`private`と`private-scoped`エンドポイントではミドルウェアを使用してリクエストに含まれる`Authorization`ヘッダのBearerトークンが有効であるかどうかをチェックします。

まずはミドルウェアを作成しましょう。

```
php artisan make:middleware CheckJWT
```

```php:app/Http/Middleware/CheckJWT.php
namespace App\Http\Middleware;

use Auth0\Login\Contract\Auth0UserRepository;
use Auth0\SDK\Exception\CoreException;
use Auth0\SDK\Exception\InvalidTokenException;
use Closure;

class CheckJWT
{
    protected $userRepository;

    /**
     * CheckJWT constructor.
     *
     * @param Auth0UserRepository $userRepository
     */
    public function __construct(Auth0UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $auth0 = \App::make('auth0');

        // リクエストからBearerトークンを取得する
        $accessToken = $request->bearerToken();

        try {
            // トークンをデコードし、JWTを検証する
            $tokenInfo = $auth0->decodeJWT($accessToken);
            // トークンからユーザの情報を取得する
            $user = $this->userRepository->getUserByDecodedJWT($tokenInfo);
            if (!$user) {
                return response()->json(["message" => "Unauthorized user"], 401);
            }

        } catch (InvalidTokenException $e) {
            return response()->json(["message" => $e->getMessage()], 401);
        } catch (CoreException $e) {
            return response()->json(["message" => $e->getMessage()], 401);
        }

        return $next($request);
    }
}
```

作成したミドルウェアを`jwt`という名前でカーネル登録します。

```php:Kernel.php
class Kernel extends HttpKernel {
	// ...
	protected $routeMiddleware = [
	    // ...
	    'jwt' => \App\Http\Middleware\CheckJWT::class,
	    // ...
	];
	// ...
}
```

では早速各エンドポイントにミドルウェアを設定しましょう。

```php:routes/api.php
// 認証が必要ないエンドポイント
Route::get('/public', function (Request $request) {
    return response()->json(["message" => "パブリックなエンドポイントへようこそ！このレスポンスを確認するのに認証は必要ありません。"]);
});

// アクセストークンによる認証が必要なエンドポイント
Route::get('/private', function (Request $request) {
    return response()->json(["message" => "プライベートなエンドポイントへようこそ！これを表示するには有効なアクセストークンが必要です。"]);
})->middleware('jwt');
```

これで実装が完了したので、実際にエンドポイントにリクエストを送ってみましょう。

```
$ php artisan serve --port=3010
```

まずはパブリックなエンドポイントにリクエストします。

`http://localhost:3010/api/public`にGETでリクエストを送ります。

```json
{"message": "パブリックなエンドポイントへようこそ！このレスポンスを確認するのに認証は必要ありません。"}
```

では今度はプライベートなエンドポイントにリクエストします。

アクセストークンを取得するにはAuth0の`APIs`から最初に作成した`Quickstarts API`を選択し、その`Test`タブを開きます。

そこに各言語でのアクセストークンの発行方法と、そのレスポンスが書かれているので`Copy Token`からアクセストークンをコピーします。

<img width="1140" alt="スクリーンショット 2020-06-07 18.15.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391127/fd73209b-6c67-ca56-2d7f-7b3071feec99.png">

そしたらトークンを`Authorization`ヘッダーにセットして`http://localhost:3010/api/private`にリクエストしましょう。

```json
{"message": "プライベートなエンドポイントへようこそ！これを表示するには有効なアクセストークンが必要です。"}
```


## スコープの構成

ここまでで作成したミドルウェアはアクセストークンの存在と有効性は確認することができますが、それに含まれるスコープを確認することはできません。
なので次は特定のスコープがアクセストークンに含まれるかをチェックするミドルウェアを作成しましょう。

```
php artisan make:middleware CheckScope
```

```php:app/Http/Middleware/CheckScope.php
namespace App\Http\Middleware;

use Auth0\Login\Contract\Auth0UserRepository;
use Auth0\SDK\Exception\CoreException;
use Auth0\SDK\Exception\InvalidTokenException;
use Closure;

class CheckScope
{
    protected $userRepository;

    /**
     * CheckScope constructor.
     * @param  Auth0UserRepository  $userRepository
     */
    public function __construct(Auth0UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param string $scope
     * @return mixed
     */
    public function handle($request, Closure $next, string $scope)
    {
        $auth0 = \App::make('auth0');

        $accessToken = $request->bearerToken();

        try {
            $tokenInfo = $auth0->decodeJWT($accessToken);
            $user = $this->userRepository->getUserByDecodedJWT($tokenInfo);
            if (!$user) {
                return response()->json(["message" => "Unauthorized user"], 401);
            }

            // 引数に認証を許可するスコープが指定されていた場合
            if($scope) {
                $hasScope = false;
                if(isset($tokenInfo['scope'])) {
                    // アクセストークン内にスコープが含まれることを確認する
                    $scopes = explode(" ", $tokenInfo['scope']);
                    foreach ($scopes as $s) {
                        if ($s === $scope)
                            $hasScope = true;
                    }
                }
                if(!$hasScope) {
                    return response()->json(["message" => "Insufficient scope"], 403);
                }
            }
        } catch (InvalidTokenException $e) {
            return response()->json(["message" => $e->getMessage()], 401);
        } catch (CoreException $e) {
            return response()->json(["message" => $e->getMessage()], 401);
        }

        return $next($request);
    }
}
```

こちらも`CheckJWT`と同様にKernelに登録します。

```php:Kernel.php
class Kernel extends HttpKernel {
	// ...
	protected $routeMiddleware = [
	    // ...
        'check.scope' => \App\Http\Middleware\CheckScope::class,
	    // ...
	];
	// ...
}
```

では作成した`CheckScope`ミドルウェアを`/private-scoped`エンドポイントに適用しましょう。
ミドルウェアを設定するときに`check.scope:read:messages`のようにすることで`read:messages`の部分をミドルウェアへ引数として受け渡すことができます。

参照：[Laravel 6.x ミドルウェア-ミドルウェアパラメータ](https://readouble.com/laravel/6.x/ja/middleware.html)

```php:routes/api.php
// このエンドポイントの認証には"read:messages"スコープを持つアクセストークンが必要です
Route::get('/private-scoped', function (Request $request) {
    return response()->json([
        "message" => "プライベートなエンドポイントへようこそ！これを表示するには有効なアクセストークンとread:messagesのスコープが必要です。"
    ]);
})->middleware('check.scope:read:messages');
```

試しに`read:messages`スコープがないアクセストークンでリクエストをしてみましょう。

`/private`にリクエストした時に使用したアクセストークンで`http://localhost:3010/api/private-scoped`にアクセスしてみます。

すると以下のようにスコープが不足していることを表すエラーメッセージが返ってきます。

```json
{ "message": "Insufficient scope" }
```

### `read:messages`スコープの含まれたアクセストークンの発行方法

Auth0のダッシュボードを開き、`APIs`タブからQuickstarts APIを選択します。

そうしたら`Machine tp Machine Applications`タブを開き、そこで認可されている`Quickstarts API`の`AUTHORIZED`ボタンをクリックし一旦`UNAUTHORIZED`にしてから再度`AUTHORIZED`に変更します。

するとパーミッション（スコープ）選択画面が表示されるので、`read:messages`スコープを選択して`UPDATE`ボタンを押します。

<img width="1130" alt="スクリーンショット 2020-06-07 23.07.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391127/d44939f5-1535-d39e-83a1-b24915214ff7.png">

これでスコープの設定は完了したので、`Test`タブからアクセストークンを取得しリクエストしてみましょう。

```json
{"message": "プライベートなエンドポイントへようこそ！これを表示するには有効なアクセストークンとread:messagesのスコープが必要です。"}
```

### アクセストークンの中身を見てみる

JWTの解説に関しては探せばいくらでも良質な記事が出てくるので、ここでは簡単な確認方法だけ記載します。

[jwt.io](https://jwt.io/)のEncodedの部分にアクセストークンをコピペするだけで中身を確認することができます。

先ほど作成したアクセストークンをデコードしてみると、Payload内に`scope`というkeyと値が追加されています。
`CheckScope`ミドルウェアではこれを参照して判定しています。

## おまけ：Auth0ダッシュボード以外からのアクセストークンの取得フロー

今回はチュートリアルということもあり、Auth0のダッシュボード上から有効期限の短いテスト用のアクセストークンを発行していました。

実際にプロダクトで使用する際には以下のフローを参考に取得してください。

* Webアプリケーションの場合：[Authorization Code Flow](https://auth0.com/docs/flows/concepts/auth-code)
* モバイルやデスクトップ、その他のネイティブアプリケーションの場合：[Authorization Code Flow with PKCE](https://auth0.com/docs/flows/concepts/auth-code-pkce)
* バックエンドで実行されているCLIやデーモン、サービスの場合：[Client Credentials Flow](https://auth0.com/docs/flows/concepts/client-credentials)

（そのうち別の記事でまとめたい）

## おまけその２：CORSを構成する

`larave-cors`を追加し、Kernelの`middleware`に`HandleCors`を追加します。
（Laravel7以前の場合は最初から含まれているので不要です。`config/cors.php`だけ書き換えましょう。）

```php:app/Http/Kernel.php
protected $middleware = [
    // ...
    \Barryvdh\Cors\HandleCors::class,
];
```

`config/cors.php`に設定を追加します。

```php:config/cors.php
return [
    'supportsCredentials' => true,
    'allowedOrigins' => ['http://localhost:3000'],
    'allowedOriginsPatterns' => [],
    'allowedHeaders' => ['*'],
    'allowedMethods' => ['*'],
    'exposedHeaders' => [],
    'maxAge' => 0,
];
```

# 最後に

Auth0は本当に多機能で痒いところまでだいたい手が届くのですが、その分使いこなせるようになるまでの学習が結構大変です。（体感）

チュートリアルなどが充実していますが、それでもいきなり最初からSSOやEnterprise Connections、CustomDB、Ruleなど一度に手を出してしまうとほぼ確実に挫折します。

なので簡単なチュートリアルを順番にこなしてAuth0についての機能を一つづつ理解していくのがなんだかんだで一番近道だと思います。


## 参考文献
[Auth0公式チュートリアル](https://auth0.com/docs/quickstart/backend/laravel/01-authorization)
[Auth0公式チュートリアルのサンプルコード(GitHub)](https://github.com/auth0-samples/auth0-laravel-api-samples/tree/master/01-Authorization-RS256)
[Auth0-トラブルシューティング](https://auth0.com/docs/quickstart/backend/laravel/03-troubleshooting)
[自分のAuth0勉強用ブランチ(GitHub)](https://github.com/yumemi-dendo/sample-lighthouse_and_auth0)