はじめに
本記事は 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
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
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 に追加します。
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 サーバへリダイレクトしています。
<?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 サーバからのコールバック
公式ドキュメントに記載されている処理を基に作成します。処理の内容は以下の通りです。
-
state
が不正な値でないかチェックする。 - OAuth サーバに認可コードを送信してアクセストークンを取得する。
- 認証済みのユーザ情報を OAuth サーバから取得する。
- ログインしたユーザのレコードを
users
テーブルに新規登録する。
/**
* 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 にアクセスするルートを登録します。
- 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
グループに追加します。
'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 を実行しています。また認証有無によって、ログイン/ログアウトボタンの表示を切り替えています。
<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 を表示するルートを登録します。
- routes: []
+ routes: [
+ {
+ path: '/',
+ name: 'index',
+ component: () => import('../views/Index.vue')
+ }
+ ]
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
に追加します。処理の内容は以下の通りです。
- OAuth サーバのアクセストークン/リフレッシュトークンを取り消すため、トークン取消 APIへリクエストを送信する。(トークン取消 API は後ほど作成します)
- クライアントアプリからログアウトする。
- OAuth サーバにおいてもログアウトするため、OAuth サーバの
/logout
ルートへリダイレクトする。また、OAuth サーバでログアウトした後にクライアントアプリへ戻ってきた際の URL をパラメータとして付加しておく。
+ 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);
+ }
ルートの登録
追加したログアウト処理を実行するルートを登録します。
+ Route::middleware('auth')->group(function () {
+ Route::get('/logout', [\App\Http\Controllers\AuthController::class, 'logout']);
+ });
おわりに
続きはこちらの記事をご覧ください。