การสร้างระบบ Auth ใน Laravel 12 + Keycloak

ภาพรวมระบบ (System Overview)

เราจะใช้ Keycloak เป็น Identity Provider (IDP) หลักในการจัดการผู้ใช้งานและการยืนยันตัวตนทั้งหมด โดย Laravel จะทำหน้าที่เป็น Service Provider (SP) ที่เชื่อมต่อกับ Keycloak เพื่อรับข้อมูลผู้ใช้งานมาจัดการภายในแอปพลิเคชัน Laravel ของเรา

สถาปัตยกรรม (Architecture)

ขั้นตอนการตั้งค่าและแนวทางปฏิบัติ (Setup and Best Practices)

1. การเตรียม Keycloak (Keycloak Setup)

รัน Keycloak บน Docker Image เป็น Best Practice เพื่อความสะดวกในการจัดการและ Deployment

1.1 Docker Compose สำหรับ Keycloak (Recommended)

สร้างไฟล์ docker-compose.yml ในโปรเจกต์ของคุณ (หรือแยกออกมาในโฟลเดอร์สำหรับ Infrastructure)

# docker-compose.yml
version: ‘3.8’
services:
keycloak:
image: quay.io/keycloak/keycloak:latest
container_name: keycloak
command: start-dev # สำหรับการพัฒนา, ห้ามใช้ใน Production!
ports:
– “8080:8080”
– “8443:8443” # ถ้าต้องการใช้ HTTPS
volumes:
– ./keycloak-data:/opt/keycloak/data
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
KC_HOSTNAME: localhost # หรือ IP Address ของเครื่องคุณ
KC_HTTP_PORT: 8080 # พอร์ตที่ Keycloak จะรัน

1.2 รัน Keycloak

docker-compose up -d

คุณควรจะเข้าถึง Keycloak Admin Console ได้ที่ http://localhost:8080 (หรือพอร์ตที่คุณตั้งค่าไว้) ด้วย username admin และรหัสผ่านที่คุณตั้งค่าไว้

1.3 การตั้งค่า Realm และ Client ใน Keycloak

  • สร้าง Realm ใหม่:

    • เข้าสู่ Keycloak Admin Console
    • คลิกที่ Master (ด้านซ้ายบน) แล้วเลือก Add realm
    • ตั้งชื่อ Realm เช่น LaravelApp
  • สร้าง Client ใหม่:

    • ใน Realm ที่สร้างขึ้น LaravelApp เลือก Clients
    • คลิก Create client
    • Client type: OpenID Connect
    • Client ID: ตั้งชื่อ laravel-app (ใช้ใน Laravel config)
    • Name: Laravel Application (ชื่อที่แสดง)
    • Description: (ไม่บังคับ)
    • Root URL: URL หลักของแอปพลิเคชัน Laravel ของคุณ เช่น http://localhost:8000
    • Valid Redirect URIs: เพิ่ม http://localhost:8000/auth/callback (สำคัญมาก!)
    • Web origins: http://localhost:8000
    • Standard Flow Enabled: ON
    • Client authentication: ON
  • สร้าง Credentials สำหรับ Client:

    • เมื่อสร้าง Client แล้ว ไปที่แท็บ Credentials
    • คุณจะเห็น Client secret ตรงนี้ ให้คัดลอกค่านี้ไปใช้ใน Laravel config

2. การตั้งค่า Laravel (Laravel Setup)

2.1 ติดตั้ง Laravel 12

ตรวจสอบให้แน่ใจว่าคุณติดตั้ง Laravel 12 เรียบร้อยแล้วผ่าน Composer

2.2 ติดตั้ง Socialite และ SocialiteProviders/Keycloak

composer require laravel/socialite socialiteproviders/keycloak

2.3 ตั้งค่า Environment Variables (.env)

# Keycloak
KEYCLOAK_CLIENT_ID=”laravel-app”
KEYCLOAK_CLIENT_SECRET=”<YOUR_KEYCLOAK_CLIENT_SECRET>” # คัดลอกมาจาก Keycloak Client Credentials
KEYCLOAK_REDIRECT_URI=”http://127.0.0.1:8000/auth/callback” # ต้องตรงกับ Valid Redirect URI ใน Keycloak
KEYCLOAK_BASE_URL=”http://localhost:8080″
KEYCLOAK_REALM=”LaravelApp”

2.4 ตั้งค่า config/services.php

เพิ่ม Keycloak ลงใน config/services.php

