1
1

More than 1 year has passed since last update.

Laravel PassportでOAuth2を用いた認証機能を作成しよう!③

Last updated at Posted at 2023-08-18

はじめに

本記事は Laravel Passport で OAuth2 を用いた認証機能を作成する手順の一部です。
事前にこちらの記事からご覧ください。

※ 太字の部分が本記事で説明している内容です。

【リソースサーバ 兼 認証サーバ】
1. Laravel Passport のインストール
2. Laravel Passport の初期設定
3. ログイン画面の作成
4. マイページ画面の作成
5. ユーザ情報確認/更新画面の作成
6. ログアウト処理の追加

【クライアントアプリ】
7. トークンのリクエスト処理の追加
8. Laravel Sanctum の初期設定
9. ログインユーザ確認画面の作成
10. ログアウト処理の追加

【リソースサーバ 兼 認証サーバ】
11. Laravel Passport のルート登録
12. トークン取消 API の作成

作成手順

本記事ではクライアントアプリを作成していきます。

7. トークンのリクエスト処理の追加

Migration の作成

users テーブルから不要なカラム( email_verified_at, password, remember_token )を削除するため、 make:migration コマンドを実行してファイルを作成した後、以下の記述を追加します。また、email カラムに関しては一意制約を削除します。

php artisan make:migration drop_columns_from_users_table
YYYY_MM_DD_HHmmss_drop_columns_from_users_table.php
public function up(): void
{
    Schema::table('users', function (Blueprint $table) {
-       //
+       $table->dropColumn('email_verified_at');
+       $table->dropColumn('password');
+       $table->dropColumn('remember_token');
+
+       $table->dropUnique(['email']);
    });
}

public function down(): void
{
    Schema::table('users', function (Blueprint $table) {
-       //
+       $table->timestamp('email_verified_at')->nullable()->after('email');
+       $table->string('password')->after('email_verified_at');
+       $table->rememberToken()->after('password');
+
+       $table->unique(['email']);
    });
}

続いて、access_token を保存するカラムを追加するため、同様に make:migration コマンドを実行してファイルを作成した後、以下の記述を追加します。

php artisan make:migration add_columns_to_users_table
YYYY_MM_DD_HHmmss_add_columns_to_users_table.php
public function up(): void
{
    Schema::table('users', function (Blueprint $table) {
-       //
+       $table->string('access_token', 2048)->after('email');
    });
}

public function down(): void
{
    Schema::table('users', function (Blueprint $table) {
-       //
+       $table->dropColumn('access_token');
    });
}

migrate コマンドを実行して、マイグレーションを実行します。

php artisan migrate

Model の変更

存在しなくなったカラムに関する記述を Model から削除します。また、追加した access_token カラムに関する記述を Model に追加します。

app\Models\User.php
protected $fillable = [
    'name',
    'email',
-   'password',
+   'access_token',
];

protected $hidden = [
-   'password',
-   'remember_token',
+   'access_token',
];

- protected $casts = [
-   'email_verified_at' => 'datetime',
-   'password' => 'hashed',
- ];

Controller の作成

AuthController を作成し、「OAuth サーバへのリダイレクト」と「OAuth サーバからのコールバック」の処理を記述します。

OAuth サーバへのリダイレクト

公式ドキュメントに記載されている処理を基に作成します。
処理はシンプルで、必要なパラメータを生成して OAuth サーバへリダイレクトしています。

app\Http\Controllers\AuthController.php
<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Symfony\Component\HttpKernel\Exception\HttpException;

