nurbekjummayev / filament-tdc-sso
TDC-SSO (OAuth2 Authorization Code + PKCE) login, screen lock and PIN unlock plugin for Filament panels.
Package info
github.com/nurbekjummayev/filament-tdc-sso
pkg:composer/nurbekjummayev/filament-tdc-sso
Requires
- php: ^8.4
- filament/filament: ^5.0
- illuminate/contracts: ^12.0|^13.0
- spatie/laravel-package-tools: ^1.92.7
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- pestphp/pest-plugin-livewire: ^4.0
Suggests
- spatie/laravel-permission: Default roles/permissions for new users and role sync from the SSO payload.
Provides
None
Conflicts
None
Replaces
None
README
A Filament v5 plugin for TDC-SSO login over OAuth2 (Authorization
Code + PKCE), with a screen lock and PIN unlock — the panel
counterpart of laravel-tdc-sso-client
(which serves SPAs through Passport cookies).
A Filament panel is server-rendered, so there is no Passport and no token cookies: the plugin logs the user into the panel's session guard. The provider token never reaches the browser.
laravel-tdc-sso-client (API / SPA) |
filament-tdc-sso (panel) |
|
|---|---|---|
| Session | Passport session_token cookie |
Laravel session (panel guard) |
| Lock | revoke session_token |
log the guard out, remember the locked user |
| Unlock | unlock_token cookie + PIN |
session + PIN |
| 12h cap | unlock_token expiry |
session.max_lifetime_minutes from login |
| Idle | SPA timer + server backstop | built-in JS timer (cross-tab) + server backstop |
| Tables | sso_auth_logs, sso_lock_pins, sso_unlock_tokens |
sso_auth_logs, sso_lock_pins (same schema) |
Requirements
- PHP 8.4+ · Laravel 12/13 · Filament 5
- A users table with the lookup column (default
pin, the PINFL) spatie/laravel-permissionis optional (default roles / role sync)
Installation
composer require nurbekjummayev/filament-tdc-sso php artisan migrate
The package is private; add the VCS repository to the host composer.json
first (same as laravel-tdc-sso-client):
"repositories": [ { "type": "vcs", "url": "git@github.com:nurbekjummayev/filament-tdc-sso.git" } ]
Optionally publish the config and translations:
php artisan vendor:publish --tag=filament-tdc-sso-config php artisan vendor:publish --tag=filament-tdc-sso-translations
Register the plugin on the panel:
use Nurbekjummayev\FilamentTdcSso\TdcSsoPlugin; public function panel(Panel $panel): Panel { return $panel ->login() ->plugin(TdcSsoPlugin::make()); }
.env — the same variables as laravel-tdc-sso-client:
SSO_BASE_URL=https://apply.epauzb.uz SSO_CLIENT_ID=... SSO_CLIENT_SECRET=... # optional — defaults to the panel callback route, e.g. https://kpi.test/sso/callback SSO_REDIRECT_URI=
Register https://<panel-url>/sso/callback as the redirect URI of the SSO
client.
Flow
Login page ─▶ GET sso/redirect ─▶ provider /oauth/authorize (state + PKCE in the session)
◀─ GET sso/callback?code&state
state check ─▶ token exchange ─▶ /oauth/userinfo (one-shot)
─▶ upsert user by pin ─▶ login gate ─▶ panel guard login ─▶ intended URL
Idle (JS, all tabs) ─▶ POST sso/lock ─▶ guard logged out, user remembered ─▶ sso/lock screen
Lock screen + PIN ─▶ guard login again (no SSO round-trip)
12h cap / no PIN / gate denied ─▶ full logout ─▶ SSO login again
Plugin options
TdcSsoPlugin::make() ->loginButton() // "Sign in with SSO" under the password form (default) ->ssoOnly() // replace the password form with an SSO-only page ->screenLock() // idle lock + PIN unlock (default: config screen_lock.enabled) ->idleTimeout(15) // minutes (default: config screen_lock.idle_timeout) ->pinPage() // PIN page + user menu item (default) ->requirePin(); // users without a PIN must set one first
Options are per panel, so an admin panel can be ssoOnly() while another
keeps passwords.
Routes
Registered inside every panel that uses the plugin (filament.{panel}.sso.*):
| Method | URI | Auth | Purpose |
|---|---|---|---|
GET |
sso/redirect |
guest | Start the PKCE flow |
GET |
sso/callback |
guest | Provider callback → panel login |
GET |
sso/lock |
locked session | Lock screen (PIN form) |
POST |
sso/lock |
panel auth | Lock now (used by the idle script) |
POST |
sso/heartbeat |
panel auth | Activity ping from the idle script |
GET |
sso/pin |
panel auth | Set / change PIN page |
Screen lock
- The idle script locks after
idleTimeoutminutes without mouse / keyboard / scroll / touch input. Activity is shared throughlocalStorage, so an active tab keeps the others unlocked, and a lock in one tab locks them all. - The server enforces the same window as a backstop, counting only page loads and heartbeats — Livewire polling (e.g. database notifications) can not keep an idle session alive.
- Locking logs the panel guard out, so no
authroute accepts the session while it is locked; every panel page redirects to the lock screen. - The lock screen also offers SSO re-login and sign out.
- Users without a PIN cannot unlock, so idling logs them out instead.
max_attemptswrong PINs lock the PIN forlockout_minutes.session.max_lifetime_minutes(default 720 = 12h) is an absolute cap from login; unlocking never extends it.
User matching & provisioning
The SSO subject is matched by lookup.column = userinfo[lookup.claim]
(default pin = pin). Identity columns first_name, last_name,
father_name, full_name, tin (and a blank legacy name) are written only
when the users table has them.
Unknown users are rejected unless SSO_AUTO_CREATE_USER=true; new users get
new_user_attributes (e.g. ['role' => 'employee']). If your users table has
NOT NULL columns the SSO payload does not provide (e.g. email, password),
make them nullable or provide them there.
Login gate
Checked on callback, on unlock (before the PIN) and on every panel request.
Soft-deleted users are always rejected and FilamentUser::canAccessPanel() is
honoured. SSO_ACTIVE_COLUMN=is_active rejects users whose column is falsy;
for a custom rule bind a class implementing Contracts\LoginGate in
login_gate.
Auth logs
login, logout, lock, unlock, pin_set, pin_changed, login_denied,
session_expired go to sso_auth_logs with IP, user agent and session id
(token_id). Filament's own logout button is logged too. Add data to meta
with an AuthLogMetaResolver. Prune old rows:
Schedule::command('model:prune', [ '--model' => [\Nurbekjummayev\FilamentTdcSso\Models\SsoAuthLog::class], ])->daily();
Configuration
| Env var | Default | Description |
|---|---|---|
SSO_USER_MODEL |
App\Models\User |
Model to upsert |
SSO_LOOKUP_COLUMN / SSO_LOOKUP_CLAIM |
pin / pin |
User matching |
SSO_AUTO_CREATE_USER |
false |
Auto-provision unknown users |
SSO_ACTIVE_COLUMN |
(null) | Boolean column checked by the gate |
SSO_SESSION_MAX_LIFETIME |
720 |
Absolute session cap, minutes |
SSO_SCREEN_LOCK_ENABLED |
true |
Idle lock |
SSO_IDLE_TIMEOUT |
15 |
Idle minutes before locking |
SSO_HEARTBEAT_SECONDS |
60 |
Activity ping interval |
SSO_PIN_MIN_LENGTH / SSO_PIN_MAX_LENGTH |
4 / 4 |
PIN length |
SSO_PIN_MAX_ATTEMPTS / SSO_PIN_LOCKOUT_MINUTES |
5 / 15 |
Lockout policy |
SSO_SYNC_ROLES / SSO_DEFAULT_ROLES |
false / (empty) |
spatie roles |
SSO_AUTH_LOG_RETENTION_DAYS |
180 |
Prune window |
Laravel's own SESSION_LIFETIME must be at least SSO_IDLE_TIMEOUT, otherwise
the session expires before the lock screen can be used.
Testing
composer test # Pest composer analyse # PHPStan (larastan) composer format # Pint
License
The MIT License (MIT). See LICENSE.