‘keycloak’ => [
        ‘client_id’ => env(‘KEYCLOAK_CLIENT_ID’),
        ‘client_secret’ => env(‘KEYCLOAK_CLIENT_SECRET’),
        ‘redirect’ => env(‘KEYCLOAK_REDIRECT_URI’),
        ‘base_url’ => env(‘KEYCLOAK_BASE_URL’), // e.g., https://auth.yourdomain.com
        ‘realms’ => env(‘KEYCLOAK_REALM’), // e.g., my-app-realm
    ],
2.5 ลงทะเบียน Provider ใน app/Providers/EventServiceProvider.php
ขั้นตอน:
2.5.1. สร้าง EventServiceProvider ด้วย Artisan:
รันคำสั่งนี้ใน Terminal ของคุณ:
php artisan make:provider EventServiceProvider

คำสั่งนี้จะสร้างไฟล์ app/Providers/EventServiceProvider.php ให้คุณ

2.5.2 แก้ไขไฟล์ app/Providers/EventServiceProvider.php: เปิดไฟล์ที่เพิ่งสร้างขึ้นมา และเพิ่ม property $listen เข้าไปเหมือนใน Laravel เวอร์ชันก่อนๆ

<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;

class EventServiceProvider extends ServiceProvider
{
protected $listen = [
//  เพิ่มโค้ดส่วนนี้เข้าไป
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
\SocialiteProviders\Keycloak\KeycloakExtendSocialite::class.’@handle’,
],
];

public function boot(): void
{
//
}

public function shouldDiscoverEvents(): bool
{
return false;
}
}

2.5.3 ลงทะเบียน Provider ใหม่ใน bootstrap/app.php: นี่คือขั้นตอนที่สำคัญที่สุด หลังจากสร้าง Provider แล้ว เราต้องบอกให้ Laravel รู้จักมันด้วย

เปิดไฟล์ bootstrap/app.php และเพิ่ม EventServiceProvider เข้าไปในส่วนของ withProviders()

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.’/../routes/web.php’,
commands: __DIR__.’/../routes/console.php’,
health: ‘/up’,
)
->withMiddleware(function (Middleware $middleware) {
//
})
->withExceptions(function (Exceptions $exceptions) {
//
})
//  เพิ่มเมธอด withProviders() นี้เข้าไป (หากยังไม่มี) หรือเพิ่มในที่มีอยู่แล้ว
->withProviders([
App\Providers\EventServiceProvider::class, //  เพิ่มบรรทัดนี้
])->create();

2.6 สร้าง Database Migration สำหรับผู้ใช้งานและ Log

1. สร้างตาราง Log

  • ตาราง auth_logs: สำหรับบันทึก Login/Logout
php artisan make:migration create_auth_logs_table
  • แก้ไขไฟล์ Migration:
// database/migrations/xxxx_xx_xx_xxxxxx_create_auth_logs_table.php
public function up(): void
{
Schema::create(‘auth_logs’, function (Blueprint $table) {
$table->id();
$table->foreignId(‘user_id’)->nullable()->constrained()->onDelete(‘set null’);
$table->string(‘action’); // e.g., login, logout, login_failed
$table->string(‘ip_address’, 45)->nullable();
$table->text(‘user_agent’)->nullable();
$table->string(‘idp_provider’)->nullable(); // e.g., keycloak, google, github
$table->timestamps();
});
}

รายละเอียดคอลัมน์ในตาราง AUTH_LOGS

ID

  • คำอธิบาย: เป็น Primary Key ของตารางนี้ คือรหัสอ้างอิงที่ไม่ซ้ำกันสำหรับ Log แต่ละรายการ ถูกสร้างขึ้นโดยอัตโนมัติทุกครั้งที่มีการเพิ่มข้อมูลใหม่
  • ตัวอย่างข้อมูล: 1, 2, 3, …
  • ข้อจำกัด: NOT NULL, PRIMARY KEY (ต้องมีค่าเสมอและห้ามซ้ำ)

USER_ID

  • คำอธิบาย: เป็น Foreign Key ที่เชื่อมโยงไปยังคอลัมน์ ID ของตาราง USERS เพื่อระบุว่า Log นี้เป็นกิจกรรมของ User คนไหน
  • ตัวอย่างข้อมูล: 101, 250 (ซึ่งคือ ID ของผู้ใช้ในตาราง USERS)
  • ข้อจำกัด: NULL (สามารถเป็นค่าว่างได้)
    • เหตุผลที่ต้องเป็นค่าว่างได้: ในกรณีที่เกิดเหตุการณ์ “Login ล้มเหลว” (login_failed) เนื่องจากใส่ชื่อผู้ใช้ผิด เราจะไม่รู้ USER_ID ที่แน่นอน แต่เรายังคงต้องการบันทึกเหตุการณ์นี้ไว้เพื่อตรวจสอบความปลอดภัย

