環境
前提
Auth0でSingle Page Appliation
を作成し、ID Token
を発行できる状態を前提とします。
詳細な説明・方法は【Auth0】LaravelでAuth0を使って認証する:IDトークン編【Laravel】@akkino_D-Enがわかりやすかったので参照してください。
注意
auth0-php
の7系では今回の方法で実装できません。
また、auth0/login
という似たライブラリもあるので注意してください。
実装
1. auth0-phpのインストール
実行:composer require auth0/auth0-php
.env
にAuth0
から取得した環境変数を追加します。
# 追加
AUTH0_DOMAIN=tenant.auth0.com
AUTH0_CLIENT_ID=client_id
AUTH0_CLIENT_SECRET=secret
# 追加
AUTH0_DOMAIN=******.auth0.com
AUTH0_CLIENT_ID=******
AUTH0_CLIENT_SECRET=******
config/auth0.php
を作成し、config()
で取得できるようにします。
実行:touch 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
に登録
protected $routeMiddleware = [
//....
'auth0' => \App\Http\Middleware\CheckIdToken::class, //追加
];
ミドルウェアを編集
<?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\Auth0
のdecode()
を使ってID Token
を検証・デコードしています。
ID Token
にはemail
やsub
などの情報が含まれているため、この段階で$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
にエンドポイントを追加します。
Route::group(['middleware' => ['auth0']], function () {
Route::get('/test', function (Request $request) {
return response()->json(["auth_id" => $request['auth0_user_id']);
});
});
Postman
などを用いてHeaders
のKEY
にAuthorization
を追加、VALUE
にID Token
を
Bearer eyJhbGciOi******.************.******
のように追加して送信します。
するとsub
の値が取得できるはずです。
3. ユーザのデータを取得する
最後はおまけです。
認証済みユーザーがアクセスできるエンドポイントには常にユーザ情報が取得できる状態にしたかったので、getAuthUser()
を作成してDBかキャッシュから取得しています。
ど素人文系大学生の実装なのであくまで参考程度にしてください(逆にアドバイスなどがあればお願いします)。
<?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
を自動で取得できるようにしたいです。
次回、気が向いたら記事にします。