サイトアイコン IT & ライフハックブログ|学びと実践のためのアイデア集

【初心者〜実務まで】Laravelの認証・認可を正しく作る――ログイン/登録、Sanctum、Policy/Gate、ロール設計、セキュリティ、アクセシブルなフォームとエラー表示

php elephant sticker

Photo by RealToughCandy.com on Pexels.com

【初心者〜実務まで】Laravelの認証・認可を正しく作る――ログイン/登録、Sanctum、Policy/Gate、ロール設計、セキュリティ、アクセシブルなフォームとエラー表示

  • この記事は「Laravelでログイン機能を作りたいけれど、何が正解か分からない」方向けに、認証(Auth)と認可(Authorization)を一続きの設計として整理します
  • UI(フォーム)とバックエンド(セッション/トークン/権限)を同時に整えるため、実務の落とし穴を避けやすいです
  • アクセシビリティ(読み上げ・キーボード操作・エラー表示)を標準にするサンプルも含めます

想定読者(だれが得をする?)

  • Laravelを始めたばかりの方:Breeze/Jetstream/Sanctumの違いが分からず、まず何から作れば良いか迷っている方
  • 小規模〜中規模のWebアプリ開発者:ログイン/権限周りが場当たり的になり、運用で困り始めている方
  • チーム開発のリード/レビュー担当:認可ルールをコードで統一し、画面の事故(見えてはいけないデータが見える)を減らしたい方
  • デザイナー/QA/アクセシビリティ担当:フォームのエラー表示やログイン導線を、だれでも迷わず使える形に揃えたい方

アクセシビリティレベル:★★★★★

  • ラベルと入力の紐付け、エラーサマリ、aria-invalid/aria-describedby、色に依存しない必須/エラー表現
  • セッション期限(419)や権限不足(403)時の案内を「次の行動」まで含めて設計
  • キーボード操作でログイン〜設定変更まで完遂できる前提で構成

1. はじめに:認証と認可は「別物」ですが、同時に設計すると強くなります

Laravelでアプリを作り始めると、まず欲しくなるのがログイン機能です。ただ、ログインができるようになった次に必ず出てくるのが「この人は、この操作をして良いの?」という問題です。ここで認可の設計が後回しになってしまうと、画面やAPIが増えるほど、確認漏れが増えます。結果として、権限事故(見えてはいけない情報が見える、編集できてしまう)が起きやすくなります。

そこで、最初から次の2つをセットで考えます。

  • 認証:あなたは誰ですか(ログイン/トークンで本人確認)
  • 認可:あなたは何をして良いですか(権限チェック、所有者チェック、ロール)

この記事は、初心者の方が「とりあえず動いた」から一歩進んで、実務で安全に運用できる認証・認可を組み立てられるようになることを目標にしています。


2. まず選ぶ:Breeze / Jetstream / Fortify / 自前の違い

Laravelには認証まわりの導入方法がいくつかあります。結論としては、最初の一歩はBreezeが扱いやすいことが多いです。

  • Laravel Breeze
    • 最小構成で、ログイン/登録/パスワードリセットなど一式が揃う
    • Blade版が分かりやすく、初学者向き
  • Laravel Jetstream
    • チーム機能、2FAなどを含む高機能セット
    • 最初から全部欲しい場合は便利ですが、理解コストは上がりやすいです
  • Laravel Fortify
    • UIを持たない認証バックエンド(SPAや独自UI向け)
  • 自前実装
    • 学習には良いのですが、実務では「守るべきポイント(CSRF、セッション固定、レート制限等)」が抜けやすいので注意が必要です

最短で安全に進めたい場合は、Breeze(Blade)で土台を作り、必要に応じてAPI認証はSanctumで拡張する流れが分かりやすいです。


3. 認証の基本:セッションログインの流れ(Webアプリ)

3.1 ログインが成立するまで

一般的なWebアプリ(ブラウザ)では、次の流れが基本です。

  1. ログインフォームでメールとパスワードを送信
  2. サーバが本人確認(パスワードハッシュ照合)
  3. 成功したらセッションを作り、以後はセッションIDでログイン状態を維持
  4. CSRF対策とセットで「フォーム送信の安全性」を守る

Laravelはこの仕組みを標準で備えているため、変に独自化しない方が安全です。

3.2 重要な設定(.env)

  • APP_KEY は必ず生成されていること
  • 本番は APP_DEBUG=false
  • セッションストア(Redis等)を選ぶ場合は運用も含めて検討

4. パスワード設計:強さ、リセット、通知の標準化

パスワードはセキュリティの中心ですが、強すぎるとサポートが増え、弱すぎると危険です。現場では「最低限の強さ+リセット導線を丁寧に」がバランスが良いです。

4.1 バリデーション例(FormRequest)