ACTION

  • คำอธิบาย: ใช้เก็บชื่อของ “เหตุการณ์” ที่เกิดขึ้น เพื่อให้เราสามารถ Query หรือกรองข้อมูลได้ง่าย
  • ตัวอย่างข้อมูล:
    • 'login' (เข้าระบบสำเร็จ)
    • 'logout' (ออกจากระบบ)
    • 'login_failed' (พยายามเข้าระบบแต่ไม่สำเร็จ)
    • 'token_refreshed' (มีการต่ออายุ Token)
  • ข้อจำกัด: NOT NULL (ทุก Log ต้องระบุว่าเกิดเหตุการณ์อะไร)

IP_ADDRESS

  • คำอธิบาย: เก็บหมายเลข IP Address ของเครื่องที่ผู้ใช้ทำการ Login เข้ามา มีประโยชน์อย่างมากในการตรวจสอบว่ามีการพยายามเข้าระบบจากสถานที่หรือ IP ที่น่าสงสัยหรือไม่
  • ตัวอย่างข้อมูล: '192.168.1.10', '203.150.221.45'
  • ข้อจำกัด: NULL (สามารถเป็นค่าว่างได้)

USER_AGENT

  • คำอธิบาย: เก็บข้อมูลรายละเอียดของเบราว์เซอร์และระบบปฏิบัติการของผู้ใช้ ข้อมูลส่วนนี้มีประโยชน์ในการวิเคราะห์พฤติกรรมที่ผิดปกติ เช่น มีการ Login ด้วยเบราว์เซอร์แปลกๆ หรือจาก Bot
  • ตัวอย่างข้อมูล: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
  • ข้อจำกัด: NULL (สามารถเป็นค่าว่างได้)

IDP_PROVIDER

  • คำอธิบาย: คอลัมน์นี้สำคัญมากสำหรับระบบที่ใช้ Keycloak เพื่อบันทึกว่าผู้ใช้ยืนยันตัวตนผ่านช่องทางไหน
  • ตัวอย่างข้อมูล:
    • 'keycloak' (Login ด้วย Username/Password ที่หน้า Keycloak โดยตรง)
    • 'google' (Login ผ่าน Google)
    • 'github' (Login ผ่าน GitHub)
  • ข้อจำกัด: NULL (สามารถเป็นค่าว่างได้)

CREATED_AT และ UPDATED_AT

  • คำอธิบาย: เป็นคอลัมน์มาตรฐานของ Laravel (timestamps())
    • CREATED_AT: จะบันทึกวันและเวลาที่ Log นี้ถูกสร้างขึ้นโดยอัตโนมัติ
    • UPDATED_AT: จะบันทึกวันและเวลาที่ Log นี้ถูกแก้ไขล่าสุด (ซึ่งสำหรับตาราง Log มักจะไม่ถูกใช้งาน แต่มีไว้ตามมาตรฐาน)
  • ตัวอย่างข้อมูล: '2025-06-13 11:22:23'
  • ข้อจำกัด: NULL (สามารถเป็นค่าว่างได้)

รายละเอียด Constraints

CONSTRAINT auth_logs_id_pk PRIMARY KEY (ID)

  • คำอธิบาย: กำหนดให้คอลัมน์ ID เป็น Primary Key เพื่อรับประกันว่าข้อมูลแต่ละแถวจะมีเอกลักษณ์ไม่ซ้ำกัน

CONSTRAINT auth_logs_user_id_fk FOREIGN KEY (USER_ID) REFERENCES USERS(ID) ON DELETE SET NULL

  • คำอธิบาย: สร้างความสัมพันธ์ระหว่างตารางนี้กับตาราง USERS
  • ON DELETE SET NULL: เป็นส่วนที่สำคัญมาก หมายความว่า “ถ้าหาก User ที่ถูกอ้างอิงในตาราง USERS ถูกลบทิ้ง ให้ค่า USER_ID ในตาราง AUTH_LOGS นี้เปลี่ยนเป็น NULL แทน” ซึ่งดีกว่าการลบ Log ทิ้งไป เพราะทำให้เรายังคงเก็บประวัติการเข้าระบบไว้ได้ แม้ว่าตัวตนของ User จะถูกลบไปแล้วก็ตาม
  • รัน migration:
