認証#
セッションベースの認証で、UserProvider とパスワードハッシュは差し替え可能です。
1. UserProvider を実装#
<?php
use Polidog\Relayer\Auth\{Credentials, Identity, UserProvider};
final class PdoUserProvider implements UserProvider
{
public function __construct(private readonly \PDO $pdo) {}
public function findByIdentifier(string $identifier): ?Credentials
{
$stmt = $this->pdo->prepare(
'SELECT id, name, password_hash, roles FROM users WHERE email = ?'
);
$stmt->execute([\strtolower(\trim($identifier))]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if (false === $row) {
return null;
}
return new Credentials(
identity: new Identity(
id: (int) $row['id'],
displayName: (string) $row['name'],
roles: \json_decode((string) $row['roles'], true) ?: [],
),
passwordHash: (string) $row['password_hash'],
);
}
}2. プロバイダをバインド#
services:
App\Auth\PdoUserProvider: ~
Polidog\Relayer\Auth\UserProvider:
alias: App\Auth\PdoUserProviderUserProvider を登録すると Authenticator が自動で DI コンテナに登録され、型で受け取れるようになります。
3. ログイン#
<?php
use Polidog\Relayer\Auth\Authenticator;
use Polidog\Relayer\Router\Component\PageContext;
return function (PageContext $ctx, Authenticator $auth): Closure {
$error = null;
$login = $ctx->action('login', function (array $form) use ($auth, $ctx, &$error): void {
$identity = $auth->attempt(
(string) ($form['email'] ?? ''),
(string) ($form['password'] ?? ''),
);
if (null === $identity) {
$error = 'メールアドレスまたはパスワードが違います。';
return;
}
$ctx->redirect('/dashboard');
});
return fn () => (<form action={$login}>{/* ... */}</form>);
};4. ページを保護#
クラススタイルは属性で:
#[Auth]
final class DashboardPage extends PageComponent {}
#[Auth(roles: ['admin'])]
final class AdminPage extends PageComponent {}関数スタイルは PageContext で:
return function (PageContext $ctx): Closure {
$user = $ctx->requireAuth(); // 未認証なら例外→リダイレクト/401
// $ctx->requireAuth(['admin']) でロール必須
return fn () => <h1>ようこそ {$user->displayName}</h1>;
};条件付き表示には $ctx->user()(未ログインなら null)を使います。非 null の Identity を引数に取るページは「認証必須」を意味します。
5. トークン認証(Firebase / Cognito)#
クライアント SDK(Firebase JS SDK、AWS Amplify、Cognito Hosted UI)が署名済み ID トークンを発行する構成向けです。OAuth のリダイレクト/コード交換はフレームワークに持たず、クライアントが Authorization: Bearer <jwt> で送ったトークンを IdP の JWKS で署名検証し、登録クレーム(iss・aud・exp/nbf/iat、Cognito は token_use)を検証します。
TokenVerifier をバインド#
TokenVerifier は UserProvider のトークン版です。Firebase / Cognito ファクトリで生成し、設定だけで完結します(サービスと DI の factory 構文、JWKS 取得は HTTP クライアント 経由)。
# config/services.yaml
services:
_defaults: { autowire: true, autoconfigure: true, public: true }
# Firebase
Polidog\Relayer\Auth\Token\TokenVerifier:
factory: ['Polidog\Relayer\Auth\Token\Firebase', 'verifier']
arguments:
$http: '@Polidog\Relayer\Http\Client\HttpClient'
$projectId: '%env(FIREBASE_PROJECT_ID)%'
$cacheDir: '%app.project_root%/var/cache/jwks'
# …または Cognito
# Polidog\Relayer\Auth\Token\TokenVerifier:
# factory: ['Polidog\Relayer\Auth\Token\Cognito', 'verifier']
# arguments:
# $http: '@Polidog\Relayer\Http\Client\HttpClient'
# $region: '%env(COGNITO_REGION)%'
# $userPoolId: '%env(COGNITO_USER_POOL_ID)%'
# $appClientId: '%env(COGNITO_APP_CLIENT_ID)%'
# $cacheDir: '%app.project_root%/var/cache/jwks'| 環境変数 | 用途 | 例 |
|---|---|---|
FIREBASE_PROJECT_ID | Firebase | my-app |
COGNITO_REGION | Cognito | ap-northeast-1 |
COGNITO_USER_POOL_ID | Cognito | ap-northeast-1_AbCdEf |
COGNITO_APP_CLIENT_ID | Cognito | 7f3k…(app client id) |
JWKS は URL ごとにディスクへキャッシュし、応答の Cache-Control: max-age を尊重します。鍵ローテーションは自動で、キャッシュに無い kid のトークンが来ると 1 回だけ(レート制限付きで)リフレッシュします(偽 kid の連打を JWKS フェッチ洪水に増幅しない)。JWKS エンドポイント到達不能は運用障害として表に出し、全ユーザーを黙ってログアウト扱いにはしません。
モード A — ステートレス API(リクエスト毎の Bearer)#
TokenVerifier だけをバインド(UserProvider なし)すると、 AuthenticatorInterface はステートレスな TokenAuthenticator に解決され、セクション 4 と同じ #[Auth] / requireAuth() がそのまま機能します。毎リクエストで Bearer ヘッダから principal を再導出し、何も永続化しません。
#[Auth(redirectTo: '')] // トークン無効/不在は 401(リダイレクトなし)
final class ApiEndpoint extends PageComponent { /* ... */ }
#[Auth(roles: ['admin'])] // ロールはトークンのクレームから
final class AdminApi extends PageComponent { /* ... */ }モード B — セッションログイン(1 回検証して Cookie セッション)#
ログインルートでトークンを検証し、得た Identity を Authenticator::login() に渡します。セッション Authenticator は UserProvider を必要としなくなったので、ローカルにパスワードを持たない Firebase/Cognito アプリでも初回以降は通常の Cookie セッションが使えます(API ルート の route.php)。
<?php
// src/Pages/auth/token/route.php — POST を Authorization: Bearer <jwt> 付きで
declare(strict_types=1);
use Polidog\Relayer\Auth\Authenticator;
use Polidog\Relayer\Auth\Token\BearerToken;
use Polidog\Relayer\Auth\Token\TokenVerifier;
use Polidog\Relayer\Auth\Token\AuthorizationHeader;
use Polidog\Relayer\Http\Response;
return [
'POST' => static function (
TokenVerifier $verifier,
Authenticator $auth,
AuthorizationHeader $header,
): Response {
$identity = $verifier->verify(
BearerToken::parse($header->value()) ?? '',
);
if (null === $identity) {
return Response::json(['error' => 'invalid token'], 401);
}
$auth->login($identity); // セッション ID を再生成
return Response::json(['user' => $identity->toArray()]);
},
];優先順位(ルールは 1 つ・ハイブリッド層なし)#
| バインド済みサービス | AuthenticatorInterface は… | 備考 |
|---|---|---|
UserProvider | セッション Authenticator | パスワードアプリ(従来動作)。 |
TokenVerifier のみ | TokenAuthenticator | トークン優先 API。#[Auth] が Bearer を強制。 |
| 両方 | セッション Authenticator | セッション優先。TokenAuthenticator は特定 API ルートで型指定により注入可。 |
注意#
- Bearer のみ。 トークンは
Authorization: Bearer …から読みます。
Apache/CGI では再注入ルールが無いと Authorization ヘッダが落ちることがあります(REDIRECT_HTTP_AUTHORIZATION フォールバックは読みますが、その rewrite ルール自体は当該ホストに必要)。
- クレームマッピングは差し替え可能。 両ファクトリは任意の
identityMapper(fn(\stdClass $claims): ?Identity)を受け取ります。デフォルトは sub を id、表示名は name → email → sub (Cognito は cognito:username も)、ロールは cognito:groups (Cognito)/カスタム roles 配列(Firebase)から読みます。
TokenAuthenticatorのattempt()/login()/logout()は
LogicException。ステートレス Bearer にはパスワードハンドシェイクもセッションも無く、黙って no-op にせず明示的に失敗させます。
変更履歴 (3)
- 「自動配線」を「自動で DI コンテナに登録」に。直訳の配線(wiring)が不明瞭なため明確化
- バージョン差分表記(vX.Y.Z 追加/破壊的変更/依存バージョン注記)を削除し現在形に整理
- 新規作成