はじめに
本記事は Laravel Passport で OAuth2 を用いた認証機能を作成する手順の一部です。
事前にこちらの記事からご覧ください。
※ 太字の部分が本記事で説明している内容です。
【リソースサーバ 兼 認証サーバ】
1. Laravel Passport のインストール
2. Laravel Passport の初期設定
3. ログイン画面の作成
4. マイページ画面の作成
5. ユーザ情報確認/更新画面の作成
6. ログアウト処理の追加
【クライアントアプリ】
7. トークンのリクエスト処理の追加
8. Laravel Sanctum の初期設定
9. ログインユーザ情報表示画面の作成
10. ログアウト処理の追加
【リソースサーバ 兼 認証サーバ】
11. Laravel Passport のルート登録
12. トークン取消 API の作成
作成手順
1. Laravel Passport のインストール
Laravel Passport を用いるため、不要となる Laravel Sanctum をアンインストールします。関連するファイルから Sanctum に関する記述を削除します。また、config\sanctum.php
はファイルごと削除します。
"require": {
"php": "^8.1",
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10.10",
- "laravel/sanctum": "^3.2",
"laravel/tinker": "^2.8"
},
- 'paths' => ['api/*', 'sanctum/csrf-cookie'],
+ 'paths' => ['api/*'],
- use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
- use HasApiTokens, HasFactory, Notifiable;
+ use HasFactory, Notifiable;
Sanctum に関する記述を削除したら、composer.json
に "laravel/passport": "^11.8"
の記述を追加します。(2023年8月時点の最新バージョンを指定)
"require": {
"php": "^8.1",
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10.10",
+ "laravel/passport": "^11.8",
"laravel/tinker": "^2.8"
},
以下のコマンドを実行してパッケージを更新すると、Sanctum が削除されて Passport がインストールされます。
composer update
2. Laravel Passport の初期設定
Laravel Passport の公式ドキュメント に記載されている内容を基に設定を行います。
データベースの準備
データベースのテーブルを作成するため、migrate
コマンドを実行します。
php artisan migrate
クライアントの作成
Passport の設定ファイルなどを作成するため、passport:install
コマンドを実行します。実行すると Personal access client と Password grant client が作成されますが、今回は使用しませんので ID や Secret を保存しておく必要はありません。また、Laravel Passport のデフォルトではクライアントに連番の ID が割り振られますが、今回は ID として UUID を用いる方式を採用するため、--uuids
オプションを指定してコマンドを実行します。
php artisan passport:install --uuids
途中でマイグレーションを再実行してもよいかと尋ねられたら yes
と入力します。
In order to finish configuring client UUIDs, we need to rebuild the Passport database tables. Would you like to rollback and re-run your last migration? (yes/no) [no]:
> yes
次に、実際に使用する Authorization Code Grant のクライアントを作成します。今回のクライアントアプリはパブリックな環境ではありませんが、認可コード横取り攻撃のリスクを低減するため、PKCE を用いた Authorization Code Grant で認証を行いたいと思います。そこで、クライアントを作成するコマンド passport:client
に --public
オプションを指定して実行します。Client ID は後ほど使用するため、保存しておいてください。
php artisan passport:client --public
Which user ID should the client be assigned to? (Optional):
> [入力なしで Enterキー押下]
What should we name the client?:
> OAuth2Server Authorization Code Grant
Where should we redirect the request after authorization? [http://localhost/auth/callback]:
> http://localhost:8000/auth/callback
New client created successfully.
Client ID: ********-****-****-****-************
Client secret:
認証に関する設定
認証で用いる User モデルに HasApiTokens
トレイトを追記します。
+ use Laravel\Passport\HasApiTokens;
class User extends Authenticatable
{
- use HasFactory, Notifiable;
+ use HasApiTokens, HasFactory, Notifiable;
Passport の認証で用いる api
ガードを作成するため、auth.php
に以下の内容を追記します。
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
+ 'api' => [
+ 'driver' => 'passport',
+ 'provider' => 'users',
+ ],
],
api.php
で用いる Guard を sanctum
から api
に変更します。
- Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
+ Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});
追加されたルートの整理
Laravel Passport ではデフォルトで以下のルートが追加されます。
GET|HEAD oauth/authorize .................. passport.authorizations.authorize › Laravel\Passport › AuthorizationController@authorize
POST oauth/authorize ............... passport.authorizations.approve › Laravel\Passport › ApproveAuthorizationController@approve
DELETE oauth/authorize ........................ passport.authorizations.deny › Laravel\Passport › DenyAuthorizationController@deny
GET|HEAD oauth/clients ........................................ passport.clients.index › Laravel\Passport › ClientController@forUser
POST oauth/clients .......................................... passport.clients.store › Laravel\Passport › ClientController@store
PUT oauth/clients/{client_id} ............................ passport.clients.update › Laravel\Passport › ClientController@update
DELETE oauth/clients/{client_id} .......................... passport.clients.destroy › Laravel\Passport › ClientController@destroy
GET|HEAD oauth/personal-access-tokens .... passport.personal.tokens.index › Laravel\Passport › PersonalAccessTokenController@forUser
POST oauth/personal-access-tokens ...... passport.personal.tokens.store › Laravel\Passport › PersonalAccessTokenController@store
DELETE oauth/personal-access-tokens/{token_id} passport.personal.tokens.destroy › Laravel\Passport › PersonalAccessTokenControlle…
GET|HEAD oauth/scopes ............................................... passport.scopes.index › Laravel\Passport › ScopeController@all
POST oauth/token .......................................... passport.token › Laravel\Passport › AccessTokenController@issueToken
POST oauth/token/refresh .......................... passport.token.refresh › Laravel\Passport › TransientTokenController@refresh
GET|HEAD oauth/tokens ........................... passport.tokens.index › Laravel\Passport › AuthorizedAccessTokenController@forUser
DELETE oauth/tokens/{token_id} .............. passport.tokens.destroy › Laravel\Passport › AuthorizedAccessTokenController@destroy
各ルートがどのような用途で使用されるかを把握し、正しく管理できるなら問題ありませんが、なんとなくで残しておくのは悪用される恐れがあるため、避けた方が良いと考えます。
そこで今回は必要なルートのみを登録するため、AppServiceProvider
にルートを無視する設定を追記します。後ほど、必要に応じてルートを追加したいと思います。
public function register(): void
{
+ Passport::ignoreRoutes();
}
認可画面をスキップする設定
今回は認証の用途で使用するため、認可の画面をスキップする(表示させない)設定を行います。設定を行うためには Client クラスをオーバーライドする必要があるので、新たに PassportClient
クラスを作成します。
<?php
namespace App\Clients;
use Laravel\Passport\Client;
class PassportClient extends Client
{
public function skipsAuthorization(): bool
{
return true;
}
}
作成した PassportClient
クラスを Laravel Passport で用いるため、AuthServiceProvider
に以下の記述を追加します。
public function boot(): void
{
+ Passport::useClientModel(PassportClient::class);
}
有効期限の設定
トークンの有効期限がデフォルトでは1年後となっており非常に長いため、今回は短めの1日後に設定をします。また、リフレッシュトークンは使用しない想定のため、リフレッシュトークンにも同じ有効期限を設定します。
public function boot(): void
{
Passport::useClientModel(PassportClient::class);
+ Passport::tokensExpireIn(now()->addDay());
+ Passport::refreshTokensExpireIn(now()->addDay());
}
3. ログイン画面の作成
Migration の作成
users
テーブルに最終ログイン日時を保存したいため、カラムを追加します。make:migration
コマンドを実行してファイルを作成した後、以下の記述を追加します。
php artisan make:migration add_columns_to_users_table
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
- //
+ $table->timestamp('last_login_at')->nullable()->after('remember_token');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
- //
+ $table->dropColumn('last_login_at');
});
}
migrate
コマンドを実行して、users
テーブルに last_login_at
カラムを追加します。
php artisan migrate
Model の変更
追加した last_login_at
カラムに関する記述を Model に追加します。
protected $fillable = [
'name',
'email',
'password',
+ 'last_login_at',
];
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
+ 'last_login_at' => 'datetime:Y-m-d H:i:s',
];
Controller の作成
ログイン処理を実行する AuthController
を作成します。ログイン処理の内容は以下の通りです。
-
email
とpassword
の入力値をチェックする。 -
email
とpassword
を用いて認証を試みる。
2-1. 認証成功であれば、最終ログイン日時を更新してリダイレクトする。
2-2. 認証失敗であれば、エラーメッセージを返却する。
<?php
namespace App\Http\Controllers;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class AuthController extends Controller
{
/**
* ログイン
*
* @param Request $request
* @return RedirectResponse
*/
public function login(Request $request): RedirectResponse
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (Auth::attempt($credentials)) {
/** @var User ログインユーザ */
$user = Auth::user();
$user->update(['last_login_at' => Carbon::now()]);
$request->session()->regenerate();
return redirect()->intended(RouteServiceProvider::HOME);
}
return back()
->withErrors(['email' => '認証に失敗しました。'])
->onlyInput('email');
}
}
Tailwind CSS のインストール
フロントエンドの見た目を良くするため、Tailwind CSS をインストールします。以下のコマンドを実行して、インストールおよび設定ファイルの作成を行います。
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
対象のファイルにスタイルを適用するため、tailwind.config.js
を変更します。
export default {
- content: [],
+ content: ['./resources/**/*.{js,blade.php}'],
app.css
ファイルに Tailwind CSS の記述を追加します。
@tailwind base;
@tailwind components;
@tailwind utilities;
View の作成
ログイン画面として表示する login.blade.php
を新たに作成します。
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ config('app.name') }}</title>
@vite(['resources/css/app.css'])
</head>
<body>
<div class="flex flex-col p-8">
<h2 class="mt-4 text-center text-3xl font-bold">ログイン</h2>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form action="/login" method="POST">
@csrf
<div class="mt-4">
<label for="email" class="text-sm font-medium">メールアドレス</label>
<div class="mt-2">
<input
id="email"
name="email"
type="email"
value="{{ old('email') }}"
autocomplete="email"
required
placeholder="test@example.com"
class="w-full rounded-md border-0 p-2 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 sm:leading-6"
/>
@error('email')
<div
class="bg-red-100 border border-red-400 text-red-700 px-2 py-1 mt-1 text-sm rounded relative"
role="alert"
>
{{ $message }}
</div>
@enderror
</div>
</div>
<div class="mt-6">
<label for="password" class="text-sm font-medium"> パスワード </label>
<div class="mt-2">
<input
id="password"
name="password"
type="password"
autocomplete="current-password"
required
class="w-full rounded-md border-0 p-2 ring-1 ring-inset ring-gray-300 sm:leading-6"
/>
@error('password')
<div
class="bg-red-100 border border-red-400 text-red-700 px-2 py-1 mt-1 text-sm rounded relative"
role="alert"
>
{{ $message }}
</div>
@enderror
</div>
</div>
<div class="flex justify-center mt-10">
<button
type="submit"
class="flex w-full sm:w-1/2 justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 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"
>
ログインする
</button>
</div>
</form>
</div>
</div>
</body>
</html>
ルートの登録
作成した Controller と View にアクセスするルートをそれぞれ登録します。
- Route::get('/', function () {
- return view('welcome');
- });
+ Route::get('/login', fn () => view('login'))->name('login');
+ Route::post('/login', [App\Http\Controllers\AuthController::class, 'login']);
4. マイページ画面の作成
マイページはログイン直後に表示する画面です。
したがって、認証済みの場合のみ表示可能とするように作成していきます。
Vue.js のインストール
Vite 環境で Vue を動作させるため、@vitejs/plugin-vue
をインストールします。
npm install @vitejs/plugin-vue --save-dev
vite.config.js
に Vue の設定を追記します。併せて、Base URL も変更しておきます。
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
+ import vue from '@vitejs/plugin-vue';
export default defineConfig({
+ base: '/',
plugins: [
+ vue(),
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
],
});
共通のテンプレートとなる resources\views\app.blade.php
を新規作成します。
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ config('app.name') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
<div id="app"></div>
</body>
</html>
Vue ファイルにおいても、共通で使用する resources\js\App.vue
を新規作成します。後ほど setup()
内に非同期処理を記述するため、<Suspense>
を用いてレンダリングを制御します。
<script setup>
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView v-slot="{ Component }">
<Suspense>
<div>
<component :is="Component" />
</div>
</Suspense>
</RouterView>
</template>
resources\js\app.js
に createApp()
の記述を追加して、Vueアプリケーションのインスタンスを作成します。
import './bootstrap'
+ import App from './App.vue'
+ import { createApp } from 'vue'
+ const app = createApp(App)
+ app.mount('#app')
Vue ファイルに Tailwind CSS を適用するため、tailwind.config.js
を変更します。
export default {
- content: ['./resources/**/*.{js,blade.php}'],
+ content: ['./resources/**/*.{vue,js,blade.php}'],
Vue Router のインストール
フロントエンドのルートを担う役割として Vue Router を用いるため、vue-router
をインストールします。
npm install vue-router@4
Vue Router のインスタンスを作成する index.js
を新規作成します。
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: []
})
export default router
作成したインスタンスを Vue に読み込ませるため、app.js
に以下の記述を追加します。
import './bootstrap'
import App from './App.vue'
import { createApp } from 'vue'
+ import router from './router'
const app = createApp(App)
+ app.use(router)
app.mount('#app')
JavaScript から認証済みリクエストを送る設定
Laravel Passport を使用した状態でフロントエンド(JavaScript)から認証済みリクエストを送るためには、CreateFreshApiToken
ミドルウェアの追加が必要です。このミドルウェアを追加すると、暗号化された JWT が含まれた Cookie がサーバから送られるため、以降はその Cookie を用いて認証の有無を判定してくれます。
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
+ \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],
また、Cookie の名前がデフォルトで "laravel_token" となるため、名前の変更を行います。AuthServiceProvider
に以下の記述を追加します。(名前は任意で設定)
public function boot(): void
{
Passport::useClientModel(PassportClient::class);
Passport::tokensExpireIn(now()->addDay());
Passport::refreshTokensExpireIn(now()->addDay());
+ Passport::cookie(Str::lower(env('APP_NAME') . '_token'));
}
View の作成
マイページ画面として表示する Index.vue
を新たに作成します。
初期表示時にユーザ情報を取得する API を実行しています。未認証であればログイン画面にリダイレクトします。
<script setup>
import axios from 'axios'
let name = ''
await axios
.get('/api/user')
.then((response) => {
name = response?.data?.name
})
.catch((reason) => {
if (reason?.response?.status === 401) {
window.location.href = '/login'
}
})
</script>
<template>
<div class="text-center px-6 py-24">
<div class="text-4xl font-bold">ようこそ、<br class="sm:hidden" />{{ name }} さん</div>
<div class="mt-16 flex items-center justify-center space-x-5">
<a
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>
<button
type="button"
class="rounded-md bg-gray-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600"
>
ユーザ情報確認
</button>
</div>
</div>
</template>
ルートの登録
追加した View を表示する ルートを追加します。また、未認証/認証済みでルートを制御するため、ミドルウェアを適用します。
+ Route::middleware(['guest'])->group(function () {
Route::get('/login', fn () => view('login'))->name('login');
Route::post('/login', [App\Http\Controllers\AuthController::class, 'login']);
+ });
+ Route::middleware(['auth:web'])->group(function () {
+ Route::get('/', fn () => view('app'));
+ });
- routes: []
+ routes: [
+ {
+ path: '/',
+ name: 'index',
+ component: () => import('../views/Index.vue')
+ }
+ ]
認証時にリダイレクトされる Laravel の HOME
ルートを変更します。
class RouteServiceProvider extends ServiceProvider
{
- public const HOME = '/home';
+ public const HOME = '/';
動作確認
ここまで正しく作成できているか確認のため、実際に開発用サーバを起動して動作させてみます。以下のコマンドを実行して、http://localhost:8000
にアクセスします。
npm run build
php artisan serve
アクセスすると、このようなログイン画面が表示されます。
ログインするユーザが存在しないので、Seeder を用いてユーザを登録します。DatabaseSeeder
を変更してから db:seed
コマンドを実行するとユーザを登録できます。
public function run(): void
{
// \App\Models\User::factory(10)->create();
- // \App\Models\User::factory()->create([
- // 'name' => 'Test User',
- // 'email' => 'test@example.com',
- // ]);
+ \App\Models\User::factory()->create([
+ 'name' => 'Test User',
+ 'email' => 'test@example.com',
+ ]);
}
php artisan db:seed
ログイン画面に以下の情報を入力して「ログインする」をクリックします。
メールアドレス : test@example.com
パスワード : password
以下のようなマイページ画面が表示されれば OK です。
おわりに
続きはこちらの記事をご覧ください。