13
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Auth0】LaravelでAuth0を使って認証する:IDトークン編【Laravel】

Last updated at Posted at 2020-07-01

今回はSPAを作成する際に使用するJWT形式のIDトークンを検証をする方法について記事にしました。

関連記事

Validating JWTs with Auth0-PHP
【Auth0】LaravelでAuth0を使って認証する:アクセストークン編【Laravel】

環境

PHP: 7.2.5
laravel: 7.0
auth0/auth0-php: 7.2

Auth0の設定

追記:後述のSPA ApplicationはデフォルトのManegimant APIを使用するので、APIを作成する必要はありませんでした。

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

https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_391127_ba0dde3b-fa26-f2ec-74b0-a3bbf289a671.png

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

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あたりを設定しておきましょう。

https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_391127_ecf2609b-fde8-2eff-0ad4-f67dc84fa7bd.png

SPA Applicationの作成

今回はID Tokenでの認証を行うので、Single Page Appliation(SPA)用のAuth0 Appを作成します。

Applicationsタブの右上のCREATE APPLICATIONを選択します。

スクリーンショット 2020-06-21 11.44.59.png

好きな名前を設定し、Single Page Web Applicationsを選択して新規にAppを作成します。

スクリーンショット 2020-06-21 11.45.28.png

ライブラリの導入と設定

ID Tokenの検証にはAuth0のPHP用のライブラリを使用します。

$ composer require auth0/auth0-php

auth0/loginというライブラリがありますが、間違えないように注意しましょう。

次に.envにAuth0との接続情報を記入します。

.env
AUTH0_DOMAIN=tenant.auth0.com
AUTH0_CLIENT_ID=hogehogeClientId
AUTH0_CLIENT_SECRET=hogehogeClientSecret

Applicationsから先ほど作成したSPAアプリを選択し、そのDomainClient IDClient Secretを使用します。

スクリーンショット 2020-06-22 20.42.26.png

また定数クラスを作成しておきます。

config/const.php
return [
    'auth0' => [
        'domain' => env('AUTH0_DOMAIN', ''),
        'client_id' => env('AUTH0_CLIENT_ID', ''),
        'client_secret' => env('AUTH0_CLIENT_SECRET', ''),
    ]
];

Laravelでの実装

まずはID Tokenを検証するためのミドルウェアを作成します。

$ php artisan make:middleware CheckIdToken
App\Http\Middleware\CheckIdToken.php
namespace App\Http\Middleware;

use Auth0\SDK\Helpers\JWKFetcher;
use Auth0\SDK\Helpers\Tokens\AsymmetricVerifier;
use Auth0\SDK\Helpers\Tokens\IdTokenVerifier;
use Auth0\SDK\Helpers\Tokens\SymmetricVerifier;
use Closure;

class CheckIdToken
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        // リクエストヘッダにBearerトークンが存在するか確認
        if (empty($request->bearerToken())) {
            return response()->json(["message" => "Token dose not exist"], 401);
        }

        $id_token = $request->bearerToken();

        // JWTのヘッダー部分を取得し、デコードしてalgを取り出す
        $id_token_header = explode('.', $id_token)[0];

        try {
            $token_alg = json_decode(base64_decode($id_token_header))->alg;
        } catch (\Exception $e) {
            return response()->json(["message" => $e->getMessage()], 401);
        }

        $token_issuer = 'https://' . config('const.auth0.domain') . '/';

        $signature_verifier = null;

        // id tokenを検証するためのVerifierクラスを呼び出す
        // RS256のみで検証したい場合はHS256の分岐を削除する
        if ('RS256' === $token_alg) {
            // 指定したissuerからjwksを取得し、証明書(CERTIFICATE)で取得する
            $jwks_fetcher = new JWKFetcher();
            $jwks = $jwks_fetcher->getKeys($token_issuer.'.well-known/jwks.json');
            $signature_verifier = new AsymmetricVerifier($jwks);
        } else if ('HS256' === $token_alg) {
            $signature_verifier = new SymmetricVerifier(config('const.auth0.client_secret'));
        } else {
            return response()->json(["message" => "Invalid alg"]);
        }

        $token_verifier = new IdTokenVerifier(
            $token_issuer,
            config('const.auth0.client_id'),
            $signature_verifier
        );

        // トークンを検証する
        try {
            $decoded_token = $token_verifier->verify($id_token);
        } catch (\Exception $e) {
            return response()->json(["message" => $e->getMessage()], 401);
        }

        // user_idを$requestに追加する。
        $request->merge([
            'auth0_user_id' => $decoded_token['sub']
        ]);

        return $next($request);
    }
}

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

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

最後にエンドポイントにミドルウェアを設定します。

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

ID Tokenを発行する

Auth0にユーザを登録する

まずは作成したAuth0のアプリケーションにSSO(シングルサインオン)でアクセスしてユーザをAuth0に登録しましょう。

今回はGoogleのSSOを使用します。

ConnectionsSocialタブからデフォルトで有効化しているGoogleを選択し、トグルスイッチの上にあるTRYをクリックします。

スクリーンショット 2020-06-20 23.52.24.png

もしくはGoogleと表示されている部分をクリックして設定を展開し、一番下にあるTRYをクリックします。

スクリーンショット 2020-06-20 23.52.43.png

するとGoogleのログイン画面が表示され、ログインが成功するとAuth0にリダイレクトされ、ログインしたユーザの情報が表示されます。

