17
12

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を使って認証する:アクセストークン編【Laravel】

Last updated at Posted at 2020-06-08

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

今回はLaravelでのJWT形式のアクセストークンの認証のチュートリアルに関してまとめます。
Auth0公式チュートリアル

関連記事

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

環境

PHP: 7.2.5
laravel: 7.0
Auth0/login: 6.0

Auth0の設定

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

スクリーンショット 2020-06-03 23.48.57.png

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

スクリーンショット 2020-06-03 23.54.54.png

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

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

スクリーンショット 2020-06-04 0.10.50.png

ライブラリの導入と設定

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

$ composer require auth0/login

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

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

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

これによりconfig/laravel-auth0.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_DOMAINSettingsCustom Domainsから、 AUTH0_API_IDENTIFIERAPIsQuickstartsIdentifierの値をそれぞれ入力しましょう。

スクリーンショット 2020-06-04 0.40.16.png スクリーンショット 2020-06-04 0.33.38.png

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

スクリーンショット 2020-06-07 18.08.01.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スコープが付与されたアクセストークンが含まれたリクエストでの認証により使用可能

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

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

php artisan make:middleware CheckJWT
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という名前でカーネル登録します。

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

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

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でリクエストを送ります。

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

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

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

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

スクリーンショット 2020-06-07 18.15.18.png

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

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

スコープの構成

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

php artisan make:middleware CheckScope
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に登録します。

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 ミドルウェア-ミドルウェアパラメータ

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にアクセスしてみます。

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

{ "message": "Insufficient scope" }

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

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

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

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

スクリーンショット 2020-06-07 23.07.50.png

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

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

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

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

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

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

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

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

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

(そのうち別の記事でまとめたい)

おまけその2:CORSを構成する

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

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

config/cors.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公式チュートリアル
Auth0公式チュートリアルのサンプルコード(GitHub)
Auth0-トラブルシューティング
自分のAuth0勉強用ブランチ(GitHub)

17
12
2

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
17
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?