class AuthController extends Controller
{
    /**
     * OAuth サーバへリダイレクト
     *
     * @param Request $request
     * @return RedirectResponse
     */
    public function redirect(Request $request): RedirectResponse
    {
        $state = Str::random(40);
        $request->session()->put('state', $state);

        $codeVerifier = Str::random(128);
        $request->session()->put('code_verifier', $codeVerifier);

        $codeChallenge = strtr(rtrim(
            base64_encode(hash('sha256', $codeVerifier, true)),
            '='
        ), '+/', '-_');

        $query = http_build_query([
            'client_id' => env('LARAVELPASSPORT_CLIENT_ID'),
            'redirect_uri' => url('/auth/callback'),
            'response_type' => 'code',
            'scope' => '',
            'state' => $state,
            'code_challenge' => $codeChallenge,
            'code_challenge_method' => 'S256',
        ]);

        return redirect(env('LARAVELPASSPORT_HOST') . '/oauth/authorize?' . $query);
    }

OAuth サーバからのコールバック

公式ドキュメントに記載されている処理を基に作成します。処理の内容は以下の通りです。

  1. state が不正な値でないかチェックする。
  2. OAuth サーバに認可コードを送信してアクセストークンを取得する。
  3. 認証済みのユーザ情報を OAuth サーバから取得する。
  4. ログインしたユーザのレコードを users テーブルに新規登録する。
app\Http\Controllers\AuthController.php
    /**
     * OAuth サーバからのコールバック
     *
     * @param Request $request
     * @return RedirectResponse
     */
    public function callback(Request $request): RedirectResponse
    {
        $state = $request->session()->pull('state');
        if (strlen($state) <= 0 || $state !== $request->state) {
            throw new \InvalidArgumentException('Invalid state value.');
        }

        $codeVerifier = $request->session()->pull('code_verifier');
        $accessToken = $this->getToken($codeVerifier, $request->code);

        $response = $this->authorizedRequest($accessToken)
            ->get(env('LARAVELPASSPORT_HOST') . '/api/user');

        $user = User::create([
            'email' => $response['email'],
            'name' => $response['name'],
            'access_token' => $accessToken,
        ]);

        Auth::login($user);

        $request->session()->regenerate();

        return redirect('/');
    }

    /**
     * OAuth サーバからアクセストークンを取得
     *
     * @param string $codeVerifier
     * @param string $code
     * @return string
     */
    private function getToken(string $codeVerifier, string $code): string
    {
        $response = Http::asForm()->post(env('LARAVELPASSPORT_HOST') . '/oauth/token', [
            'grant_type' => 'authorization_code',
            'client_id' => env('LARAVELPASSPORT_CLIENT_ID'),
            'redirect_uri' => url('/auth/callback'),
            'code_verifier' => $codeVerifier,
            'code' => $code,
        ]);

        if ($response->status() !== 200) {
            throw new HttpException($response->status());
        }
        return $response['access_token'];
    }

    /**
     * 認証済みリクエストの送信(ヘッダの付加)
     *
     * @param string $accessToken
     * @return PendingRequest
     */
    private function authorizedRequest(string $accessToken): PendingRequest
    {
        return Http::withHeaders([
            'Accept' => 'application/json',
            'Authorization' => 'Bearer ' . $accessToken,
        ]);
    }
}

Laravel Passport に関する OAuth サーバの環境変数を .env に追記します。

LARAVELPASSPORT_CLIENT_ID=[OAuth2Server Authorization Code Grant の Client ID]
LARAVELPASSPORT_HOST=http://localhost:8080

ルートの登録

追加した Controller にアクセスするルートを登録します。

routes\web.php
- Route::get('/', function () {
-     return view('welcome');
- });
+ Route::get('/auth/redirect', [\App\Http\Controllers\AuthController::class, 'redirect'])->name('login');
+ Route::get('/auth/callback', [\App\Http\Controllers\AuthController::class, 'callback']);

8. Laravel Sanctum の初期設定

クライアントアプリでは Laravel Sanctum を使用して認証管理を行います。
そこで、Sanctum の SPA 認証を有効にするため、EnsureFrontendRequestsAreStateful ミドルウェアを api グループに追加します。

