Filament Saml2
Filament 5 plugin for SAML2 single sign-on with multi-tenant Identity Providers, powered by jeffersongoncalves/laravel-saml2.
#Filament SAML2
SAML 2.0 single sign-on for Filament v5 panels, powered by jeffersongoncalves/laravel-saml2. Every Identity Provider registered in laravel-saml2 (Entra ID, Okta, Google Workspace, Keycloak, ADFS, OneLogin…) gets a button on the panel login page; users are provisioned on first sign-in and linked to their SAML identity in a polymorphic table, so the host application's users table is never altered.
#Compatibility
| Plugin Version | Filament |
|---|---|
| 1.x | ^3.2 |
| 2.x | ^4.0 |
| 3.x | ^5.0 |
#Installation
composer require jeffersongoncalves/filament-saml2:"^3.0"
Publish and run the migrations (saml2_tenants comes from laravel-saml2, saml2_identities from this plugin):
php artisan vendor:publish --tag="filament-saml2-migrations" php artisan migrate
Optionally publish the configuration and translations:
php artisan vendor:publish --tag="filament-saml2-config" php artisan vendor:publish --tag="filament-saml2-translations"
Register your Identity Providers with the laravel-saml2 commands, e.g.:
php artisan saml2:create-tenant --key=acme --metadata-url="https://idp.acme.com/metadata.xml"
See the laravel-saml2 README for the URLs to configure at the IdP, certificates, per-tenant settings and proxies.
#Registering the plugin in a panel
use JeffersonGoncalves\Filament\Saml2\Saml2Plugin; public function panel(Panel $panel): Panel { return $panel ->id('admin') ->path('admin') ->login() ->plugin(Saml2Plugin::make()); }
The login page now shows a Sign in with … button per tenant (labelled with metadata.name, the tenant key or the IdP entity ID).
#Options
Saml2Plugin::make() ->tenants(['acme', 'okta']) // only these tenants (key or UUID); null = all ->guard('admin') // defaults to the panel auth guard ->userModel(\App\Models\Admin::class) ->autoCreateUsers(false) // only let in users that already exist ->matchUsersByEmail(false) // only link by NameID, never by e-mail ->position('before'); // buttons before the login form
#How it works
- The button sends the user to
saml2.loginwithreturnToset to the panel callback route (filament.{panel}.saml2.callback, path/{panel}/saml2/callback). - The IdP posts the assertion to
laravel-saml2's ACS endpoint, which firesSignedIn. - The plugin listener reads the panel from the
RelayState, resolves the user and logs them into the panel guard. - The ACS redirects to the callback route, which lands the user on the panel (or back on the login page with a notification if the sign-in was refused).
The panel travels in the RelayState rather than the session because the IdP posts cross-site and SameSite=Lax cookies are not sent with that request. SignedIn events whose RelayState is not a panel callback are ignored, so your own listeners keep working for the rest of the app.
For IdP-initiated sign-in, set the tenant relay_state_url to the panel callback path (/admin/saml2/callback).
#User resolution
- The
saml2_identitiesrow for the tenant UUID + NameID. resolveUserUsing(), when set.- The user with the same e-mail (
Saml2User::email()), unlessmatchUsersByEmail(false). - A new user (
name,email, randompassword), unlessautoCreateUsers(false).
The identity is then created or refreshed with the latest e-mail and attributes.
Security: matching by e-mail trusts the IdP to only assert e-mails it owns. If a panel allows an IdP you do not control (e.g. a customer-managed tenant), limit it with
->tenants([...])and disablematchUsersByEmail.
Add the HasSaml2Identities trait to expose the relation on your user model:
use JeffersonGoncalves\Filament\Saml2\Concerns\HasSaml2Identities; class User extends Authenticatable { use HasSaml2Identities; }
#Customising user provisioning
use JeffersonGoncalves\LaravelSaml2\Saml2User; Saml2Plugin::make() ->userAttributesUsing(fn (Saml2User $saml) => [ 'name' => $saml->mapped()['name'] ?? $saml->email(), 'email' => $saml->email(), 'department' => $saml->first('department'), 'password' => bcrypt(\Illuminate\Support\Str::random(40)), ]) ->resolveUserUsing(fn (Saml2User $saml) => \App\Models\User::query() ->where('employee_id', $saml->first('employeeNumber')) ->first()); // null falls back to the default lookup
#Logout
Filament's logout ends the local session. To also end the IdP session (Single Logout), send the user to laravel-saml2's logout route:
use JeffersonGoncalves\LaravelSaml2\Facades\Saml2; redirect(Saml2::logoutUrl(returnTo: filament()->getLoginUrl()));
IdP-initiated logout is handled by laravel-saml2; set saml2.logout.guard to the panel guard.
#Testing
composer test
#Changelog
Please see CHANGELOG for more information on what has changed recently.
#Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
#Credits
#License
The MIT License (MIT). Please see License File for more information.