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.

Maintainers

Package info

github.com/SengHeat/sso-token

Language:Blade

pkg:composer/sengheat/laravel-sso

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

2.0.2 2026-05-16 06:15 UTC

This package is auto-updated.

Last update: 2026-05-16 17:02:13 UTC


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