ปัญหาที่เจอ
ผมมีแอป Laravel เล็ก ๆ หลายตัวที่ใช้เองคนเดียว ทั้งระบบหลังบ้าน เครื่องมือภายใน และหน้า admin ของเว็บนี้ ทุกตัวมีหน้า login กับรหัสผ่านเป็นของตัวเอง ต้องดูแล reset password และต้องจำว่าตัวไหนใช้รหัสอะไร
แอปพวกนี้ส่วนใหญ่ผมใช้งานเอง หรือเปิด allow list ให้ใช้ในกลุ่มเล็ก ๆ ระบบสมัครสมาชิกหรือสิทธิ์หลายระดับจึงเกินความจำเป็น ที่ต้องการจริง ๆ คือล็อกอินด้วยบัญชี Google ที่เปิด 2-Step Verification ไว้แล้ว และใช้ได้เหมือนกันทุกแอป เลยแยกส่วนนี้ออกมาเป็นแพ็กเกจ laravel-sso แล้วเปิดเป็น open source ให้คนที่มีโจทย์คล้ายกันนำไปใช้ได้ครับ
หลักการทำงาน
แพ็กเกจจะลงทะเบียน route /login ให้เอง และตัดสินใจตามขั้นตอนนี้
เครื่อง dev เข้าได้ทันที ส่วนเครื่องจริงต้องผ่าน Google และ allowlist
- ใช้ allowlist แทนการสมัคร อีเมลที่อยู่ใน
SSO_ALLOWED_EMAILSจะล็อกอินเป็น user เจ้าของคนเดียวกัน (SSO_LOGIN_AS) อีเมลอื่นจะได้หน้า 403 และแพ็กเกจไม่สร้าง user ใหม่ให้เลย - จำการล็อกอินไว้ยาว ๆ ทุกครั้งที่ล็อกอินจะตั้ง remember cookie ของ Laravel ซึ่งอยู่ได้จนกว่าเบราว์เซอร์จะตัด (ประมาณ 400 วัน)
- ใช้ OAuth client ตัวเดียวทุกแอป เมื่อล็อกอิน Google ไว้แล้ว แอปถัดไปแค่กดเลือกบัญชีครั้งเดียว
ติดตั้ง
composer require phattarachai/laravel-sso
php artisan sso:install
sso:install จะเพิ่ม key ที่ต้องใช้ลงใน .env.example ตรวจว่ามี user เจ้าของอยู่ในฐานข้อมูลหรือยัง และพิมพ์ redirect URI ที่ต้องไปลงทะเบียนใน Google ออกมาให้
จากนั้นตั้งค่าใน .env
SSO_ALLOWED_EMAILS=you@gmail.com,you@your-company.com
SSO_LOGIN_AS=you@gmail.com
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
SSO_LOGIN_AS ไม่ใส่ก็ได้ ถ้าไม่ใส่จะใช้อีเมลแรกใน allowlist
ขั้นสุดท้ายคือลบ route login / logout controller ที่ตรวจรหัสผ่าน และหน้า login เดิมของแอปออก แพ็กเกจจะลงทะเบียน route เหล่านี้ให้แทน
| Method | URI | ชื่อ route | ทำอะไร |
|---|---|---|---|
| GET | /login |
login |
ส่งไป Google หรือเข้าทันทีบนเครื่อง dev |
| GET | /auth/sso/callback |
sso.callback |
ตรวจ allowlist แล้วล็อกอินเป็นเจ้าของ |
| POST | /logout |
logout |
ออกจากระบบแล้วไปหน้า /signed-out |
| GET | /signed-out |
sso.signed-out |
หน้าแจ้งว่าออกจากระบบแล้ว พร้อมลิงก์เข้าใหม่ |
middleware auth ของ Laravel ส่ง guest ไปที่ route ชื่อ login อยู่แล้ว ส่วนอื่นของแอปจึงไม่ต้องแก้ route ของ API, webhook และ broadcasting ก็ไม่ได้รับผลกระทบ เพราะแพ็กเกจเปลี่ยนเฉพาะการล็อกอินผ่าน session ของเว็บ
ตั้งค่า Google OAuth client
ใน Google Cloud Console ให้สร้าง OAuth client แบบ Web application หนึ่งตัว แล้วใช้ร่วมกันทุกแอป
- Consent screen เลือก External และขอ scope แค่
openid,emailและprofilescope ชุดนี้ไม่ต้องให้ Google ตรวจสอบแอป - Authorized redirect URIs ใส่บรรทัดละหนึ่งแอป เช่น
https://notes.example.app/auth/sso/callback
Google ไม่มี API หรือคำสั่ง gcloud สำหรับจัดการ OAuth client แบบ Web การเพิ่มแอปใหม่จึงต้องเข้าไปเพิ่ม redirect URI ใน Console เอง
ล็อกอินบนเครื่อง dev ได้โดยไม่ผ่าน Google
Google ไม่รับ redirect URI ที่ลงท้ายด้วย .test บนเครื่อง dev จึงทำ OAuth จริงไม่ได้ แพ็กเกจแก้ด้วยการให้ /login ล็อกอินเป็นเจ้าของทันที แต่ต้องผ่าน ทั้งสองเงื่อนไข
APP_ENV=local- host ของ request ตรงกับ
local_hostsคือlocalhost,127.0.0.1,*.testหรือ*.localhost
ถ้าเครื่อง production ถูกตั้ง APP_ENV=local ผิดไป host ก็ยังเป็นโดเมนจริง จึงยังต้องไปผ่าน Google อยู่ดี ถ้าใช้โดเมน dev แบบอื่นก็เพิ่มได้ใน config
php artisan vendor:publish --tag=sso-config
'local_login' => env('SSO_LOCAL_LOGIN', true),
'local_hosts' => ['localhost', '127.0.0.1', '*.test', '*.localhost', '*.ddev.site'],
ถ้าไม่อยากให้เครื่องไหนเข้าได้ทันที ให้ตั้ง SSO_LOCAL_LOGIN=false
ใช้กับ Inertia
/login กับ /logout ต้องพาเบราว์เซอร์ไปที่โดเมนของ Google ซึ่ง XHR ของ Inertia ตามไปไม่ได้ เมื่อ request มี header X-Inertia แพ็กเกจจึงตอบเป็น 409 พร้อม X-Inertia-Location ทำให้ Inertia โหลดหน้าใหม่ทั้งหน้าแทน ปุ่ม logout จึงเขียนด้วย Link ได้ตามปกติ
import { Link } from '@inertiajs/react';
import { logout } from '@/routes';
<Link href={logout()} as="button">
ออกจากระบบ
</Link>;
ตัวอย่างนี้ใช้ route ที่ Wayfinder สร้างให้ ถ้าไม่ได้ใช้ Wayfinder ก็ใส่ href="/logout" method="post" แทนได้
หลัง logout แพ็กเกจจะพาไปหน้า /signed-out ไม่ได้พากลับ /login เพราะ session ของ Google ยังอยู่ ถ้ากลับไป /login ก็จะล็อกอินกลับเข้ามาทันที
ถ้าอยากให้ Google ขึ้นหน้าเลือกบัญชีทุกครั้ง เช่น ทำปุ่ม "ใช้บัญชีอื่น" ให้เติม ?select=1
<a href="/login?select=1">ใช้บัญชี Google อื่น</a>
เปลี่ยนไปใช้ IdP ของตัวเองในอนาคต
ตัวที่คุยกับ Google คือ driver ของ Laravel Socialite และเลือกได้จาก config ถ้าวันหนึ่งมี IdP ของตัวเองที่มี Socialite driver ก็เปลี่ยนแค่ค่าเดียว ไม่ต้องแก้โค้ดในแต่ละแอป
SSO_DRIVER=google
ข้อควรรู้ก่อนใช้
User::firstOrCreate(
['email' => '[email protected]'],
['name' => 'Owner', 'password' => Str::random(64)],
);
- ทุกอีเมลใน allowlist คือ user คนเดียวกัน ถ้าแอปต้องแยกว่าใครทำอะไร หรือต้องมีสิทธิ์หลายระดับ แพ็กเกจนี้ไม่เหมาะ
- ถ้าแอปใช้ Breeze, Fortify หรือ Jetstream ต้องเอา route
login/logoutของแอปออกก่อน ไม่อย่างนั้นชื่อ route จะชนกัน - บัญชีที่ Google ยังไม่ยืนยันอีเมล (
email_verifiedเป็น false) จะถูกปฏิเสธ แม้อีเมลจะอยู่ใน allowlist - ถ้า
config/services.phpมี keygoogleอยู่แล้ว ค่าของแอปจะทับค่าที่แพ็กเกจตั้งไว้ ให้ตรวจว่าclient_idและclient_secretชี้ไปที่ env ตัวเดียวกัน - 2-Step Verification แอปไม่ได้ตรวจ MFA เอง เพราะ Google ไม่ได้ส่งข้อมูลนี้มากับการล็อกอิน ให้เปิด 2-Step Verification ที่บัญชี Google ใน allowlist แทน
เวอร์ชันที่รองรับ
- PHP 8.3 ขึ้นไป
- Laravel 12 และ 13
- Laravel Socialite 5.28 ขึ้นไป (ติดตั้งมาพร้อมแพ็กเกจ)
GitHub: phattarachai/laravel-sso


