Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Php Saml Laravel Package

onelogin/php-saml

onelogin/php-saml adds SAML 2.0 SSO support to PHP applications, including SP- and IdP-initiated login, signed assertions, logout handling, strict mode, and configurable security checks. The 4.x branch supports PHP 7.3+ and PHP 8.x.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pick the right major version. onelogin/php-saml is not a Laravel package — there is no service provider, no facade, no config publish. It is a plain PHP toolkit whose entry point is OneLogin\Saml2\Auth (v4.x, PHP 8+, actively developed under the SAML-Toolkits org but still published as onelogin/php-saml).

composer require onelogin/php-saml:^4.2

Do not use 2.x/3.x on Laravel 10+/11/12 — they target PHP 5.3–7.2. Check composer audit after install; this package pulls robrichards/xmlseclibs, which has had multiple CVEs (signature wrapping, XXE).

Where to look first. Open these in vendor/onelogin/php-saml/lib/Saml2/:

  • Auth.php — the only class most apps touch: login(), processResponse(), logout(), processSLO(), getAttributes(), getNameId(), getErrors(), getLastErrorReason().
  • Settings.php — the array schema; construct it early with your config to fail fast on typos.
  • IdPMetadataParser.php — onboarding an IdP from a metadata URL/file instead of hand-copying certs.
  • Utils.php — setBaseURL(), setProxyVars(), redirect() (note the exit).

Do not copy settings_example.php or the certs/ folder into vendor/. The README explicitly warns these are wiped by composer update. Pass settings as an array, keep keys in storage/app/saml/, and expose them through a config file or a small settings factory.

Your first use case is almost always: publish SP metadata → paste into the IdP → SP-initiated SSO login redirect → ACS callback → create/lookup a local user → Auth::login(). Build that one vertical slice before touching SLO, encryption, or metadata signing.

Minimum viable wiring:

// routes/web.php — keep in `web` for session, but exclude CSRF (see Gotchas)
Route::get('/saml/login', [SamlController::class, 'login'])->name('saml.login');
Route::post('/saml/acs', [SamlController::class, 'acs'])->name('saml.acs');
Route::get('/saml/sls', [SamlController::class, 'sls'])->name('saml.sls');
Route::get('/saml/metadata', [SamlController::class, 'metadata'])->name('saml.metadata');

A SamlSettingsFactory in app/Support/Saml/ that returns the settings array is the single most useful abstraction you can add — it keeps cert loading, tenant resolution, and Utils::setBaseURL() out of controllers.

Implementation Patterns

Build settings from config, not files. Load certs lazily and cache the parsed PEM:

// app/Support/Saml/SamlSettingsFactory.php
public function for(?Tenant $tenant = null): array
{
    $sp = $tenant?->samlSp ?? config('saml.sp');

    return [
        'strict'   => true,
        'debug'    => config('saml.debug'),
        'sp' => [
            'entityId'                 => url('/saml/metadata'),
            'assertionConsumerService' => [
                'url'     => url('/saml/acs'),
                'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
            ],
            'singleLogoutService' => [
                'url'     => url('/saml/sls'),
                'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
            ],
            'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
            'x509cert'     => $this->cert($sp['cert_path']),
            'privateKey'   => $this->key($sp['key_path']),
        ],
        'idp' => [
            'entityId'              => $sp['idp_entity_id'],
            'singleSignOnService'   => ['url' => $sp['idp_sso_url'], 'binding' => '...HTTP-Redirect'],
            'singleLogoutService'   => ['url' => $sp['idp_slo_url'], 'binding' => '...HTTP-Redirect'],
            'x509cert'              => $sp['idp_cert'], // prefer cert over certFingerprint
        ],
        'security' => [
            'authnRequestsSigned'   => true,
            'logoutRequestSigned'   => true,
            'wantAssertionsSigned'  => true,
            'wantMessagesSigned'    => true,
            'signatureAlgorithm'    => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
            'digestAlgorithm'       => 'http://www.w3.org/2001/04/xmlenc#sha256',
            'rejectUnsolicitedResponsesWithInResponseTo' => true,
            'destinationStrictlyMatches' => true,
        ],
    ];
}

Bind it as a singleton and resolve per request: new Auth(app(SamlSettingsFactory::class)->for($tenant)).