スクリーンショット 2020-06-20 23.53.01.png

この状態でUsers & RolesからUsersタブを開くと先ほどログインしたユーザが追加されているのがわかります。(画像はほとんど黒塗りでわかりにくいかもしれません)

スクリーンショット 2020-06-20 23.53.15.png

Authentication API Debuggerの登録

今回はID Tokenを取得するのにAuth0 Authentication API Debuggerを使用します。

ExtensionsタブからAuth0 Authentication API Debuggerを検索し、有効化します。

スクリーンショット 2020-06-20 23.53.48.png スクリーンショット 2020-06-20 23.53.54.png

そうしたらCallback URLの値を最初に作成したLaravel-SampleアプリケーションのAllowed Callback URLsに登録します。

スクリーンショット 2020-06-28 17.09.08.png スクリーンショット 2020-06-20 23.56.02.png

入力したらページの最下部にあるSAVE CHANGESを押して変更を保存します。

ID Tokenを取得する

Authentication API DebuggerLoginConfiguration内のApplicationにID Tokenを取得したいアプリケーションを指定します。

スクリーンショット 2020-06-28 17.19.24.png

次にOAuth2/OIDCタブの下の方にあるSettingResponse TypeNoncetoken id_tokenを設定します。

スクリーンショット 2020-06-20 23.54.45.png

最後にUser FlowsOAUTH2 / OIDC LOGINと書いてある水色のボタンを押すと画面が遷移し、ID Tokenを取得できます。

スクリーンショット 2020-06-20 23.55.00.png

今回はtoken id_tokenを指定したのでアクセストークンとIDトークンの両方を取得しています。

またID TokenのブロックではIDトークン(JWT)をデコードした中身をみることができます。

スクリーンショット 2020-06-28 17.33.14.png

リクエストを送る

IDトークンを取得できたのでサーバーを起動してリクエストを送ってみます。

$ php artisan serve --port=3010

先ほど取得したIDトークンをAuthorizationヘッダーにセットしてhttp://localhost:3010/api/privateにリクエストしましょう。

{
  "autho_user_id": "google-oauth2|1234567890",
  "message": "プライベートなエンドポイントへようこそ!これを表示するには有効なアクセストークンが必要です。"
}

jwksをキャッシュする

この実装だとリクエストのたびにjwksを取得しに行ってしまいます。
なのでキャッシュする実装に変更しましょう。

Auth0のQ&Aを見ると、jwksはトークンの検証に失敗した際に再取得しにいくのが良いようです。

※Redisや関連ライブラリの導入方法に関しての説明は省きます。

App\Http\Middleware\CheckIdToken.php
class CheckIdToken
{
    public function handle($request, Closure $next)
    {
        //...中略...

        $signature_verifier = null;

        if ('RS256' === $token_alg) {
            // キャッシュに保存する
            $jwks = Cache::remember('auht0_jwks_key', 43200, function () use ($token_issuer) {
                $jwks_fetcher = new JWKFetcher();
                return $jwks_fetcher->getKeys($token_issuer.'.well-known/jwks.json');
            });
            $signature_verifier = new AsymmetricVerifier($jwks);
        } else if ('HS256' === $token_alg) {
            $signature_verifier = new SymmetricVerifier(config('const.auth0.client_secret'));
        } else {
            return response()->json(["message" => "Invalid alg"]);
        }

        $token_verifier = new IdTokenVerifier(
            $token_issuer,
            config('const.auth0.client_id'),
            $signature_verifier
        );

        // トークンを検証する
        try {
            $decoded_token = $token_verifier->verify($id_token);
        } catch (\Exception $e) {
            logger()->info('id_tokenの初回検証に失敗しました。 Caught: Exception - '.$e->getMessage());

            // 検証に失敗したら一度だけjwksを最新のものに更新し、再度検証する
            // 実際に実装する際はprivate functionなどに抜き出して共通化すると良いでしょう
            $jwks_fetcher = new JWKFetcher();
            $new_jwks = $jwks_fetcher->getKeys($token_issuer.'.well-known/jwks.json');
            Cache::put('auht0_jwks_key', $new_jwks, 43200)

            $new_signature_verifier = new AsymmetricVerifier($new_jwks);

            $new_token_verifier = new IdTokenVerifier(
                $token_issuer,
                config('const.auth0.client_id'),
                $new_signature_verifier
            );

            try {
                $decoded_token = $new_token_verifier->verify($id_token);
            } catch (\Exception $e) {
                logger()->warning('id_tokenの2回目の検証に失敗しました。');
                return response()->json(["message" => $e->getMessage()], 401);
            };
        }

        // user_idを$requestに追加する。
        $request->merge([
            'auth0_user_id' => $decoded_token['sub']
        ]);

        return $next($request);
    }
}

本当はjwks内に一致するkidが存在しない場合のみ更新するようにしたかったのですが、トークンの検証失敗時には全てInvalidTokenExceptionという型が投げられ判別できなかったので、このような形になっています。

参考資料

自分のメモ代わりとして、参考資料へのリンクは多めに貼ってあります。

【Auth0】LaravelでAuth0を使って認証する:アクセストークン編【Laravel】
SPA + API: Solution Overview
Validating JWTs with Auth0-PHP
Auth0 Quickstarts - PHP
Auth0-PHP
Auth0 Authentication API Debugger
Caching JWKS signing key
Validate JSON Web Tokens

13
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?