RRelayer
ホーム/機能

認証#

セッションベースの認証で、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\PdoUserProvider

UserProvider を登録すると 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 で署名検証し、登録クレーム(issaudexp/nbf/iat、Cognito は token_use)を検証します。

TokenVerifier をバインド#

TokenVerifierUserProvider のトークン版です。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_IDFirebasemy-app
COGNITO_REGIONCognitoap-northeast-1
COGNITO_USER_POOL_IDCognitoap-northeast-1_AbCdEf
COGNITO_APP_CLIENT_IDCognito7f3k…(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 { /* ... */ }

ログインルートでトークンを検証し、得た IdentityAuthenticator::login() に渡します。セッション AuthenticatorUserProvider を必要としなくなったので、ローカルにパスワードを持たない 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 ルール自体は当該ホストに必要)。

  • クレームマッピングは差し替え可能。 両ファクトリは任意の

identityMapperfn(\stdClass $claims): ?Identity)を受け取ります。デフォルトは sub を id、表示名は name → email → sub (Cognito は cognito:username も)、ロールは cognito:groups (Cognito)/カスタム roles 配列(Firebase)から読みます。

  • TokenAuthenticatorattempt() / login() / logout()

LogicException。ステートレス Bearer にはパスワードハンドシェイクもセッションも無く、黙って no-op にせず明示的に失敗させます。

最終更新: 2026-05-19
変更履歴 (3)
  • 「自動配線」を「自動で DI コンテナに登録」に。直訳の配線(wiring)が不明瞭なため明確化
  • バージョン差分表記(vX.Y.Z 追加/破壊的変更/依存バージョン注記)を削除し現在形に整理
  • 新規作成