Always use $stay = true and let Laravel redirect. Utils::redirect() calls header() then exit — bypassing Laravel middleware, terminate(), and (for file/database drivers) the session save. That is a silent source of "why is my request ID missing on the ACS call".

public function login(Request $request)
{
    $auth = $this->saml->auth();
    $url  = $auth->login(route('dashboard'), [], false, false, true); // stay = true
    $request->session()->put('saml.requestId', $auth->getLastRequestID());

    return redirect()->away($url);
}

This also sidesteps the toolkit's reliance on native $_SESSION — capture the request ID into Laravel's session store instead of trusting $auth->getLastRequestID() on the next request.

ACS: validate, guard against replay, then map attributes.

public function acs(Request $request)
{
    $auth = $this->saml->auth();
    $auth->processResponse($request->session()->pull('saml.requestId'));

    if (! $auth->isAuthenticated()) {
        Log::channel('saml')->warning('SAML auth failed', [
            'errors' => $auth->getErrors(),
            'reason' => $auth->getLastErrorReason(),
            'id'     => $auth->getLastMessageId(),
        ]);
        return redirect()->route('login')->withErrors('Single sign-on failed.');
    }

    // Replay guard: message + assertion IDs must be unique for the assertion lifetime.
    $ttl = now()->addMinutes(15);
    foreach (array_filter([$auth->getLastMessageId(), $auth->getLastAssertionId()]) as $id) {
        abort_unless(Cache::add("saml:seen:{$id}", true, $ttl), 403, 'Replay detected');
    }

    $user = $this->provisioner->fromAssertion(
        $auth->getNameId(),
        $auth->getAttributes(),
    );

    Auth::login($user, true);
    $request->session()->regenerate();
    $request->session()->put([
        'saml.nameId'       => $auth->getNameId(),
        'saml.sessionIndex' => $auth->getSessionIndex(),
    ]);

    return redirect()->intended(route('dashboard'));
}

Metadata endpoint — the only place you construct Settings directly:

public function metadata()
{
    $settings = new Settings($this->saml->settings(), true); // spValidationOnly
    $xml      = $settings->getSPMetadata();
    $errors   = $settings->validateMetadata($xml);
    abort_if($errors, 500, implode(', ', (array) $errors));

    return response($xml, 200, ['Content-Type' => 'application/xml']);
}

Onboarding an IdP — don't hand-copy certs; use the parser and cache the result:

$parsed  = IdPMetadataParser::parseRemoteXML($tenant->idp_metadata_url);
$settings = IdPMetadataParser::mergeSettings(config('saml'), $parsed);
Cache::forever("saml:idp:{$tenant->id}", $settings['idp']);

SLO — you must invalidate the Laravel session yourself; processSLO() only validates the SAML message:

public function sls(Request $request)
{
    $auth = $this->saml->auth();
    $url  = $auth->processSLO($request->session()->pull('saml.requestId'), true, null, true);

    Auth::logout();
    $request->session()->invalidate();
    $request->session()->regenerateToken();

    return $url ? redirect()->away($url) : redirect()->route('login');
}

To initiate logout, pass $sessionIndex (stored at login) or the IdP may not terminate the right session: $auth->logout(null, [], $session['saml.nameId'], $session['saml.sessionIndex'], true).

Attribute mapping belongs in your own class, not a controller. IdPs emit either friendly names (mail, memberOf) or full URIs (urn:oid:0.9.2342...), and single-value vs multi-value inconsistently. Normalize to a DTO (SamlIdentity) once, then have findOrCreate/link logic behind an interface so you can unit-test it without a real SAML response.

Testing. In TestCase::setUp() call Utils::setBaseURL('https://app.test') — otherwise getSelfURL() reads $_SERVER and metadata/ACS URLs drift per environment. Test your mapping layer directly; for end-to-end, run a real IdP (SimpleSAMLphp in Docker, or an IdP sandbox) against a staging ACS rather than hand-forging signed assertions.

Gotchas and Tips

exit() in Utils::redirect(). The single biggest Laravel pitfall. $auth->login() and $auth->logout() redirect-and-exit by default. Always pass $stay = true and return a Laravel redirect. This also preserves Session::save() and any terminating middleware.