// app/Http/Requests/RegisterRequest.php
class RegisterRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => ['required','string','max:50'],
            'email' => ['required','email','max:255','unique:users,email'],
            'password' => [
                'required','string','min:12','confirmed',
            ],
        ];
    }

    public function attributes(): array
    {
        return [
            'name' => 'お名前',
            'email' => 'メールアドレス',
            'password' => 'パスワード',
        ];
    }
}

4.2 パスワードリセットは「事故が起きにくい文言」が大切です

  • メールアドレスが存在しない場合でも「送信しました」と表示する(存在確認を漏らさない)
  • 有効期限を明示し、期限切れ時は再発行導線へ誘導する
  • メール本文はHTMLだけでなくプレーンテキストも用意する(読み上げや環境差に強い)

5. 認可の基本:PolicyとGateで「できる/できない」を統一する

認可は「if文で画面に散らす」と破綻しやすいです。Laravelでは、次の2つを軸にすると整理しやすくなります。

  • Policy:モデル(例:Post/Project/Order)に対する権限をまとめる
  • Gate:モデルがない判断(例:管理画面に入れるか、機能フラグ)をまとめる

5.1 Policyを作る

例として、プロジェクトを編集できるかどうかをPolicyで定義します。

php artisan make:policy ProjectPolicy --model=Project
// app/Policies/ProjectPolicy.php
class ProjectPolicy
{
    public function view(User $user, Project $project): bool
    {
        return $user->tenant_id === $project->tenant_id;
    }

    public function update(User $user, Project $project): bool
    {
        if ($user->tenant_id !== $project->tenant_id) return false;
        return $user->role === 'admin' || $user->role === 'owner';
    }

    public function delete(User $user, Project $project): bool
    {
        return $user->tenant_id === $project->tenant_id
            && $user->role === 'owner';
    }
}

5.2 コントローラで使う(統一された入口)

public function edit(Project $project)
{
    $this->authorize('update', $project);
    return view('projects.edit', compact('project'));
}

この形にしておくと、画面の見た目がどう変わっても、権限の判定が一箇所に集約されます。レビューも「Policyだけ見れば良い」状態になり、漏れが減ります。


6. ロール設計:最初は少なく、でも“意味”は明確に

ロールを増やしすぎると管理が難しくなります。最初は次のような4段階が分かりやすいです。

  • owner:契約、課金、削除など最重要
  • admin:設定やメンバー管理
  • member:通常操作
  • viewer:閲覧のみ

重要なのは「ロール名」と「できること」をドキュメント化し、コード(Policy)で守ることです。画面にボタンを表示しないだけでは安全ではありません。必ずサーバ側で authorize を通してください。


7. API認証:Sanctumで「SPA」「外部クライアント」を無理なく扱う

Web(セッション)とは別に、APIを公開したいことがあります。LaravelではSanctumが扱いやすいです。

7.1 2つの使い方

  • SPA認証(同一ドメインのSPA)
    • セッション+CSRFを使いつつ、APIを叩くスタイル
  • パーソナルアクセストークン(外部クライアント)
    • トークンを発行し、Authorization: Bearer で認証

7.2 トークン発行例(外部クライアント)

$token = $user->createToken('cli', ['orders:read'])->plainTextToken;
return response()->json(['token' => $token]);

7.3 ルート保護

Route::middleware('auth:sanctum')->get('/api/v1/orders', function () {
    return OrderResource::collection(Order::latest()->paginate());
});

スコープ(abilities)を付けておくと、トークン漏洩時の被害を最小化できます。


8. レート制限:ログイン・リセット・招待は必ず守る

ログインやパスワードリセットは攻撃対象になりやすいので、レート制限を必ず検討します。Laravelは throttle で対応できます。

Route::post('/login', [AuthController::class, 'store'])->middleware('throttle:10,1');
  • 1分に10回まで、のように現実的な値を決めます
  • 429のときは「しばらく待ってからお試しください」と案内します
  • 失敗時の文言は、情報を与えすぎない(メールが存在するか等)ようにします

9. アクセシブルなログインフォーム:ラベル、エラー、フォーカスの“標準形”

認証画面は多くの人が通るので、アクセシビリティの効果が出やすい場所です。ここでは「最小で効く型」を示します。

9.1 エラーサマリ(サーババリデーション)

@if ($errors->any())
  <div id="error-summary" role="alert" tabindex="-1" class="border p-3 mb-4">
    <h2 class="font-semibold">入力内容を確認してください。</h2>
    <ul class="list-disc pl-5">
      @foreach ($errors->all() as $error)
        <li>{{ $error }}</li>
      @endforeach
    </ul>
  </div>

  <script>
    (function(){
      const el = document.getElementById('error-summary');
      if (el) el.focus();
    })();
  </script>
@endif
  • 送信後にサマリへフォーカスが移ると、読み上げでも状況が把握しやすいです
  • 見た目の赤色だけに頼らず、テキストで説明します

9.2 入力とエラーの紐付け

