Search by

nurbekjummayev / filament-tdc-sso

nurbekJummayev

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

Statistics

Installs: 6

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.1 2026-10-04 20:07 UTC

This package is auto-updated.

Last update: 2026-10-04 20:08:54 UTC


README

Tests License: MIT

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-permission is 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 idleTimeout minutes without mouse / keyboard / scroll / touch input. Activity is shared through localStorage, 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 auth route 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_attempts wrong PINs lock the PIN for lockout_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.