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

Steam Authentication Bundle Laravel Package

knojector/steam-authentication-bundle

A Symfony bundle for authenticating users through Steam OpenID. It simplifies adding Steam login to Symfony applications, handling the authentication flow so you can integrate Steam-based sign-in with minimal configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

Read it, don't require it. This is a Symfony bundle — Bundle subclass, DI extension, config tree, security authenticator wired into Symfony’s firewall. Laravel’s package manifest and container will never boot it, and its symfony/* constraints will fight the versions Laravel already pins. Before writing a single route, run the sanity check:

composer require --dry-run knojector/steam-authentication-bundle

Expect a conflict. Then go read the source in vendor/ (or on GitHub). The four files that matter:

  1. DependencyInjection/Configuration.php — the full option surface (realm, return route, firewall wiring). This is your spec for what to reimplement.
  2. The authenticator / firewall factory — shows how it plugs into auth.
  3. The controller or listener that builds the redirect to https://steamcommunity.com/openid/login.
  4. The validator that POSTs the assertion back for check_authentication. This is the part you actually want. It’s the ~100–200 lines of OpenID 2.0 protocol logic that’s painful to get right and framework-agnostic underneath the Symfony types.

First use case: feature-flagged "Sign in with Steam." Two routes, one validator, one controller — nothing more:

GET /auth/steam           -> redirect to Steam OpenID endpoint
GET /auth/steam/callback  -> validate assertion, Auth::login(), redirect

Your callback’s only job is to produce a verified steam_id_64. Persist it, look up or create the user, log in. Profile data (avatar, name, owned games) is not in the OpenID assertion — that requires a separate Steam Web API key and GetPlayerSummaries. Treat enrichment as a separate, degradable step.

If you want "just make it work" today, the default recommendation is laravel/socialite + socialiteproviders/steam: Socialite::driver('steam')->user()->getId() is your SteamID64 in a 30-minute integration. Come back to this bundle’s validator only if that driver mishandles something (e.g. weak signature or claimed_id checks) — port the logic, don’t install the package.

Local dev note: Steam OpenID requires a publicly reachable return_to over HTTPS. Use a tunnel (ngrok, expose) or a staging host; do not try to validate against http://localhost.

Implementation Patterns

Port the validator, not the bundle. Wrap it in a plain class with an injected HTTP client so it's testable and framework-free:

final class SteamOpenIdValidator
{
    public function __construct(private Http $http, private string $realm) {}

    public function validate(array $query): string // returns verified steam_id_64
    {
        // 1. POST the full query back with openid.mode=check_authentication
        $response = $this->http->asForm()->timeout(5)->post(
            'https://steamcommunity.com/openid/login',
            $query + ['openid.mode' => 'check_authentication']
        );

        // 2. Require is_valid:true
        abort_unless(str_contains($response->body(), 'is_valid:true'), 403);

        // 3. Re-check the things the response is *supposed* to echo
        //    - op_endpoint === steamcommunity.com/openid/login
        //    - claimed_id / identity prefix === steamcommunity.com/openid/id/
        //    - return_to === url('/auth/steam/callback')   (exact match)
        //    - nonce/assoc_handle within freshness window

        // 4. Extract
        preg_match('#/id/(\d{17})$#', $query['openid.claimed_id'], $m);
        abort_unless(isset($m[1]), 403);
        return $m[1];
    }
}

Bind it as a singleton in a SteamAuthServiceProvider and pull config from config/services.php. Every check in step 3 is a real vulnerability if skipped — trusting the raw claimed_id query param is account takeover.

Persistence. Keep the SteamID64 as a string end-to-end. CHAR(17) or VARCHAR(17) with a unique index in a social_accounts table (provider = 'steam', provider_user_id, user_id). Never BIGINT that flows into JSON — 7656119… exceeds Number.MAX_SAFE_INTEGER and silently corrupts in the frontend.

Schema::create('social_accounts', function (Blueprint $t) {
    $t->id();
    $t->foreignId('user_id')->constrained()->cascadeOnDelete();
    $t->string('provider', 20);
    $t->string('provider_user_id', 64);   // SteamID64 stays a string
    $t->json('meta')->nullable();          // cached profile summary
    $t->timestamps();
    $t->unique(['provider', 'provider_user_id']);
});

Controller flow. Validate → resolve/create/link user → Auth::login() → $request->session()->regenerate() → redirect to intended(). Regenerate after login, always; a captured callback URL is otherwise a session-fixation vector. Put the whole flow behind a steam_login feature flag (Pennant/Laravel config) so you can kill it without a deploy.