php artisan migrate

2. สร้าง Controller

php artisan make:controller Auth/KeycloakAuthController

3. โค้ดใน KeycloakAuthController.php

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;

class KeycloakAuthController extends Controller
{
/**
* Redirect the user to the Keycloak authentication page.
*/
public function redirectToProvider()
{
return Socialite::driver(‘keycloak’)->redirect();
}

/**
* Obtain the user information from Keycloak.
*/
public function handleProviderCallback(Request $request)
{
try {
$keycloakUser = Socialite::driver(‘keycloak’)->user();
} catch (\Exception $e) {
$this->logAuthAttempt(‘login_failed’, $request, null, null, ‘Keycloak callback failed: ‘ . $e->getMessage());
return redirect(‘/login’)->withErrors(‘Authentication failed. Please try again.’);
}

// ค้นหาหรือสร้างผู้ใช้ใหม่ในฐานข้อมูล Laravel
$user = User::updateOrCreate(
[‘keycloak_id’ => $keycloakUser->getId()],
[
‘name’ => $keycloakUser->getName(),
’email’ => $keycloakUser->getEmail(),
‘password’ => Hash::make(Str::random(24)), // สร้างรหัสผ่านปลอม ไม่ได้ใช้จริง
’email_verified_at’ => now(), // ถือว่า Keycloak ยืนยันอีเมลแล้ว
]
);

// ดึงข้อมูล Provider จาก Token (ถ้ามี)
$idp = $this->getIdentityProvider($keycloakUser->accessTokenResponseBody);

// ทำการ Login ผู้ใช้ใน Laravel
Auth::login($user, true); // `true` for “remember me”

// บันทึก Log การ Login สำเร็จ
$this->logAuthAttempt(‘login’, $request, $user->id, $idp);

return redirect()->intended(‘/dashboard’); // ไปยังหน้า Dashboard หรือหน้าที่ต้องการ
}

/**
* Log the user out of the application.
*/
public function logout(Request $request)
{
$user = Auth::user();
$this->logAuthAttempt(‘logout’, $request, $user->id);

Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();

// สร้าง URL สำหรับ Logout ออกจาก Keycloak ด้วย
$logoutUrl = Socialite::driver(‘keycloak’)->getLogoutUrl(redirect(‘/’)->getTargetUrl());

return redirect($logoutUrl);
}

/**
* Helper function to log authentication events.
*/
protected function logAuthAttempt(string $action, Request $request, ?int $userId, ?string $idp = ‘keycloak’, ?string $details = null)
{
DB::table(‘auth_logs’)->insert([
‘user_id’ => $userId,
‘action’ => $action,
‘ip_address’ => $request->ip(),
‘user_agent’ => $request->userAgent(),
‘idp_provider’ => $idp,
‘created_at’ => now(),
‘updated_at’ => now(),
]);
}

/**
* Helper to get the original Identity Provider from the token.
*/
protected function getIdentityProvider(array $tokenBody): ?string
{
if (isset($tokenBody[‘identity_provider’])) {
return $tokenBody[‘identity_provider’]; // e.g., ‘google’, ‘github’
}
return ‘keycloak’; // Default if logged in directly
}
}

Best Practice:

  • updateOrCreate(): เป็นวิธีที่ปลอดภัยและมีประสิทธิภาพในการซิงค์ข้อมูลผู้ใช้จาก Keycloak
  • Stateless Password: เราไม่เก็บรหัสผ่านจริงใน Laravel แต่สร้างค่าสุ่มขึ้นมาเพื่อ memenuhi constraint ของตาราง users
  • Keycloak Logout: การ Logout ที่สมบูรณ์จะต้อง Redirect ไปยัง Endpoint ของ Keycloak ด้วย เพื่อเคลียร์ Session ที่ฝั่ง Keycloak

4. เพิ่มคอลัมน์ keycloak_id

อย่าลืมเพิ่มคอลัมน์ keycloak_id ในตาราง users เพื่อใช้เป็น Unique Identifier ในการเชื่อมโยงกับ Keycloak

php artisan make:migration add_keycloak_id_to_users_table –table=users
// In the new migration file

public function up(): void
{
Schema::table(‘users’, function (Blueprint $table) {
$table->string(‘keycloak_id’)->unique()->after(‘id’)->nullable();
$table->string(‘password’)->nullable()->change(); // ทำให้ password nullable
});
}

รายละเอียดคอลัมน์ที่เปลี่ยนแปลงในตาราง users

