Search by

sinemacula / laravel-sso

sinemacula

Tenant single sign-on over SAML 2.0 and OpenID Connect for Laravel 12 and 13: connections, verified domains, enforcement, linking and a verified handoff.

Package info

github.com/sinemacula/laravel-sso

pkg:composer/sinemacula/laravel-sso

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 2

dev-master 2026-11-03 22:39 UTC

This package is auto-updated.

Last update: 2026-11-03 23:41:43 UTC


README

Latest Stable Version PHP Version Laravel Version Build Status Quality Gates Maintainability Code Coverage Total Downloads License

Tenant single sign-on for Laravel, over SAML 2.0 and OpenID Connect. Each tenant signs its people in through its own identity provider, and the package hands the application a verified, single-use handoff to sign them in with.

Works on its own, or alongside the rest of the Sine Macula identity and access packages. It ships no routes, controllers or views: the application's own endpoints call one helper each, and the package never issues the application's tokens or creates its sessions.

Note

The package is under active development and has no release yet. The features below describe its intended scope.

Core Concept

Concept What it is
Connection One tenant's trust in one identity provider, over SAML 2.0 or OpenID Connect.
Verified domain An email domain a tenant has proven it controls, which routes discovery to its connection.
Verified result The issuer, subject and connection of a completed sign-in, produced only after every check.
Link The stored association from a connection's issuer and subject to an identity.
Handoff code A single-use, short-lived code the package issues, stores and redeems, bound to the client's flow.

