Laravel の TokenGuard を見直してみる
Guard とは?
Laravel の config/auth.php でお馴染みのコレ。
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
この driver の値 'session' は、
認証機構の種類を表しており、デフォルトでは 'session' と 'token' が用意されている他、
Sanctum を利用していれば 'sanctum' も指定可能です。
Auth::extend() で独自の Guard を定義し、ドライバとして指定することもできます。
Guard (driver) の概要
session とか token などのエイリアスが付けられていますが、実際に指しているものは、
Guard インターフェースを実装したクラスです。
'session' であれば SessionGuard 、'token' であれば TokenGuard になります。
Guard インターフェースを見れば、これらのクラスが果たしている役割をイメージしやすいでしょう。
namespace Illuminate\Contracts\Auth;
interface Guard
{
/**
* Determine if the current user is authenticated.
*
* @return bool
*/
public function check();
/**
* Determine if the current user is a guest.
*
* @return bool
*/
public function guest();
/**
* Get the currently authenticated user.
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user();
/**
* Get the ID for the currently authenticated user.
*
* @return int|string|null
*/
public function id();
/**
* Validate a user's credentials.
*
* @param array $credentials
* @return bool
*/
public function validate(array $credentials = []);
/**
* Determine if the guard has a user instance.
*
* @return bool
*/
public function hasUser();
/**
* Set the current user.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @return void
*/
public function setUser(Authenticatable $user);
}
ざっくり言えば、現在のリクエストにおいて、有効にログインしているユーザを「識別」するためのクラスです。
SessionGuard はこれをセッションにより識別しており、
namespace Illuminate\Auth;
// 略
class SessionGuard implements StatefulGuard, SupportsBasicAuth
{
// 略
public function user()
{
if ($this->loggedOut) {
return;
}
if (! is_null($this->user)) {
return $this->user;
}
$id = $this->session->get($this->getName());
if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) {
$this->fireAuthenticatedEvent($this->user);
}
// 略
return $this->user;
}
// 略
}
TokenGuard では、これを Authorization ヘッダやクエリパラメータ等から識別しています。
namespace Illuminate\Auth;
// 略
class TokenGuard implements Guard
{
// 略
public function user()
{
if (! is_null($this->user)) {
return $this->user;
}
$user = null;
$token = $this->getTokenForRequest();
if (! empty($token)) {
$user = $this->provider->retrieveByCredentials([
$this->storageKey => $this->hash ? hash('sha256', $token) : $token,
]);
}
return $this->user = $user;
}
public function getTokenForRequest()
{
$token = $this->request->query($this->inputKey);
if (empty($token)) {
$token = $this->request->input($this->inputKey);
}
if (empty($token)) {
$token = $this->request->bearerToken();
}
if (empty($token)) {
$token = $this->request->getPassword();
}
return $token;
}
}
なお、ユーザの「取得」自体は、どちらもコンストラクタで受け取る UserProvider クラス
($this->provider) に投げているため、
Guard 自体はモデル横断で使えるようになっています。
※Guard と UserProvider の関係については [図解] Laravel の認証周りのややこしいあれこれ。
でもう少しだけ詳しく触れています。
また、ログインやトークンの発行自体はこのクラスは担っていない、という点にも留意が必要です。
あくまでログイン済のユーザを識別することがこのクラスの責務です。
お気づきかもしれませんが、普段よく使用する
Auth::user()
Auth::id()
Auth::check()
Auth::guard(xxx)->user()
Auth::guard(xxx)->id()
Auth::guard(xxx)->check()
は、これらのクラスのメソッドを呼び出しています。
(id() や check() は、両クラスが使用している GuardHelper トレイトにあります)
TokenGuard を使ってみる
さて、TokenGuard が想定している仕様は
- ユーザテーブルにアクセストークンを持っている
- Bearer もしくはクエリストリングでトークンを受け取る
というもののようです。
ユーザテーブルのトークンを格納するフィールド名、
クエリストリングでトークンを受け取る場合のキーは、
ともに 'api_token' となっています。
シンプルなAPI認証であれば、このままでも十分使えそうです。
// config/auth.php
'guards' => [
'api' => [
'driver' => 'token',
'provider' => 'users',
],
],
格納フィールド名、キー名はコンストラクタで渡せるようになっているため、
変更したい場合は、下記のように TokenGuard クラスを用いてカスタムガードを作ることができますね。
// AuthServiceProvider
public function boot(): void
{
Auth::extend('custom_token', static function ($app, $name, $config) {
return new TokenGuard(
provider: $app['auth']->createUserProvider($config['provider'] ?? null),
request: $app->make('request'),
inputKey: 'token', // トークンをクエリストリングで渡す場合のキー
storageKey: 'access_token', // ユーザテーブルのトークンを格納するフィールド
);
});
}
// config/auth.php
'guards' => [
'api' => [
'driver' => 'custom_token',
'provider' => 'users',
],
],
TokenGuard を期限付きトークンに対応 (パターンA. TokenGuard の継承)
しかしこれだけでは、トークンの有効期限をチェックする機構がありません。
アプローチとしては、
- TokenGuard を継承した Guard クラスを作る
- UserProvider クラスを作りチェックを行う
の2つがありそうです。
期限チェックは Guard の責務といって良さそうですので、まずは、
TokenGuard を継承してカスタマイズしてみます。
namespace App\Auth;
use Illuminate\Auth\TokenGuard;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Http\Request;
class ExpiringTokenGuard extends TokenGuard implements Guard
{
// トークンの有効期限を保持しているユーザテーブルのフィールド
protected string $storageExpiredKey;
public function __construct(
UserProvider $provider,
Request $request,
$inputKey = 'token',
$storageKey = 'access_token',
$storageExpiredKey = 'access_token_expired',
$hash = false)
{
$this->storageExpiredKey = $storageExpiredKey;
parent::__construct($provider, $request, $inputKey, $storageKey, $hash);
}
public function user()
{
if (! is_null($this->user)) {
return $this->user;
}
$user = null;
$token = $this->getTokenForRequest();
if (! empty($token)) {
$user = $this->provider->retrieveByCredentials([
$this->storageKey => $this->hash ? hash('sha256', $token) : $token,
]);
}
// トークンの有効期限チェック
if (
$user
&& $user->{$this->storageExpiredKey}
&& $user->{$this->storageExpiredKey}->lte(now())
) {
return null;
}
return $this->user = $user;
}
}
コメントを入れた箇所以外は、TokenGuard の内容そのままです。
これを AuthServiceProvider で「カスタムガード」として登録し、config に設定します。
// AuthServiceProvider
public function boot(): void
{
Auth::extend('expiring_token', static function ($app, $name, $config) {
return new ExpiringTokenGuard(
provider: $app['auth']->createUserProvider($config['provider'] ?? null),
request: $app->make('request'),
);
});
}
// config/auth.php
'guards' => [
'api' => [
'driver' => 'expiring_token',
'provider' => 'users',
],
],
簡単ですね。
TokenGuard を期限付きトークンに対応 (パターンB. UserProvider の定義)
UserProvider クラスを用いた方法も見てきましょう。
namespace App\Auth;
use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
class TokenUserProvider extends EloquentUserProvider implements UserProvider
{
public function retrieveByCredentials(array $credentials): Authenticatable|null
{
$user = parent::retrieveByCredentials(['access_token' => $credentials['api_token']]);
if (
$user
&& $user->access_token_expired?->lte(now())
) {
return null;
}
return $user;
}
}
デフォルトで使用される EloquentUserProvider を継承して、
ユーザの取得後に有効期限のチェックを入れています。
(access_token_expired が万一 null であった場合の例外処理はお好みで。)
これを今度は AuthServiceProvider で「カスタムプロバイダ」として登録し、config に設定します。
// AuthServiceProvider
public function boot(): void
{
Auth::provider('token_user_provider', static function ($app, array $config) {
return new TokenUserProvider($app['hash'], $config['model']);
});
}
// config/auth.php
'guards' => [
'api' => [
'driver' => 'token',
'provider' => 'token_users',
],
],
'providers' => [
'token_users' => [
'driver' => 'token_user_provider',
'model' => App\Models\User::class,
],
],
この場合、クエリストリングのキーを変更する必要がなければ(あるいはトークンをクエリストリングを受け取る必要がなければ)、
driver はデフォルトの 'token' のままで足ります。
やや責務の逸脱を感じますが、トークンを別テーブルで管理している場合など、より深くカスタマイズする必要がある場合は、
このアプローチの方が見通しが良いかもしれません。
TokenGuard は deprecated なのか?
ところで、Laravel 7.x までは、デフォルトの config/auth.php は以下のようになっていました。
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| here which uses session storage and the Eloquent user provider.
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| Supported: "session", "token"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
],
ところが、8.x 以降は、'token' を用いたサンプルが消え、
コメントの 'Supported' からも消えました。
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| here which uses session storage and the Eloquent user provider.
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| Supported: "session"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
同時に、config/sanctum.php がデフォルトで入るようになったため、
従来の TokenGuard を捨て、Sanctum をデファクトとする方向に舵を切ったのでしょう。
しかし Sanctum には、複数ガード対応が煩わしかったり、
リフレッシュトークンの機構を持っていなかったりと、
要件次第では大いに過不足があります。
シンプルなトークン認証であれば、TokenGuard ベースのカスタマイズも、
候補に入れてよいのではないでしょうか。
※もし正式に deprecated と宣言されている情報があれば、教えてください。
Discussion