Looking to hire Laravel developers? Try LaraJobs

laravel-sso maintained by webimpian

Description
Sign staff in to a Laravel app through an OIDC SSO portal, with access checks, back-channel logout and staff management.
Author
Last update
2026/10/24 00:56 (dev-main)
License
Links
Downloads
15

Comments
comments powered by Disqus

Laravel SSO

tests PHP 8.1+ Laravel 10 to 13 Filament 3 to 5 License: MIT

Sign staff in to a Laravel app through an OpenID Connect SSO portal. The portal handles sign-in, two-step verification and who may open the app. The app keeps its own users and roles.

You need the app registered in the portal, with its client ID and secret.

Installation

composer require webimpian/laravel-sso
php artisan migrate

The migration adds sso_sub and sso_pending to the users table.

Copy the .env block from the app's Connection tab in the portal:

SSO_ISSUER=https://sso.example.com
SSO_CLIENT_ID=your-client-id
SSO_CLIENT_SECRET=your-client-secret

Send guests to the portal, and sign out through it:

// routes/web.php
Route::redirect('login', 'auth/sso/login')->name('login');
<form method="POST" action="{{ route('sso.logout') }}">
    @csrf
    <button>Sign out</button>
</form>

Then add these URLs to the app in the portal:

Portal field URL
Callback URL https://your-app/auth/sso/callback
Sign-in URL https://your-app/auth/sso/login
Back-channel logout URL https://your-app/auth/sso/backchannel-logout

The portal calls the back-channel URL server to server, so let it through Cloudflare.

How it works

When What happens
First sign-in The package links the app's existing account by WorkOS ID (with SSO_WORKOS_ID_COLUMN) or by email, and saves sso_sub. With no account, SSO_PROVISIONING decides.
Every sign-in The account is found by sso_sub, never by email, and the portal's name and email are copied onto it. The portal is where they change, so make them read-only in the app.
Access check interval passes The next page load checks with the portal silently. Forms and background requests are never interrupted.
Access removed, revoked, or signed out in the portal The app session ends.
Idle past the app's timeout Coming back needs a full sign-in.
Portal unreachable Staff stay signed in, and the next check tries again.

The idle timeout and check interval are set per app in the portal. Sessions that didn't come from the portal, such as a break-glass admin login, are left alone.

Accounts

SSO_PROVISIONING decides what a first sign-in without an account gets:

Mode What happens
existing-only (default) "Ask an admin of this app to add you"
auto The account is created, with SSO_DEFAULT_ROLE if set
pending-approval The account is created but held until php artisan sso:approve someone@example.com

To change how accounts are found or created, set callbacks in AppServiceProvider::boot():

use Webimpian\Sso\Accounts;
use Webimpian\Sso\Sso;
use Webimpian\Sso\SsoException;

Sso::createUsersUsing(fn (array $claims) => (new User)->forceFill([
    'name' => $claims['name'],
    'email' => $claims['email'],
    'user_type' => UserType::Staff,
]));

Sso::resolveUsersUsing(function (array $claims) {
    $user = Accounts::resolve($claims);

    if ($user->suspended_at !== null) {
        throw SsoException::refuse('Your access to this app was removed.');
    }

    return $user;
});

The claims are sub, sid, name, email and picture, plus roles and workos_user_id with the staff scope.

Roles

Roles stay in the app. To copy the portal's WorkOS roles on every sign-in, map them:

SSO_ROLE_MAP=admin:admin,member:viewer
SSO_DEFAULT_ROLE=viewer

Models with syncRoles() (Spatie Permission) work as they are. Otherwise, tell the package how to give roles:

Sso::syncRolesUsing(fn (User $user, array $roles) => $user->update(['role' => $roles[0] ?? 'viewer']));

A role map or SSO_WORKOS_ID_COLUMN adds the staff scope, which an admin must allow on the app's Access tab.

Adding and removing staff

An app can manage its own staff once an admin turns on Can manage staff on its Access tab.

$staff = Sso::addStaff('aina@example.com', 'Aina', 'Rahman');
User::create(['name' => $staff['name'], 'email' => $staff['email'], 'sso_sub' => $staff['sub']]);

Sso::removeStaff($user->sso_sub);
  • addStaff() creates new people in WorkOS and the portal, and finds people already on staff. Nobody is emailed, so tell them to sign in.
  • For an app with an access list, addStaff() adds them to it, and removeStaff() takes them off and signs them out of the app at once.
  • For an app open to all staff, or by role or group, that access stays with portal admins, and access_reason says so.
  • People outside the company open only the apps whose list names them. addStaff() can't create them, but both calls add or remove an existing one on this app's list, whatever its access mode.
  • Refusals throw SsoException with the portal's reason and status: 409 revoked, 422 not a company address, 503 WorkOS down.

Revoked staff

When a portal admin revokes someone, the portal deactivates their WorkOS membership, signs them out of every app, and tells each app they used or are listed on. The package ends their sessions and fires an event, so the app can suspend the account itself:

use Webimpian\Sso\Events\StaffRestored;
use Webimpian\Sso\Events\StaffRevoked;

Event::listen(StaffRevoked::class, fn (StaffRevoked $event) => $event->user->update(['suspended_at' => now()]));
Event::listen(StaffRestored::class, fn (StaffRestored $event) => $event->user->update(['suspended_at' => null]));

The notice comes through the back-channel logout URL. Without a listener, the account stays as it is and the person can't sign in.

Filament

Panels don't use the web group, so add the middleware to each panel:

use Webimpian\Sso\Http\Middleware\KeepSsoSession;

return $panel
    ->login(null)
    ->middleware([
        // ...the panel's other middleware
        KeepSsoSession::class,
    ]);

Guests go to the portal through the login route, and the user menu signs out through it on Filament 3, 4 and 5.

Moving off an app's own WorkOS login

Today With the package
Login page, email code, Google and MFA The portal's sign-in
Match by email, then save workos_id SSO_WORKOS_ID_COLUMN=workos_id
Only some roles may get an account The app's access in the portal, with SSO_PROVISIONING=auto
Extra fields on a new account Sso::createUsersUsing()
Suspended check, last sign-in, avatar Sso::resolveUsersUsing()
workos_session_id in the session Sso::check()
Creating WorkOS users Sso::addStaff()
Removing the WorkOS membership Sso::removeStaff() for this app; removing someone from the company is a portal admin's job
WorkOS sync, Resync and RevokeWorkosSessionOnLogout Remove them

Testing

Sso::fake() signs people in, and answers addStaff() and removeStaff(), without calling the portal:

Sso::fake(['email' => $user->email, 'roles' => ['admin']]);

$this->followingRedirects()->get('/dashboard')->assertOk();

Configuration

Setting Default
SSO_ISSUER required, the portal's URL
SSO_REDIRECT_URI /auth/sso/callback on this app
SSO_SCOPES openid profile email
SSO_PROVISIONING existing-only
SSO_DEFAULT_ROLE none
SSO_ROLE_MAP none
SSO_SYNC_PROFILE true; false keeps the app's own name and email
SSO_WORKOS_ID_COLUMN none
SSO_POST_LOGOUT_REDIRECT_URI the home page; must be a sign-out URI on the app in the portal
SSO_HOME /, where staff land when no page was asked for
SSO_GUARD the default guard

php artisan vendor:publish --tag=sso-config publishes the config, and --tag=sso-views the package's two pages.

License

MIT. See LICENSE.md.