postiefs / laravel-sso
Sign in to a Laravel app through the Postiefs SSO portal (Microsoft Entra ID), with portal-controlled sessions.
Requires
- php: ^8.2
- firebase/php-jwt: ^6.10|^7.0
- illuminate/auth: ^11.0|^12.0|^13.0
- illuminate/cache: ^11.0|^12.0|^13.0
- illuminate/console: ^11.0|^12.0|^13.0
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/routing: ^11.0|^12.0|^13.0
- illuminate/session: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.0|^4.0|^5.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0|^5.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Sign in to a Laravel app through the Postiefs SSO portal (Microsoft Entra ID). The portal owns the session: this package trusts its launch token, mirrors its deadline, and signs the user out when an administrator ends the session or removes their access. Local logins keep working next to it.
For a Filament panel add postiefs/laravel-filament-sso on top.
Documentation
- Installation: requirements,
.env, the Callback URL, protecting routes, deploying - How it works: the launch token, the session, the check with the portal, fail open
- Configuration: every option
- Users and roles: finding and creating users, who may sign in, roles by event
- Reference: middleware, events, facade, contracts, exceptions, commands, Blade
- Testing: launch tokens and a fake portal in your tests
- Troubleshooting: a sign-in that does nothing, sessions that end too early or never
How it works
Launchpad "Open" -> portal re-checks access -> 302 {Callback URL}?sso_token=<JWT, 60s>
|
App GET /sso/callback verify the token (HS256, iss, aud, exp), single use, find or create the user, sign in
App every request deadline reached? signed out. Every 60s: POST {SSO_URL}/auth/check-access
Browser timer for the deadline + a heartbeat, so an idle tab is signed out too
Launch token claims: iss=sso-portal, aud=<app id>, exp (about 60s, the token itself), sexp (the session's
absolute deadline, adopted as is and never extended), jti (the portal session id), email, name, sub, roles,
department, groups.
If the portal cannot be reached the user stays signed in (fail open): an outage never locks everyone out.
Install
composer require postiefs/laravel-sso php artisan sso:install
sso:install publishes config/postiefs-sso.php, prints the .env keys and the Callback URL to register in the portal
(https://your-app.example.com/sso/callback, exactly). A Callback URL that points anywhere else is the usual reason a
sign-in "does nothing"; with postiefs-sso.stray_token.forward on (the default) the package forwards a token that landed on the
wrong page, and logs a warning that names the page.
SSO_APP_ID= # the id registered in the portal (the token `aud`) SSO_JWT_SECRET= # EXACTLY the portal's SSO_JWT_SECRET. The portal owns it, never generate your own SSO_URL= # server to server address of the portal (the session check), prefer an internal one SSO_PUBLIC_URL= # the address the browser is sent to for the Microsoft sign-in SSO_PORTAL_URL= # the launchpad, where users land after signing out SESSION_LIFETIME=720 # at least as long as the portal's session (12 hours)
Single sign-on is off until SSO_APP_ID and SSO_JWT_SECRET are set. Test the connection and the secret:
php artisan sso:check
Use
Protect routes with the sso middleware group (sign in through the portal + the portal's session rules), or only
the session rules on routes that already have your own auth:
Route::middleware(['web', 'sso'])->group(...); // guests go to the portal Route::middleware(['web', 'auth', 'sso.session'])->...; // keep your own login, add the portal's rules
In a Blade layout (no Filament) add the browser guard and, on your login page, the button:
@ssoSessionGuard <x-postiefs-sso::login-button />
Read the portal session anywhere:
use Postiefs\Sso\Facades\Sso; Sso::active(); // signed in through the portal (false for a local login) Sso::secondsRemaining(); Sso::hasRole('editor'); // the portal's `admin` role always passes Sso::groups(); Sso::department();
Users
postiefs-sso.user decides how the token's email becomes a local user. By default a user is found by email (case insensitive)
or created with a random password; an existing user is never changed, except the columns listed in postiefs-sso.user.sync.
| Setting | |
|---|---|
model, guard, match_on |
the user model, the auth guard, the column matched against the email |
create_missing |
create a user the first time someone signs in |
defaults |
extra attributes of a created user, for example ['status' => 'active'] |
attributes |
column => claim, written when a user is created (columns the table lacks are skipped) |
sync |
column => claim, refreshed on every sign-in |
The package ships no migration and adds no columns to users.
Who may sign in
Extend the resolver and point postiefs-sso.user.resolver at it:
class AppUserResolver extends \Postiefs\Sso\Support\DefaultUserResolver { protected function isAllowed(Model&Authenticatable $user, SsoIdentity $identity): bool { return $user->status !== UserStatus::Inactive; // a refused user sees a 403 } }
Roles stay local (for example spatie/laravel-permission); the package never applies the portal's roles. Give a new user a starting role with an event:
Event::listen(SsoUserCreated::class, fn ($event) => $event->user->assignRole('Team Member'));
Events: SsoUserCreated, SsoLoggedIn (with created), SsoSessionEnded (reason: expired, revoked,
unknown_session, a portal denial reason, logout).
Logging
Every line starts with [sso]. Warnings and errors are always written (token rejected, portal unreachable, secret
mismatch, stray token). Set SSO_DEBUG=true to log each step of a sign-in while connecting an app (callback reached,
token accepted, user resolved, redirect target), and SSO_LOG_CHANNEL to send them to their own channel.
Testing your app
use Postiefs\Sso\Testing\LaunchToken; $this->get('/sso/callback?sso_token='.LaunchToken::make(['email' => 'jane@example.com', 'roles' => ['editor']])) ->assertRedirect();
Bind a fake Postiefs\Sso\Contracts\PortalClient to fake the portal.
Extending
| Contract | |
|---|---|
Contracts\PortalClient |
talks to the portal (verify the token, check a session) |
Contracts\UserResolver |
identity to local user |
Exceptions: InvalidSsoToken, SsoNotConfigured, and SignInFailed (an HttpException, so Laravel renders the error
page with the right status). Messages live in resources/lang/en/messages.php (php artisan vendor:publish --tag=postiefs-sso-lang).
Tests
composer install vendor/bin/pest