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を作成します。
API名と識別子は先例に習ってQuickstarts API
とhttps://quickstarts/api
にします。またアルゴリズムは特に理由がなければRS256
にしましょう。
RS256
を使用することでJWKs
を使用したトークン検証を行えるようになります。
ものすごく丁寧なことに今作成したアプリケーションにアクセスするためのクイックスタートを記載したページに飛ばしてくれます。
ですが今は一旦無視してPermissions
タブを開きましょう。
ここではアクセストークンを使用してAuth0からどの情報まで取得できるかの権限を設定することができます。
とりあえず今はread:messages
, read:email
, read:users
あたりを設定しておきましょう。
ライブラリの導入と設定
Auth0側の準備が完了したので、次はLaravelの実装に移ります。
まずはアクセストークンを検証するためのライブラリを導入します。
$ composer require auth0/login
この時、auth0/auth0-phpという似た名前のライブラリがあるので注意しましょう。
以下のコマンドで設定ファイルを生成します。
$ php artisan vendor:publish --provider "Auth0\Login\LoginServiceProvider"
これによりconfig/laravel-auth0.php
に設定ファイルが生成されるので、以下のように認証に必要な値を設定します。
return [
// 許可されたトークン発行者のドメイン(テナントURL)
'authorized_issuers' => [ env('AUTH0_DOMAIN') ],
// Auth0 APIの識別子
'api_identifier' => env('AUTH0_API_IDENTIFIER'),
// 署名アルゴリズム
'supported_algs' => [ 'RS256' ],
];
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
の値をそれぞれ入力しましょう。
AUTH0_CLIENT_ID
とAUTH0_CLIENT_SECRET
にはApplications
からQuickstarts APIを選択し、そのClient IDとClient Secretを使用します。
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
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
という名前でカーネル登録します。
class Kernel extends HttpKernel {
// ...
protected $routeMiddleware = [
// ...
'jwt' => \App\Http\Middleware\CheckJWT::class,
// ...
];
// ...
}
では早速各エンドポイントにミドルウェアを設定しましょう。
// 認証が必要ないエンドポイント
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
からアクセストークンをコピーします。
そしたらトークンをAuthorization
ヘッダーにセットしてhttp://localhost:3010/api/private
にリクエストしましょう。
{"message": "プライベートなエンドポイントへようこそ!これを表示するには有効なアクセストークンが必要です。"}
スコープの構成
ここまでで作成したミドルウェアはアクセストークンの存在と有効性は確認することができますが、それに含まれるスコープを確認することはできません。
なので次は特定のスコープがアクセストークンに含まれるかをチェックするミドルウェアを作成しましょう。
php artisan make:middleware CheckScope
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に登録します。
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 ミドルウェア-ミドルウェアパラメータ
// このエンドポイントの認証には"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 API
のAUTHORIZED
ボタンをクリックし一旦UNAUTHORIZED
にしてから再度AUTHORIZED
に変更します。
するとパーミッション(スコープ)選択画面が表示されるので、read:messages
スコープを選択してUPDATE
ボタンを押します。
これでスコープの設定は完了したので、Test
タブからアクセストークンを取得しリクエストしてみましょう。
{"message": "プライベートなエンドポイントへようこそ!これを表示するには有効なアクセストークンとread:messagesのスコープが必要です。"}
アクセストークンの中身を見てみる
JWTの解説に関しては探せばいくらでも良質な記事が出てくるので、ここでは簡単な確認方法だけ記載します。
jwt.ioのEncodedの部分にアクセストークンをコピペするだけで中身を確認することができます。
先ほど作成したアクセストークンをデコードしてみると、Payload内にscope
というkeyと値が追加されています。
CheckScope
ミドルウェアではこれを参照して判定しています。
おまけ:Auth0ダッシュボード以外からのアクセストークンの取得フロー
今回はチュートリアルということもあり、Auth0のダッシュボード上から有効期限の短いテスト用のアクセストークンを発行していました。
実際にプロダクトで使用する際には以下のフローを参考に取得してください。
- Webアプリケーションの場合:Authorization Code Flow
- モバイルやデスクトップ、その他のネイティブアプリケーションの場合:Authorization Code Flow with PKCE
- バックエンドで実行されているCLIやデーモン、サービスの場合:Client Credentials Flow
(そのうち別の記事でまとめたい)
おまけその2:CORSを構成する
larave-cors
を追加し、Kernelのmiddleware
にHandleCors
を追加します。
(Laravel7以前の場合は最初から含まれているので不要です。config/cors.php
だけ書き換えましょう。)
protected $middleware = [
// ...
\Barryvdh\Cors\HandleCors::class,
];
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)