2
2

More than 1 year has passed since last update.

LaravelでAuth0のIDトークン(JWT)を検証・デコードする

Last updated at Posted at 2022-06-02

環境

前提

Auth0でSingle Page Appliationを作成し、ID Token発行できる状態を前提とします。
詳細な説明・方法は【Auth0】LaravelでAuth0を使って認証する:IDトークン編【Laravel】@akkino_D-Enがわかりやすかったので参照してください。

注意

auth0-php7系では今回の方法で実装できません
また、auth0/loginという似たライブラリもあるので注意してください。

実装

1. auth0-phpのインストール

実行:composer require auth0/auth0-php

.envAuth0から取得した環境変数を追加します。

.env.example
# 追加
AUTH0_DOMAIN=tenant.auth0.com
AUTH0_CLIENT_ID=client_id
AUTH0_CLIENT_SECRET=secret
.env
# 追加
AUTH0_DOMAIN=******.auth0.com
AUTH0_CLIENT_ID=******
AUTH0_CLIENT_SECRET=******

config/auth0.phpを作成し、config()で取得できるようにします。
実行:touch config/auth0.php

config/auth0.php
<?php

return [
    'domain' => env('AUTH0_DOMAIN'),
    'clientId' => env('AUTH0_CLIENT_ID'),
    'clientSecret' => env('AUTH0_CLIENT_SECRET'),
];

2. ミドルウェアの作成

実行:php artisan make:middleware CheckIdToken

ミドルウェアをKernel.phpに登録

app/Http/Kernel.php
    protected $routeMiddleware = [
        //....
        'auth0' => \App\Http\Middleware\CheckIdToken::class, //追加
  ];

ミドルウェアを編集

app/Http/Middleware/CheckIdToken.php
<?php

namespace App\Http\Middleware;

use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Auth0\SDK\Auth0;
use Auth0\SDK\Token;
use Closure;

class CheckIdToken
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $configure = [
            'domain'       => config('auth0.domain'),
            'clientId'     => config('auth0.clientId'),
            'clientSecret' => config('auth0.clientSecret'),
            'tokenJwksUri' => 'https://'.config('auth0.domain').'/.well-known/jwks.json',
            'tokenCache' => null,
            'tokenCacheTtl' => 43200
        ];

        $auth0 = new Auth0($configure);

        // SDKの設定でキャッシュを有効化させる
        $tokenCache = new FilesystemAdapter();
        $auth0->configuration()->setTokenCache($tokenCache);

        // リクエストヘッダにBearerトークンが存在するか確認
        if (empty($request->bearerToken())) {
            return response()->json(["message" => "Token dose not exist"], 401);
        }

        $id_token = $request->bearerToken();

        // IDトークンの検証・デコード
        try {
            $auth0->decode($id_token, null, null, null, null, null, null, Token::TYPE_ID_TOKEN);
        } catch (\Exception $e) {
            return response()->json([
                "message" => config('app.debug') ? $e->getMessage() : "401: Unauthorized"
            ], 401);
        }

        $token = new Token($auth0->configuration(), $id_token, Token::TYPE_ID_TOKEN);
        $payload = json_decode($token->toJson()); //IDトークンに格納されたClaimを取得

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

        return $next($request);
    }

解説

Auth0\SDK\Auth0decode()を使ってID Tokenを検証・デコードしています。
ID Tokenにはemailsubなどの情報が含まれているため、この段階で$auth0にユーザ情報が追加されています。

decode()は成功時に$thisを返します。dd()で中を見るとtokenClaims tokenHeaders tokenSignatureが含まれていることがわかります。
これらのプロパティはprivateなため、直接参照できません(arrayキャストすれば一応できますが)。

Auth0\SDK\Tokenにgetterがあるのでそちらを使います。

$token = new Token($auth0->configuration(), $id_token, Token::TYPE_ID_TOKEN);

// 'aud' クレームを取得
$token->getAudience();

// 'azp' クレームを取得
$token->getAuthorizedParty();

// 'auth_time' クレームを取得
$token->getAuthTime();

// 'exp' クレームを取得
$token->getExpiration();