Account linking. Decide up front: if a SteamID64 is new but the user already has an email account, do you prompt, auto-link, or reject? Don't guess at runtime. The clean pattern is: if Auth::check() on callback, link to the current user; otherwise find by social_accounts.provider_user_id, else create a fresh user (or send them through a "link your existing account" form). Duplicate accounts on an already-linked SteamID64 must be a distinct, logged error, not a 500.

Profile enrichment (Steam Web API, separate concern).

GET https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key=KEY&steamids=7656...

Cache for ~15 min (Cache::remember), and consider a queued job rather than doing it synchronously on login. Make every field nullable — private profiles return limited data.

Socialite path. If you skip the port: composer require laravel/socialite socialiteproviders/steam, register the provider in AppServiceProvider, add credentials to config/services.php, and reuse Socialite’s route patterns. A custom driver is worth it if you want stricter return_to checking or to log raw assertions for debugging.

Testing. Mock the HTTP client with a canned is_valid:true body and the query you sent in. Then assert the negative cases explicitly: tampered claimed_id → 403; is_valid:false → 403; wrong return_to → 403; stale nonce → 403. These are your regression guardrails — one test per check.

Gotchas and Tips

Symfony ≠ Laravel. Don't composer require it into a Laravel app and expect anything to boot. The bundle's Extension expects a Symfony ContainerBuilder and Kernel. Even if Composer resolves, nothing runs.

Version pins will hurt. The bundle constrains symfony/* and PHP versions from the Symfony 4/5 era. Laravel pins HttpFoundation, HttpKernel, Routing, Translation transitively. You'll get a resolvable-looking conflict at install time that fails at runtime. Fork to own the constraint surface.

OpenID 2.0 is a permanent constraint. Steam has never shipped OAuth2 and shows no sign of it. Any solution here is a legacy-spec shim you maintain forever. Document that explicitly so it isn't mistaken for "just another OAuth provider."

Never trust openid.claimed_id from the query string. The entire security model collapses to that one check. Verify the assertion server-side via the check_authentication POST; treat the returned is_valid:true line as necessary but not sufficient — also pin op_endpoint, exact return_to, and nonce freshness.

return_to exact-match breaks behind proxies. Behind a load balancer, TLS terminator, or container, $request->url() may be http:// or an internal host. The bundle (and reimplementations) match return_to byte-for-byte. Force HTTPS URL generation, configure TrustProxies, and generate the callback URL from URL::to(...) / APP_URL, not from the request.

SteamID64 is not a JS integer. This is the #1 silent corruption bug. 76561198000000000 serializes fine in PHP but loses precision in JS. Keep it a string in API responses, in Eloquent casts, and in the migration. If you expose it as BIGINT and echo it in an Inertia/Livewire payload, you'll ship wrong IDs for months before anyone notices.

Timeout the validation call. steamcommunity.com is on your login critical path and is synchronous. Set a hard client timeout (~5s) and fail closed with a friendly error page. Steam outages otherwise hang workers.

Replay defense. Ensure the nonce/assoc_handle is single-use and short-lived, and regenerate the session on successful login. A captured callback URL replaying is a real attack surface on an unmaintained validator.

No profile data in the assertion. Nothing from the OpenID response gives you the player's name, avatar, or games. Any real "Steam integration" needs a Web API key, a second HTTP call, and its own error handling. Scope Steam login as auth plumbing, not integration.

Web API rate limits. Default is 100k calls/day per key. Cache GetPlayerSummaries output for ~15 minutes, batch the IDs, and degrade gracefully — private profiles return sparse data, which must map to nulls, not exceptions.

Config quirks you'll reimplement. The bundle's tree (realm, return_to route, firewall name) becomes config/services.php entries in your port. Keep the same key names to reduce confusion when you diff against upstream.

Debugging the callback. Temporarily log the raw query string (scrub openid.assoc_handle/sig) and the is_valid line from Steam. In 90% of "login fails" reports, it's return_to mismatch or op_endpoint drift — the raw capture shows it in seconds.

Extension points. On Symfony, the authenticator is the swap point for a custom user provider. In your port, that's SteamOpenIdValidator (injectable HTTP client) and the user-resolution service. Keep them separate so you can test validation without touching persistence.

Maintenance reality. Zero stars, zero dependents, single maintainer. Treat the fork as permanently internal: pin the commit, record the fork point, keep a diff for cherry-picks if upstream revives, and add a runbook entry for "Steam auth is down" with a documented fallback (email/password or another provider) so the Steam cohort isn't locked out.

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.
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
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings