Open source

laravel-sso เข้าสู่ระบบด้วย Google ให้แอป Laravel ที่มีเจ้าของคนเดียว

แพ็กเกจที่เปลี่ยนหน้า login ด้วยรหัสผ่านของแอป Laravel ให้เป็น Sign in with Google พร้อม allowlist อีเมล และล็อกอินบนเครื่อง dev ได้โดยไม่ต้องผ่าน Google

เผยแพร่
เวลาอ่าน
5 นาที

ปัญหาที่เจอ

ผมมีแอป Laravel เล็ก ๆ หลายตัวที่ใช้เองคนเดียว ทั้งระบบหลังบ้าน เครื่องมือภายใน และหน้า admin ของเว็บนี้ ทุกตัวมีหน้า login กับรหัสผ่านเป็นของตัวเอง ต้องดูแล reset password และต้องจำว่าตัวไหนใช้รหัสอะไร

แอปพวกนี้ส่วนใหญ่ผมใช้งานเอง หรือเปิด allow list ให้ใช้ในกลุ่มเล็ก ๆ ระบบสมัครสมาชิกหรือสิทธิ์หลายระดับจึงเกินความจำเป็น ที่ต้องการจริง ๆ คือล็อกอินด้วยบัญชี Google ที่เปิด 2-Step Verification ไว้แล้ว และใช้ได้เหมือนกันทุกแอป เลยแยกส่วนนี้ออกมาเป็นแพ็กเกจ laravel-sso แล้วเปิดเป็น open source ให้คนที่มีโจทย์คล้ายกันนำไปใช้ได้ครับ

หลักการทำงาน

แพ็กเกจจะลงทะเบียน route /login ให้เอง และตัดสินใจตามขั้นตอนนี้

ขั้นตอนการล็อกอินของ laravel-sso เครื่อง 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 และ profile scope ชุดนี้ไม่ต้องให้ 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 ล็อกอินเป็นเจ้าของทันที แต่ต้องผ่าน ทั้งสองเงื่อนไข

  1. APP_ENV=local
  2. 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 มี key google อยู่แล้ว ค่าของแอปจะทับค่าที่แพ็กเกจตั้งไว้ ให้ตรวจว่า 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

อ่านต่อ

ล่าสุด

ดูทั้งหมด →