// 'iat' クレームを取得
$token->getIssued();

// 'iss' クレームを取得
$token->getIssuer();

// 'nonce' クレームを取得
$token->getNonce();

// 'org_id' クレームを取得
$token->getOrganization();

// 'sub' クレームを取得
$token->getSubject();

// ID Tokenのペイロード部を連想配列として取得
$payload = $token->toArray();

// ID Tokenのペイロード部をJson形式の文字列として取得
$payload = $token->toJson();

ID Tokenの詳細な仕様はIDトークンが分かれば OpenID Connect が分かる@TakahikoKawasakiがわかりやすいです。

subをプライマリーキーの値として実装するケースが一般的なようなので、今回も$requestにsubを追加し、コントローラに渡しました。

試しにapi.phpもしくはweb.phpにエンドポイントを追加します。

routes/api.php
Route::group(['middleware' => ['auth0']], function () {
    Route::get('/test', function (Request $request) {
        return response()->json(["auth_id" => $request['auth0_user_id']);
    });
});

Postmanなどを用いてHeadersKEYAuthorizationを追加、VALUEID Token

Bearer eyJhbGciOi******.************.******

のように追加して送信します。
するとsubの値が取得できるはずです。

3. ユーザのデータを取得する

最後はおまけです。
認証済みユーザーがアクセスできるエンドポイントには常にユーザ情報が取得できる状態にしたかったので、getAuthUser()を作成してDBかキャッシュから取得しています。

ど素人文系大学生の実装なのであくまで参考程度にしてください逆にアドバイスなどがあればお願いします)。

app/Http/Middleware/CheckIdToken.php
<?php

namespace App\Http\Middleware;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Auth0\SDK\Auth0;
use Auth0\SDK\Token;
use Closure;

class CheckIdToken
{
    /**
     * ユーザデータをキャッシュする時間
     *
     * @var integer
     */
    private int $cache_minutes = 30;

    /**
     * IDトークンから取得した`sub`(`auth_id`)
     *
     * @var string|null
     */
    private ?string $auth_id = null;

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $configure = [
            'domain'       => config('auth0.domain'),
            'clientId'     => config('auth0.clientId'),
            'clientSecret' => config('auth0.clientSecret'),
            'tokenJwksUri' => 'https://'.config('auth0.domain').'/.well-known/jwks.json',
            'tokenCache' => null,
            'tokenCacheTtl' => 43200
        ];

        $auth0 = new Auth0($configure);

        // SDKの設定でキャッシュを有効化させる
        $tokenCache = new FilesystemAdapter();
        $auth0->configuration()->setTokenCache($tokenCache);

        // リクエストヘッダにBearerトークンが存在するか確認
        if (empty($request->bearerToken())) {
            return response()->json(["message" => "Token dose not exist"], 401);
        }

        $id_token = $request->bearerToken();

        // IDトークンの検証・デコード
        try {
            $auth0->decode($id_token, null, null, null, null, null, null, Token::TYPE_ID_TOKEN);
        } catch (\Exception $e) {
            return response()->json([
                "message" => config('app.debug') ? $e->getMessage() : "401: Unauthorized"
            ], 401);
        }

        //IDトークンに格納されたClaimを取得
        $token = new Token($auth0->configuration(), $id_token, Token::TYPE_ID_TOKEN);
        $payload = json_decode($token->toJson());

        $this->auth_id = $payload->sub;

        $user = json_decode($this->getAuthUser());

        // ユーザのデータが存在しない場合
        if(!$user){
            return response()->json(["message" => "User profile is not registered"]);
        }

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

        return $next($request);
    }

    /**
     * キャッシュから`auth_id`に一致するユーザを返す。存在しな場合はDBを参照する
     *
     * @return ?string
     */
    private function getAuthUser()
    {
        return Cache::remember($this->auth_id, $this->cache_minutes, function () {
            $u = DB::table('users')->where('auth_id', $this->auth_id)->get();
            return json_encode($u);
        });
    }
}

今後やりたいこと

自動テストの実行のためにID Tokenを自動で取得できるようにしたいです。
次回、気が向いたら記事にします。

参考記事

2
2
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
2
2