sengheat / laravel-sso
A simple and flexible Single Sign-On (SSO) package for Laravel supporting Google, GitHub, Azure AD, and any OAuth2/OpenID Connect provider.
2.0.2
2026-05-16 06:15 UTC
Requires
- php: ^8.1
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
- laravel/socialite: ^5.0
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
README
A Laravel SSO package — one central auth-service issues tokens, every other service verifies them without HTTP calls.
How It Works
┌─────────────────────────────────────────────────────────┐
│ auth-service (Issuer) │
│ /sso/login → user logs in → issues api_token │
└───────────────────────────┬─────────────────────────────┘
│ token
┌─────────────────┼─────────────────┐
▼ ▼ ▼
order-service logistic-portal mobile-app
verifies token verifies token verifies token
via auth_db via auth_db via auth_db
(no HTTP call) (no HTTP call) (no HTTP call)
Installation
composer require sengheat/sso-token php artisan sso:install
Part 1 — Auth Service (Issuer)
The service that owns the users table and issues tokens.
1. Enable issuer mode in .env
SSO_FORM_AUTH=true SSO_FORM_AUTH_REGISTER=true SSO_ALLOWED_REDIRECTS=http://localhost:3000/sso/callback,https://portal.yourdomain.com/sso/callback
2. Publish and configure config/sso.php
return [ 'form_auth' => [ 'enabled' => env('SSO_FORM_AUTH', false), 'allow_register' => env('SSO_FORM_AUTH_REGISTER', true), ], 'allowed_redirects' => array_filter(explode(',', env('SSO_ALLOWED_REDIRECTS', ''))), 'redirect_after_login' => env('SSO_REDIRECT_AFTER_LOGIN', '/dashboard'), 'redirect_after_logout' => env('SSO_REDIRECT_AFTER_LOGOUT', '/sso/login'), 'user_model' => env('SSO_USER_MODEL', \App\Models\User::class), 'register_routes' => true, 'run_migrations' => true, 'cache_store' => env('SSO_CACHE_STORE', null), 'token_cache_ttl' => env('SSO_TOKEN_CACHE_TTL', 300), ];
3. Add HasSSOProfile trait to User model
use SengHeat\LaravelSso\Traits\HasSSOProfile; class User extends Authenticatable { use HasSSOProfile; protected $fillable = [ 'name', 'email', 'password', 'api_token', 'sso_provider', 'sso_provider_id', 'sso_token', 'sso_avatar', ]; protected $hidden = [ 'password', 'remember_token', 'api_token', 'sso_token', 'sso_provider', 'sso_provider_id', 'sso_avatar', ]; }
4. Add api guard to config/auth.php
'guards' => [ 'web' => ['driver' => 'session', 'provider' => 'users'], 'api' => ['driver' => 'token', 'provider' => 'users', 'hash' => true], ],
5. Run migration
php artisan migrate
Adds to users table: sso_provider, sso_provider_id, sso_token, sso_avatar, api_token (indexed).
Available routes (auto-registered)
| Method | URI | Description |
|---|---|---|
| GET | /sso/login |
Login page (form) |
| POST | /sso/login |
Process login |
| GET | /sso/register |
Register page |
| POST | /sso/register |
Process register |
| POST | /api/sso/login |
API login → {user, token} |
| POST | /api/sso/register |
API register → {user, token} |
| POST | /api/sso/exchange |
Exchange one-time code → {user, token} |
| POST | /api/sso/logout |
Revoke token |
| GET | /api/sso/user |
Current user info |
Part 2 — Other Services (Consumers)
Order service, product service, any service that needs to verify tokens.
1. Install package
composer require sengheat/sso-token
2. Add auth_db connection to config/database.php
'auth_db' => [ 'driver' => 'pgsql', 'host' => env('AUTH_DB_HOST', '127.0.0.1'), 'port' => env('AUTH_DB_PORT', '5432'), 'database' => env('AUTH_DB_DATABASE', 'auth_service'), 'username' => env('AUTH_DB_USERNAME', 'postgres'), 'password' => env('AUTH_DB_PASSWORD', ''), ],
3. Add sso guard to config/auth.php
'guards' => [ 'sso' => ['driver' => 'token', 'provider' => 'sso_users', 'hash' => true], ], 'providers' => [ 'sso_users' => ['driver' => 'eloquent', 'model' => App\Models\User::class], ],
4. User model — points to auth_db
use SengHeat\LaravelSso\Traits\HasSSOProfile; class User extends Authenticatable { use HasSSOProfile; protected $connection = 'auth_db'; protected $fillable = [ 'name', 'email', 'api_token', 'sso_provider', 'sso_provider_id', 'sso_avatar', ]; }
5. Register middleware in bootstrap/app.php
use SengHeat\LaravelSso\Http\Middleware\EnsureSSOUser; use SengHeat\LaravelSso\Http\Middleware\CacheSsoToken; ->withMiddleware(function (Middleware $middleware) { $middleware->alias([ 'sso.only' => EnsureSSOUser::class, 'sso.cache' => CacheSsoToken::class, ]); })
6. Protect routes
// sso.cache runs first — loads user from Redis or auth_db // auth:sso runs second — user already set, no DB query Route::middleware(['sso.cache:sso', 'auth:sso'])->group(function () { Route::apiResource('orders', OrderController::class); });
7. .env
AUTH_DB_HOST=127.0.0.1 AUTH_DB_PORT=5432 AUTH_DB_DATABASE=auth_service AUTH_DB_USERNAME=order_svc AUTH_DB_PASSWORD=your_password SSO_CACHE_STORE=redis SSO_TOKEN_CACHE_TTL=300 SSO_FORM_AUTH=false
How token verification works
Request: Authorization: Bearer <token>
│
├─ sso.cache:sso
│ HIT → load from Redis → Auth::guard('sso')->setUser($user)
│ MISS → query auth_db → store in Redis for 300s
│
└─ auth:sso → user already set → skip DB ✓
PostgreSQL security (production)
-- Create restricted read-only user per service CREATE USER order_svc WITH PASSWORD 'secret'; GRANT CONNECT ON DATABASE auth_service TO order_svc; GRANT USAGE ON SCHEMA public TO order_svc; GRANT SELECT ON TABLE public.users TO order_svc;
# pg_hba.conf — only allow order-service IP
host auth_service order_svc <ORDER_SERVICE_IP>/32 scram-sha-256
host auth_service all 0.0.0.0/0 reject
Part 3 — Web Portal (Next.js / React)
Token is never visible in the URL. A 30-second one-time code is used instead.
Flow
1. Portal → redirect to auth-service login with redirect_to
2. User logs in on auth-service
3. Auth-service → redirect back with ?code=xxx (30s expiry, one-time)
4. Portal → POST /api/sso/exchange with code → receives real token
5. Token stored in app state / secure storage
Step 1 — Redirect to auth-service
// .env.local // NEXT_PUBLIC_SSO_URL=http://localhost:8000 // NEXT_PUBLIC_SSO_REDIRECT_URI=http://localhost:3000/sso/callback const params = new URLSearchParams({ redirect_to: process.env.NEXT_PUBLIC_SSO_REDIRECT_URI! }) window.location.href = `${process.env.NEXT_PUBLIC_SSO_URL}/sso/login?${params}`
Step 2 — Callback page (/sso/callback)
"use client" import { useEffect } from "react" import { useRouter, useSearchParams } from "next/navigation" export default function SsoCallbackPage() { const router = useRouter() const searchParams = useSearchParams() useEffect(() => { const code = searchParams.get("code") if (!code) { router.replace("/login"); return } fetch(`${process.env.NEXT_PUBLIC_SSO_URL}/api/sso/exchange`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code }), }) .then(res => res.json()) .then(({ user, token }) => { // store token in zustand / context / cookie storeLogin(user, token) router.replace("/dashboard") }) .catch(() => router.replace("/login")) }, []) return <div>Signing you in…</div> }
Step 3 — Use token on any service
fetch("http://localhost:8001/api/orders", { headers: { Authorization: `Bearer ${token}` } })
.env.local
NEXT_PUBLIC_SSO_URL=http://localhost:8000 NEXT_PUBLIC_SSO_REDIRECT_URI=http://localhost:3000/sso/callback
Part 4 — Mobile App (React Native / Flutter)
Mobile calls the API endpoints directly — no browser redirect needed.
Login
POST /api/sso/login
{ "email": "user@example.com", "password": "password" }
→ { "user": {...}, "token": "xxxx" }
Store token securely (iOS Keychain / Android Keystore).
Use token on any service
GET http://order-service/api/orders
Authorization: Bearer xxxx
Logout
POST /api/sso/logout
Authorization: Bearer xxxx
→ token revoked on all services instantly
React Native
import * as SecureStore from "expo-secure-store" // Login const { user, token } = await fetch("http://auth:8000/api/sso/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), }).then(r => r.json()) await SecureStore.setItemAsync("sso_token", token) // Use on any service const token = await SecureStore.getItemAsync("sso_token") const orders = await fetch("http://orders:8001/api/orders", { headers: { Authorization: `Bearer ${token}` }, }).then(r => r.json())
Flutter
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; final storage = FlutterSecureStorage(); // Login final res = await http.post(Uri.parse('http://auth:8000/api/sso/login'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({'email': email, 'password': password})); final token = jsonDecode(res.body)['token']; await storage.write(key: 'sso_token', value: token); // Use on any service final token = await storage.read(key: 'sso_token'); final orders = await http.get(Uri.parse('http://orders:8001/api/orders'), headers: {'Authorization': 'Bearer $token'});
Trait Helpers
$user->isSSOUser(); // true if logged in via OAuth provider $user->usesProvider('google'); // true | false $user->ssoAvatar('/default.png'); // avatar URL with fallback $user->generateApiToken(); // generates token, stores SHA256 hash, returns plain $user->revokeApiToken(); // sets api_token = null (logout) User::fromProvider('google')->get(); // query scope User::nativeUsers()->get(); // users without OAuth
Security Summary
| Concern | Solution |
|---|---|
| Token in URL | One-time code exchange — token never in URL |
| Token storage | SHA256 hash in DB — plain token only in HTTP response |
| Open redirect | SSO_ALLOWED_REDIRECTS whitelist |
| DB access | Dedicated read-only PostgreSQL user per service |
| Network | pg_hba.conf IP whitelist on auth_db port |
| Brute force | throttle:20,1 on login routes |
| Cache | Redis with TTL — stale tokens auto-expire |
Environment Variables Reference
| Variable | Service | Default | Description |
|---|---|---|---|
SSO_FORM_AUTH |
Issuer | false |
Enable built-in login/register |
SSO_FORM_AUTH_REGISTER |
Issuer | true |
Allow new registrations |
SSO_ALLOWED_REDIRECTS |
Issuer | — | Comma-separated portal callback URLs |
SSO_REDIRECT_AFTER_LOGIN |
Issuer | /dashboard |
Fallback redirect after login |
SSO_REDIRECT_AFTER_LOGOUT |
Issuer | /sso/login |
Redirect after logout |
SSO_CACHE_STORE |
Consumer | null (default) |
Cache driver: redis, file |
SSO_TOKEN_CACHE_TTL |
Consumer | 300 |
Token cache TTL in seconds |
AUTH_DB_HOST |
Consumer | 127.0.0.1 |
Auth-service DB host |
AUTH_DB_PORT |
Consumer | 5432 |
Auth-service DB port |
AUTH_DB_DATABASE |
Consumer | auth_service |
Auth-service DB name |
AUTH_DB_USERNAME |
Consumer | — | Read-only DB user |
AUTH_DB_PASSWORD |
Consumer | — | DB password |
License
MIT — Seng Heat