Native $_SESSION vs Laravel's session. Auth stores the request ID, NameID, and session index in $_SESSION, which Laravel's file/redis/database drivers never populate. Don't rely on getLastRequestID() across requests — capture it immediately after login()/logout() into Laravel's session and feed it back into processResponse($id) / processSLO($id). If you must use native sessions, call session_name('saml_session') + session_start() in a dedicated middleware so you don't fight Laravel's cookie.

CSRF. Exclude saml/acs and saml/sls in bootstrap/app.php:

->withMiddleware(fn (Middleware $m) => $m->validateCsrfTokens(except: ['saml/acs', 'saml/sls']))

Proxy and URL detection. Behind a load balancer or in a subdirectory, Utils::getSelfURL() guesses wrong, and destinationStrictlyMatches then rejects a perfectly valid response. In AppServiceProvider::boot(): Utils::setBaseURL(config('app.url')); Utils::setProxyVars(true); — but only after TrustProxies is configured correctly.

strict must stay true, even in dev. Setting it false hides signature, destination, and audience failures that only appear in production. If an IdP won't validate, fix the cert/metadata; don't loosen strict. Also avoid the fingerprint method in production (collision risk) — use the full IdP cert.

post_max_size and large assertions. IdPs with large memberOf/group lists can exceed post_max_size, producing an empty $_POST and a confusing "Invalid SAML Response" with no error detail. Check php -i | grep post_max_size before debugging signatures.

Octane. php-saml reads $_POST, $_SERVER, and $_SESSION directly. Long-lived workers can leak state between requests and native sessions behave unpredictably. Safest: route SAML endpoints to a non-Octane worker, or at minimum reset $_POST/$_SESSION in a middleware and write an integration test that hits login → ACS twice.

Don't process SAML in jobs, commands, or artisan. Utils needs HTTP_HOST, REQUEST_URI, SERVER_PORT, and HTTPS. Queue-based "process the response later" designs break on destination validation.

Debugging checklist, in order: $auth->getErrors() → $auth->getLastErrorReason() → $auth->getLastErrorException() → compare getLastRequestXML() with getLastResponseXML(). Log these to a dedicated saml channel (daily, restricted permissions) — the XML contains PII and should never land in your default stack channel or in Sentry payloads.

Clock skew. NotBefore/NotOnOrAfter are strictly validated; a container drifting a few minutes from the IdP produces intermittent "assertion not yet valid" failures. Run NTP. Also verify the Destination and Audience values in the IdP config exactly match url('/saml/acs') and url('/saml/metadata') — trailing slashes and http vs https are the usual culprits.

RelayState is attacker-controlled. It is not integrity-protected on HTTP-POST. Never pass a raw user-supplied URL as $returnTo. Either omit it and use redirect()->intended() from Laravel's session, or whitelist paths (parse_url($url, PHP_URL_HOST) === parse_url(config('app.url'), PHP_URL_HOST)).

NameIDFormat mismatches. Some IdPs reject an AuthNRequest advertising the default unspecified. Set sp.NameIDFormat explicitly (emailAddress is the most compatible), and use the setNameIdPolicy parameter of login() when the IdP demands a policy element.

lowercase_urlencoding. Certain IdPs (notably some ADFS/Azure configurations) require lowercase percent-encoding in redirect binding signatures. If signature validation on the redirect binding fails only against one IdP, try 'security' => ['lowercase_urlencoding' => true] before assuming a cert problem.

Unsolicited (IdP-initiated) responses. With rejectUnsolicitedResponsesWithInResponseTo => true, any response lacking InResponseTo is rejected. Only enable IdP-initiated SSO deliberately — it's a known SAML attack surface — and if you do, keep the requestId you pass to processResponse() as null and apply the replay guard aggressively.

SLO is best-effort. Not every IdP redirects back to your SLS endpoint, and some send LogoutRequest without a sessionIndex. Treat SLO as "the IdP logs the user out of its session"; your app must still expire its own session (and consider a short session.lifetime plus re-validation against the IdP for sensitive routes).

Extension points. Subclass OneLogin\Saml2\Auth if you need custom session keys or logging — most other behavior is fine wrapped in a service class. For advanced flows, drop to OneLogin\Saml2\AuthRequest, LogoutRequest, LogoutResponse, Metadata::builder(), and Response directly; they accept explicit settings objects and avoid superglobals, which makes them far easier to test.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names