A flow has three steps, each one helper call from a route of your own: start (an authorisation request, answered with where to send the browser), complete (at the assertion consumer service or the callback, answered with a redirect carrying a handoff code) and redeem (the code and the client's verifier, inside your transaction, answered with the identity to sign in).

Features

  • SAML 2.0 and OpenID Connect connections per tenant, with metadata and discovery import, scheduled refresh, presets for Google Workspace, Microsoft Entra ID, Okta and Auth0, key and certificate rotation, and secrets encrypted at rest.
  • Hardened validation: schema-checked SAML with signature-wrapping defences, replay protection and optional encrypted assertions; ID tokens with pinned algorithms, exact key selection and PKCE.
  • Verified domains and discovery: DNS TXT verification with scheduled rechecks, and routing an email address to its tenant's identity provider without revealing whether the person has an account.
  • SSO-only enforcement per tenant, with break-glass exemptions and a synchronous revocation hook.
  • Linking by issuer and subject, through an explicit link flow, just-in-time provisioning, or a verified domain for moving a tenant's existing members.
  • A verified handoff: a single-use code the package issues, stores and redeems, bound to the client that started the flow and to the endpoint's purpose, admitted inside your own transaction.
  • Re-authentication at the identity provider for a session, and the provider's authentication context offered to your further-factor policy, never accepted by default.
  • Session provenance: how a session began (its connection, tenant, protocol, accepted evidence, link and provider deadline), captured inside the redemption and projected into stable sso.* authentication context keys.
  • IdP-initiated sign-in by re-initiation, never by consuming an unsolicited assertion.
  • Events for every flow, failure and administrative change, with one shared context and never a secret.
  • Operations: pruning, domain rechecks, metadata refresh and key expiry warnings on the schedule, safe on several servers, with read-replica and Octane safety.
  • Testing helpers: fake SAML and OpenID providers, a fake DNS resolver and fetcher, fake freshness and a frozen clock, so your flows run in tests without a network.
  • Localisable messages: the pages a completion shows translate through your lang files, and every other answer is a code your application words.
  • Extension points: every policy is a contract with a default that fails closed, which laravel-iam binds to laravel-authentication and laravel-mfa.

Installation

The SAML library installs its XML schemas through a Composer plugin, so allow it first:

composer config allow-plugins.simplesamlphp/composer-xmlprovider-installer true
composer require sinemacula/laravel-sso

Configuration

Publish the config and the migrations, and migrate:

php artisan vendor:publish --tag=sso-config
php artisan vendor:publish --tag=sso-migrations
php artisan migrate

Every feature is off until you turn it on, but for the scheduled refresh of what connections fetch from their identity providers, which has nothing to do until a protocol is on. Turn on a protocol, set the base URL every endpoint is built from (with Laravel's /api prefix when your routes live in routes/api.php), and register the applications people sign into:

SSO_OIDC=true
SSO_SAML2=true
SSO_BASE_URL=https://api.example.com/api
// config/sso.php
'applications' => [
    'web' => [
        'redirect_uri'     => 'https://app.example.com/auth/sso/complete',
        'external_sign_in' => true,
    ],
],

Check it, and register the package's scheduled work on the application builder in bootstrap/app.php, importing Illuminate\Console\Scheduling\Schedule and SineMacula\Laravel\Sso\Operations\OperationsSchedule; metadata refreshes need a queue worker:

php artisan sso:check
// bootstrap/app.php
->withSchedule(function (Schedule $schedule): void {
    app(OperationsSchedule::class)->register($schedule);
})

See Installation for the extensions, the SAML library's limits and the contracts to bind, and Configuration for every option.

Usage

Start a flow from a route of your own, with the purpose fixed by the endpoint:

/** @var array{application: string, connection?: string, email?: string, code_challenge?: ?string} $input */
$input = Validator::validate($request->all(), [
    'application'    => ['required', 'string'],
    'connection'     => ['required_without:email', 'string'],
    'email'          => ['required_without:connection', 'string', 'max:320'],
    'code_challenge' => ['nullable', 'string'],
]);

$challenge = $input['code_challenge'] ?? null;

// The purpose is this endpoint's: a `purpose` in the body is never read
$result = isset($input['email'])
    ? $this->discovery->start($input['application'], Purpose::SIGN_IN, $input['email'], $challenge)
    : $this->requests->start($input['application'], Purpose::SIGN_IN, $input['connection'] ?? '', $challenge);

Complete it where the identity provider sends the browser back:

$result = $this->completions->assertionConsumerService($connection, CompletionRequest::fromRequest($request));

return $this->responses->from($result, $request->getMethod());

Redeem the handoff inside your own transaction, write the session in it, and send a credential only after it commits:

/** @var array{application: string, connection: string, code: string, verifier: string} $input */
$input = Validator::validate($request->all(), [
    'application' => ['required', 'string'],
    'connection'  => ['required', 'string'],
    'code'        => ['required', 'string'],
    'verifier'    => ['required', 'string'],
]);

// The code is consumed and the session written in one transaction, on
// the package's storage connection
[$result, $sessionId] = DB::connection(StorageConnection::name())->transaction(function () use ($input): array {
    $result = $this->redemptions->admit(Purpose::SIGN_IN, $input['application'], $input['connection'], $input['code'], $input['verifier']);

    // Active, or pending your own further factor; an unlinked handoff
    // signs nobody in
    $sessionId = $result->isRedeemed() && $result->linkedIdentity !== null ? $this->signIns->start($result) : null;

    return [$result, $sessionId];
});

// Only now, after the commit, does a credential leave
return $sessionId === null
    ? self::refusal($result)
    : response()->json($this->signIns->credential($sessionId), 200, Answers::NO_STORE);

An example application lays out every route, with its middleware, its authorisation and the client's part, from code the test suite runs.

Guides

Guide Covers
Installation Composer, extensions, migrations, the SAML library's limits, the schedule
Configuration Every configuration key, endpoints, applications and the boot check
An example application One coherent route layout, middleware, authorisation and the client's part
Setting up an identity provider Registration fields, with Microsoft Entra ID, Okta and Google Workspace
Opt-in features and their risks Every flow and opt-in, what turning it on means, and what it costs
Starting a flow The start helper, purposes and the client's duties
Completing a flow The completion helper, unlinked sign-ins and the responses it answers
Redeeming a handoff admit() and redeem(), what a redemption checks, transactions
Links The link store, the link purpose, confirmation, listing and removal
Domains and discovery Verifying a domain, and routing an email address to its connection
Enforcement SSO-only tenants, exemptions and revoking sessions
Events Every event, its context and the failure reasons
Operations The schedule, several servers, queues, pruning and key expiry warnings
Re-authenticating The reauthenticate purpose and the sessions contract it needs
Session provenance What a session keeps from its sign-in, and the sso.* context keys
Testing The fake identity providers, DNS resolver, fetcher, freshness and clock
Integrating with laravel-iam Every seam, the transaction rule and the adapter's obligations

Every guide, by task, is in the guides index. The security and lifecycle contracts behind them, for maintainers and reviewers, are in docs/design.

Requirements

  • PHP ^8.3, with the bcmath, curl, dom, hash, libxml, mbstring, openssl and zlib extensions
  • Laravel ^12.40 || ^13.3

Testing

composer test               # all suites in parallel (Paratest)
composer test:coverage      # all suites with clover coverage
composer test:unit          # unit suite only
composer test:feature       # feature suite only
composer test:integration   # integration suite only
composer test:performance   # performance budget suite (serial)
composer test:mutation      # scoped mutation gate
composer test:mutation:full # full mutation suite (no thresholds)
composer bench              # PHPBench hot-path benchmarks
composer check              # static analysis + style
composer format             # format the codebase
composer smells             # advisory code smells (duplication, complexity)

Changelog

See CHANGELOG.md for a list of notable changes.

Contributing

Contributions are welcome. Please read CONTRIBUTING.md for guidelines on branching, commits, code quality, and pull requests.

Security

If you discover a security vulnerability, please report it responsibly. See SECURITY.md for the disclosure policy and contact details.

License

Licensed under the Apache License, Version 2.0.