keycloak_id

  • คำอธิบาย: เป็นคอลัมน์ที่เพิ่มขึ้นมาใหม่เพื่อเก็บ Unique ID ของผู้ใช้จากระบบ Keycloak (บางครั้งเรียกว่า sub หรือ Subject ID) เราใช้คอลัมน์นี้เป็นตัวเชื่อมที่แน่นอนระหว่างข้อมูลผู้ใช้ใน Laravel กับข้อมูลใน Keycloak แทนที่จะใช้อีเมลซึ่งอาจเปลี่ยนแปลงได้
  • ตัวอย่างข้อมูล: 'f47ac10b-58cc-4372-a567-0e02b2c3d479' (เป็นรูปแบบ UUID)
  • ข้อจำกัด:
    • unique(): ผู้ใช้แต่ละคนใน Laravel จะต้องมี keycloak_id ที่ไม่ซ้ำกัน
    • nullable(): สามารถเป็นค่าว่างได้ เพื่อรองรับผู้ใช้เดิมในระบบที่อาจจะยังไม่ได้ผูกกับ Keycloak

password

  • การเปลี่ยนแปลง: ->nullable()->change() คือการแก้ไขคุณสมบัติของคอลัมน์ password เดิม จากที่เคยบังคับให้มีค่า (NOT NULL) ให้สามารถเป็นค่าว่าง (NULL) ได้
  • เหตุผล: เนื่องจากเราย้ายการจัดการรหัสผ่านและการยืนยันตัวตนทั้งหมดไปให้ Keycloak ดูแลแล้ว ตาราง users ใน Laravel จึงไม่จำเป็นต้องเก็บรหัสผ่านอีกต่อไป การทำให้คอลัมน์นี้เป็น nullable ช่วยให้เราสามารถสร้างผู้ใช้ใหม่ที่ยืนยันตัวตนผ่าน Keycloak โดยไม่ต้องสร้างรหัสผ่านปลอมๆ ขึ้นมาเก็บไว้ในฐานข้อมูลของ Laravel

5. กำหนด Routes

ใน routes/web.php:

use App\Http\Controllers\Auth\KeycloakAuthController;

Route::get(‘/login’, [KeycloakAuthController::class, ‘redirectToProvider’])->name(‘login’);
Route::get(‘/auth/callback’, [KeycloakAuthController::class, ‘handleProviderCallback’]);
Route::post(‘/logout’, [KeycloakAuthController::class, ‘logout’])->name(‘logout’)->middleware(‘auth’);

// ตัวอย่าง Route ที่ต้องการการยืนยันตัวตน
Route::get(‘/dashboard’, function () {
return view(‘dashboard’);
})->middleware(‘auth’)->name(‘dashboard’);


สรุปแนวทาง Best Practice

  1. Centralize Identity: ใช้ Keycloak เป็น “Single Source of Truth” สำหรับข้อมูลและการยืนยันตัวตนทั้งหมด
  2. Infrastructure as Code: บริหารจัดการ Keycloak Server ด้วย Docker Compose หรือ Kubernetes เพื่อความสม่ำเสมอและง่ายต่อการทำซ้ำ
  3. Secure Configuration: ใช้ Environment Variables สำหรับข้อมูลสำคัญ (Credentials, Hostnames) และไม่ Hardcode ลงในโค้ด
  4. Decoupled Logout: ทำให้การ Logout เป็นแบบสมบูรณ์โดยเคลียร์ Session ทั้งฝั่ง Laravel และ Keycloak
  5. Comprehensive Logging: บันทึกทุกกิจกรรมการเข้า-ออกจากระบบ พร้อมข้อมูลแวดล้อม (IP, User Agent) และแหล่งที่มาของ Provider (Google, GitHub) เพื่อการตรวจสอบความปลอดภัย
  6. Stateless Users in Laravel: Laravel Application ไม่จำเป็นต้องรู้หรือจัดการรหัสผ่านของผู้ใช้เลย ลดความเสี่ยงด้านความปลอดภัยลงอย่างมาก
  7. Seamless User Sync: ใช้ updateOrCreate เพื่อให้ข้อมูลผู้ใช้ใน Laravel (เช่น ชื่อ, อีเมล) อัปเดตตรงกับข้อมูลใน Keycloak เสมอเมื่อมีการล็อกอิน

แนวทางนี้จะช่วยให้คุณได้ระบบ Authentication ที่แข็งแกร่ง, ยืดหยุ่น, ปลอดภัย และพร้อมสำหรับการใช้งานระดับ Production อย่างแท้จริงครับ!

Scroll to Top