今回は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を作成します。
API名と識別子は先例に習ってQuickstarts API
とhttps://quickstarts/api
にします。またアルゴリズムは特に理由がなければRS256
にしましょう。
RS256
を使用することでJWKs
を使用したトークン検証を行えるようになります。
ものすごく丁寧なことに今作成したアプリケーションにアクセスするためのクイックスタートを記載したページに飛ばしてくれます。
ですが今は一旦無視してPermissions
タブを開きましょう。
ここではアクセストークンを使用してAuth0からどの情報まで取得できるかの権限を設定することができます。
とりあえず今はread:messages
, read:email
, read:users
あたりを設定しておきましょう。
SPA Applicationの作成
今回はID Tokenでの認証を行うので、Single Page Appliation
(SPA)用のAuth0 Appを作成します。
Applications
タブの右上のCREATE APPLICATION
を選択します。
好きな名前を設定し、Single Page Web Applications
を選択して新規にAppを作成します。
ライブラリの導入と設定
ID Tokenの検証にはAuth0のPHP用のライブラリを使用します。
$ composer require auth0/auth0-php
auth0/login
というライブラリがありますが、間違えないように注意しましょう。
次に.env
にAuth0との接続情報を記入します。
AUTH0_DOMAIN=tenant.auth0.com
AUTH0_CLIENT_ID=hogehogeClientId
AUTH0_CLIENT_SECRET=hogehogeClientSecret
Applications
から先ほど作成したSPAアプリを選択し、そのDomain
、Client ID
、Client Secret
を使用します。
また定数クラスを作成しておきます。
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
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という名前でカーネル登録します。
class Kernel extends HttpKernel {
// ...
protected $routeMiddleware = [
// ...
'jwt' => \App\Http\Middleware\CheckIdToken::class,
// ...
];
// ...
}
最後にエンドポイントにミドルウェアを設定します。
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を使用します。
Connections
→Social
タブからデフォルトで有効化しているGoogleを選択し、トグルスイッチの上にあるTRY
をクリックします。
もしくはGoogleと表示されている部分をクリックして設定を展開し、一番下にあるTRY
をクリックします。
するとGoogleのログイン画面が表示され、ログインが成功するとAuth0にリダイレクトされ、ログインしたユーザの情報が表示されます。
この状態でUsers & Roles
からUsers
タブを開くと先ほどログインしたユーザが追加されているのがわかります。(画像はほとんど黒塗りでわかりにくいかもしれません)
Authentication API Debuggerの登録
今回はID Tokenを取得するのにAuth0 Authentication API Debuggerを使用します。
Extensions
タブからAuth0 Authentication API Debugger
を検索し、有効化します。
そうしたらCallback URL
の値を最初に作成したLaravel-Sample
アプリケーションのAllowed Callback URLs
に登録します。
入力したらページの最下部にあるSAVE CHANGES
を押して変更を保存します。
ID Tokenを取得する
Authentication API Debugger
のLogin
→Configuration
内のApplication
にID Tokenを取得したいアプリケーションを指定します。
次にOAuth2/OIDC
タブの下の方にあるSetting
でResponse Type
とNonce
にtoken id_token
を設定します。
最後にUser Flows
のOAUTH2 / OIDC LOGIN
と書いてある水色のボタンを押すと画面が遷移し、ID Tokenを取得できます。
今回はtoken id_token
を指定したのでアクセストークンとIDトークンの両方を取得しています。
またID Token
のブロックではIDトークン(JWT)をデコードした中身をみることができます。
リクエストを送る
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や関連ライブラリの導入方法に関しての説明は省きます。
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