app\Http\Kernel.php
        'api' => [
-           // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
+           \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

また、SPA がリクエストを行うドメインを .env に設定します。

SANCTUM_STATEFUL_DOMAINS=localhost:8000

9. ログインユーザ確認画面の作成

Tailwind CSS / Vue.js / Vue Router のインストール

リソースサーバ 兼 認証サーバにインストールした手順と同じであるため、省略させて頂きます。こちらの記事をご参照ください。

View の作成

ログインユーザ確認画面として表示する Index.vue を新たに作成します。初期表示時にユーザ情報を取得する API を実行しています。また認証有無によって、ログイン/ログアウトボタンの表示を切り替えています。

resources\js\views\Index.vue
<script setup>
import axios from 'axios'

const user = await axios
  .get('/api/user')
  .then((response) => response?.data)
  .catch(() => null)
</script>

<template>
  <div class="flex justify-center">
    <div class="max-w-md w-full rounded overflow-hidden shadow-lg my-6 mx-4 md:mx-auto">
      <div class="font-bold text-xl px-4 py-6">ログインユーザ確認</div>
      <template v-if="user !== null">
        <div class="border-y border-gray-100">
          <dl class="divide-y divide-gray-100">
            <div class="px-4 py-6 sm:grid sm:grid-cols-3">
              <dt class="text-sm font-medium leading-6 text-gray-900">ユーザ名</dt>
              <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
                {{ user.name }}
              </dd>
            </div>
            <div class="px-4 py-6 sm:grid sm:grid-cols-3">
              <dt class="text-sm font-medium leading-6 text-gray-900">メールアドレス</dt>
              <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
                {{ user.email }}
              </dd>
            </div>
          </dl>
        </div>
      </template>

      <div class="flex justify-center my-4">
        <a
          v-if="user !== null"
          href="/logout"
          class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
        >
          ログアウト
        </a>
        <a
          v-else
          href="/auth/redirect"
          class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
        >
          ログイン
        </a>
      </div>
    </div>
  </div>
</template>

ルートの登録

追加した View を表示するルートを登録します。

resources\js\router\index.js
- routes: []
+ routes: [
+   {
+     path: '/',
+     name: 'index',
+     component: () => import('../views/Index.vue')
+   }
+ ]
routes\web.php
Route::get('/auth/redirect', [\App\Http\Controllers\AuthController::class, 'redirect'])->name('login');
Route::get('/auth/callback', [\App\Http\Controllers\AuthController::class, 'callback']);

+ Route::get('/', fn () => view('app'));

10. ログアウト処理の追加

Controller の作成

ログアウト処理を AuthController に追加します。処理の内容は以下の通りです。

  1. OAuth サーバのアクセストークン/リフレッシュトークンを取り消すため、トークン取消 APIへリクエストを送信する。(トークン取消 API は後ほど作成します)
  2. クライアントアプリからログアウトする。
  3. OAuth サーバにおいてもログアウトするため、OAuth サーバの /logout ルートへリダイレクトする。また、OAuth サーバでログアウトした後にクライアントアプリへ戻ってきた際の URL をパラメータとして付加しておく。
app\Http\Controllers\AuthController.php
+   public function logout(Request $request): RedirectResponse
+   {
+       /** @var User ログインユーザ */
+       $user = Auth::user();
+       $user = $user->makeVisible(['access_token']);
+
+       $url = env('LARAVELPASSPORT_HOST') . '/api/token/' . env('LARAVELPASSPORT_CLIENT_ID');
+       $this->authorizedRequest($user->access_token)->delete($url);
+
+       Auth::logout();
+       
+       $request->session()->invalidate();
+       $request->session()->regenerateToken();
+
+       $query = http_build_query(['redirect_uri' => url('/')]);
+       return redirect(env('LARAVELPASSPORT_HOST') . '/logout?' . $query);
+   }

ルートの登録

追加したログアウト処理を実行するルートを登録します。

routes\web.php
+ Route::middleware('auth')->group(function () {
+     Route::get('/logout', [\App\Http\Controllers\AuthController::class, 'logout']);
+ });

おわりに

続きはこちらの記事をご覧ください。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1