@php $emailError = $errors->first('email'); @endphp

<label for="email" class="block font-medium">
  メールアドレス <span class="sr-only">必須</span><span aria-hidden="true">(必須)</span>
</label>
<input id="email" name="email" type="email" value="{{ old('email') }}"
  autocomplete="email"
  aria-invalid="{{ $emailError ? 'true' : 'false' }}"
  aria-describedby="{{ $emailError ? 'email-error' : 'email-help' }}"
  class="border rounded px-3 py-2 w-full">

<p id="email-help" class="text-sm text-gray-600">例:hanako@example.com</p>
@if($emailError)
  <p id="email-error" class="text-sm text-red-700">{{ $emailError }}</p>
@endif

同様にパスワードも labelaria-describedby を揃えます。これを一度テンプレ化(Bladeコンポーネント化)すると、全フォームが安定します。


10. 403/419/503の“困る画面”を丁寧にする

認証・認可でユーザーが混乱しやすいのが、権限不足やセッション期限です。ここを丁寧にすると問い合わせが減り、信頼が上がります。

  • 403(権限がない)
    • 何ができないかを短く説明し、「戻る」「権限を依頼する」など次の手段を提示
  • 419(セッション期限/CSRF)
    • 「長時間操作がなかったため、もう一度お試しください」と案内し、再ログイン導線を出す
  • 503(メンテ/一時停止)
    • 復旧目安、影響範囲、代替手段(サポート)を明示

アクセシビリティ的には、見出しを明確にし、リンク文言を具体的にします。「こちら」ではなく「ログイン画面へ戻る」などが親切です。


11. 認可を“画面表示”にも反映する(ただし最終防衛はPolicy)

「編集ボタンを表示するか」は、ユーザー体験として大切です。ですが、表示制御だけでは安全になりません。

  • 表示:@can で体験を整える
  • 実行:authorize で必ず拒否する
    この二段構えが安定します。
@can('update', $project)
  <a href="{{ route('projects.edit', $project) }}">編集</a>
@endcan

12. テスト:権限事故をCIで止める

認可は事故の影響が大きいので、Featureテストで守る価値が高いです。

12.1 別ユーザーが編集できない

public function test_user_cannot_update_other_users_project()
{
    $t1 = Tenant::factory()->create();
    $t2 = Tenant::factory()->create();

    $user = User::factory()->create(['tenant_id' => $t1->id, 'role' => 'admin']);
    $otherProject = Project::factory()->create(['tenant_id' => $t2->id]);

    $this->actingAs($user);
    app()->instance('tenant', $t1);

    $res = $this->patch("/projects/{$otherProject->id}", ['name' => 'x']);
    $res->assertForbidden();
}

12.2 ownerだけ削除できる

public function test_only_owner_can_delete()
{
    $t = Tenant::factory()->create();
    $owner = User::factory()->create(['tenant_id'=>$t->id, 'role'=>'owner']);
    $admin = User::factory()->create(['tenant_id'=>$t->id, 'role'=>'admin']);
    $p = Project::factory()->create(['tenant_id'=>$t->id]);

    $this->actingAs($admin);
    app()->instance('tenant', $t);
    $this->delete("/projects/{$p->id}")->assertForbidden();

    $this->actingAs($owner);
    $this->delete("/projects/{$p->id}")->assertRedirect();
}

この2本があるだけでも、権限の基本事故をかなり防げます。


13. よくある落とし穴と回避策

  • 認可が画面ごとにバラバラ
    • Policyに集約し、コントローラで authorize を標準にする
  • ボタン非表示で安心してしまう
    • 最終防衛はサーバ側。必ず authorize
  • ロールを増やしすぎる
    • 最初は4段階程度に絞り、必要になってから拡張する
  • パスワードリセット文言でユーザー存在を漏らす
    • 成否に関わらず同じ案内に寄せる
  • 419/403の説明が不親切
    • 次の行動(ログイン、戻る、権限依頼)を必ず用意する
  • エラーが色だけ
    • サマリとテキストのエラー文、aria-* で紐付ける

14. まとめ:認証・認可は“仕組み”にすると、アプリが育っても壊れにくいです

Laravelの認証は、BreezeやSanctumを使うことで、比較的短時間で安全な土台を作れます。そこにPolicy/Gateを重ねて「何ができるか」をコードで統一すると、画面が増えても権限事故が起きにくくなります。さらに、ログインフォームやエラー表示をアクセシブルに整えることで、困る人を減らし、問い合わせも減り、結果として開発と運用の両方が楽になります。

最初の段階で、

  • 認証(ログイン/リセット)
  • 認可(Policyに集約、authorize徹底)
  • レート制限(ログイン導線の保護)
  • アクセシブルなフォーム(ラベル/エラー/フォーカス)
    この4点を標準にしておくと、チームが安心して機能追加できる土台になります。

参考リンク

モバイルバージョンを終了