Leandro Pareja Get in touch

Project / Authentication & API authorization

From platform identity to player data.

My Steam and EOS authentication research. Reported & patched.
Leandro Pareja · C# · Application security

The research in brief

I discovered and reported an information-exposure issue in a multiplayer game’s online services. My C# research client traced how Steam authentication led to access to session and player information. After my report to Studio Wildcard, checks were added to validate the requesting client ID, and the affected requests now return HTTP 403 Forbidden.

  • My role: C# development, authentication-flow analysis, API testing, and responsible disclosure.
  • Why it mattered: Proving a player’s identity should not automatically give a client access to player information beyond its intended permissions.
  • Outcome: The affected access path was patched. Requests from my research client are now rejected with HTTP 403 Forbidden.

Disclosure & patch status

After my report, checks were added to validate which client ID was requesting the information. The affected requests from my research client now return HTTP 403 Forbidden instead of the previously accessible information. The exchange flow and C# example below document my original research, before the patch.

Technical context

My client connected Steam authentication to Epic Online Services (EOS). It obtained a platform-issued session ticket—proof used to verify a Steam account—then exchanged it for an EOS access token and product user ID. It used that authenticated context to look up sessions and request player details. I also added polling, persistence, and Discord notifications to compare observations over time.

The intended security model

The workflow crosses three distinct boundaries:

  1. Steam identity: a platform-issued ticket provides material that a relying service must validate through the appropriate Steam integration.
  2. EOS identity and permissions: the exchange returns credentials for EOS operations in a configured application environment. An identifier alone is not proof of identity, and a valid token is not universal permission.
  3. Application data access: each read must satisfy the product’s policy for the requested session, target player, and response fields. Public session discovery and privileged account enrichment are different operations.

Steam distinguishes game-session ticket verification from its Web API ticket flow. Its documentation describes backend verification and validation callbacks; a local handle or initial validity check does not substitute for the appropriate verification result. The Web API flow uses GetAuthTicketForWebApi, waits for its response callback, and validates the ticket on a secure backend. These ticket types should not be treated as interchangeable. Steamworks: authentication and ownership.

EOS client policies define which services and actions a client may access. Epic explicitly warns against distributing credentials associated with a trusted-server policy in a public game build. The effective policy—not a recognizable SDK header—is the meaningful permission boundary. Epic: clients and client policies.

How I connected the exchange to player data

StageImplementation evidenceWhat it establishes
Platform loginSteamClientExample uses SteamKit2 callbacks to connect and log on.The implemented path starts with Steam account authentication; it is not an anonymous client.
Ticket acquisitionGetFreshTicketCtx requests an application ticket and returns its hex representation with a disposable context.The client obtains ticket bytes and retains their context during the subsequent operations.
Identity exchangeEpicGamesAuth.GetEosAuthTokenAsync sends a form-encoded external-auth request, identifying a Steam session ticket, to an EOS token endpoint.The code attempts an identity exchange using configured client authentication and deployment context.
Token consumptionProgram.InitialPlayerListUpdate and FetchAndPrintMatchmakingData extract access_token and product_user_id from the exchange response.The active callers use the returned product user ID rather than demonstrating substitution of an arbitrary player ID.
Session queryGetMatchmakingDataAsync sends a bearer-authenticated request to a deployment-scoped player-session filter.The client attempts a session lookup associated with that principal.
Player expansionParsePlayers collects publicPlayers identifiers; GetUserDetailsAsync submits them to a product-user search operation.Session membership information becomes input to a second data lookup.
Enrichment and outputParsePlayerDetails reads account identifiers, display names, provider identifiers, and last-login fields. Program contains persistence and notification logic.The intended downstream use correlates identity and session observations over time.

The routes my client uses include an OAuth token exchange, a matchmaking player filter, and a product-user search. Exact deployment identifiers, credentials, account values, and reusable request bodies are intentionally omitted. These route descriptions document the implementation; they are not a claim that every observed route is a supported public API contract.

The code requests a hexadecimal ticket representation. Hex encoding changes representation; it neither creates an identity nor makes a ticket authentic. Likewise, the copied SDK user-agent, correlation header, and version headers describe request metadata. Their presence does not establish that the service uses those headers as authentication factors.

Root cause analysis: identity is not permission

1. The candidate boundary failure is authorization after authentication

The client first proves a platform identity, then asks for session and player information. The central question is whether the resulting principal is permitted to obtain each returned object and attribute.

If an ordinary player credential can retrieve account-level details beyond the product’s intended visibility rules, the failure belongs at the authorization or response-disclosure boundary. Possible causes include an overly broad client policy, missing object-level checks, or excessive response fields. My client alone cannot identify which of those controls is actually responsible: it contains neither the effective policy nor the responding service’s validation logic.

The post-patch client-ID checks and HTTP 403 responses are consistent with enforcing authorization at this boundary. They do not reveal the exact internal defect or establish whether the change was made in a client policy, a service handler, or another control. A successful authenticated read can still be legitimate when the requesting client and principal have the required permissions.

2. Public session discovery does not justify every enrichment operation

ParsePlayers explicitly reads publicPlayers. That name alone does not prove a privacy violation. Epic documents public-advertised sessions that other users can discover through registered-player searches. Discoverability may be expected behavior. Epic: sessions introduction.

The more consequential boundary is the transition from discovery to account correlation: which linked identifiers and timestamps may the requesting principal obtain, and for what purpose? Those decisions need field-level and object-level policy. My source shows the lookup chain, not the policy that governed an actual response.

3. Polling can amplify a permitted or excessive disclosure

The monitoring loop waits ten seconds after each iteration. It compares player-ID sets between snapshots, prepares join/leave notifications, and updates session records. PlayerInfo models persistent account and session-history fields.

Repeated collection can turn a single lookup into a longitudinal activity record. This is a potential impact multiplier if the reads are overbroad; it is not proof of the collection’s deployed scale. Snapshot differences also measure observed presence between polls, not exact authoritative join or leave times. Missing or incomplete data must not be interpreted as a confirmed departure.

Implementation findings in the research client

While revisiting my implementation, I also identified improvements I would make to the research client itself. These are separate from the service-side authorization question.

Credential handling. My original source embeds platform credentials, a bot credential, and an EOS client-authentication value. Treat active account credentials as exposed and rotate or revoke them. Classify the EOS value by its actual client policy: a distributed client credential is not a confidential server secret and must not grant trusted-server authority. Credential literals, tokens, player identifiers, and destination IDs are excluded from this article.

Readiness and ticket lifecycle. Main starts the Steam callback loop without awaiting an explicit logged-on readiness signal before starting the rest of the application. That creates a potential readiness race. The active polling path holds the ticket context in a using block, which scopes normal disposal, but forced process termination cannot guarantee that cleanup completes. SteamAuth.cs is an alternative wrapper whose initialization is commented out in Program; it cancels the current handle on entry and logs its callback result rather than gating ticket consumption on it. These are lifecycle concerns, not evidence of backend acceptance of invalid tickets. The wrapper’s 55-minute threshold is a local heuristic, not an established platform ticket lifetime.

Response and failure handling. The token and session helpers return null for several failures. The main loop can retry session reads and terminate the process on token failure. Failed reads stop that cycle; the code does not show a fail-open authentication fallback. However, authentication failure, permission denial, transport errors, and malformed responses need separate handling so outages do not become misleading security conclusions.

Data association. ParsePlayers aggregates identifiers across returned sessions while repeatedly overwriting a single sessionData dictionary. Later code uses the first session name in places. With multiple sessions, metadata and player sets can therefore be associated inconsistently. Use a session-keyed model; do not infer a player’s exact server solely from the current aggregate representation.

HTTP and logging hygiene. The helper creates an HttpClient for each request and builds form data through string interpolation. Prefer a managed client lifetime, cancellation and bounded timeouts, and a form encoder. Commented diagnostic statements would expose tokens and raw bodies if enabled. Replace them with redacted error categories, correlation IDs, and bounded retention for player information.

My exchange flow: a conceptual C# proof of concept

This walkthrough uses the class and method names discussed in my report. Start with Program.cs, then choose a helper file on the right to follow the exchange, ParsePlayers, and player-detail parsing. These are edited illustrations of the reviewed code, with simplified signatures and synthetic schemas; they are not verbatim original files. Platform and HTTP integrations are omitted.

01 Establish a test identity

  1. PlatformSteam test identityA ticket is acquired for an authorized test account.
  2. Service boundaryIdentity exchangeThe service validates credentials under its configured policy.
  3. PrincipalAuthenticated contextIdentity and permissions remain separate questions.

02 Compare access with policy

  1. DiscoverySession lookupReturn only the permitted synthetic session references.
  2. EnrichmentPlayer-detail readCheck every target and approved response field.
  3. AssessmentPolicy comparisonExpected visibility or a candidate information exposure?
Conceptual, non-functional review flow. All identities and records are synthetic; no live request or bypass payload is supplied.
Program.csDownload file

The exchange, session lookup, and player parsing in one place. Start here, then open the helpers on the right.

using System;
using System.Text.Json;
using System.Threading.Tasks;

// Edited public walkthrough. Names follow the reviewed research client.
// Live authentication, collection, persistence, and notifications are omitted.
internal static class Program
{
    public static async Task InitialPlayerListUpdate()
    {
        using var steamClient = new SteamClientExample();
        var (ctx, hex) = steamClient.GetFreshTicketCtx();
        using (ctx)
        {
            using var authResponse = await EpicGamesAuth.GetEosAuthTokenAsync(hex);
            if (authResponse is null) return;

            string accessToken = authResponse.RootElement
                .GetProperty("access_token").GetString()!;
            string productUserId = authResponse.RootElement
                .GetProperty("product_user_id").GetString()!;

            using var matchmakingData = await MatchmakingService
                .GetMatchmakingDataAsync(productUserId, accessToken);
            if (matchmakingData is null) return;

            var (players, sessionNames, sessionData) =
                MatchmakingService.ParsePlayers(matchmakingData);

            using var userDetails = await MatchmakingService
                .GetUserDetailsAsync(players, accessToken);
            if (userDetails is null) return;

            var playerDetails = MatchmakingService.ParsePlayerDetails(userDetails);
            Console.WriteLine($"Parsed {playerDetails.Count} synthetic players.");
        }
    }
}

For this type of test, compare the returned objects and fields with the documented permissions for the requesting identity and client. A successful login alone does not establish that the subsequent data access is authorized.

Defensive remediation

Patch 1: establish identity on the authoritative side

Select the provider-supported verification flow for the credential being presented. For Steam session authentication, process the applicable backend verification result and lifecycle callbacks. For a Steam Web API backend, use its corresponding Web API ticket flow. An EOS access token used to call a service should not be assumed to be the same credential type as an EOS Connect ID token presented to a game server.

Where EOS Connect ID tokens are appropriate, use the supported verification mechanism, such as EOS_Connect_VerifyIdToken, and bind the verified subject and application context to the connection. Epic documents Connect ID-token verification for verifying another EOS user’s identity. Do not infer the token format or verification method for the reviewed OAuth response merely from its field name. Epic: EOS SDK release notes, Connect ID-token APIs.

Patch 2: enforce authorization per operation and object

For each read, evaluate the verified subject, permitted action, deployment context, target object, and the product’s intended visibility rules. Derive the caller from verified credentials rather than a request-supplied identity. Public discovery, authorized co-player visibility, and administrative player lookup should have explicitly different permissions where the product requires them.

Keep privileged EOS clients on trusted infrastructure and restrict their policies to the required actions. Use a user-required player policy where appropriate. Review any distributed client credential as recoverable, and reduce its authority accordingly. Rotating a broad client credential without fixing its permissions does not repair the trust boundary.

Patch 3: minimize the response

Return only fields the caller needs. Avoid exposing linked external account IDs, login timestamps, or historical activity simply because a player appears in a public session. Apply the approved field projection after authorization and before serialization. If a lookup needs several targets, validate every target and avoid partial-response leakage.

Patch 4: treat validation failure as a denied sensitive operation

An absent or inconclusive verification result must not be treated as a verified identity for protected reads. Keep unverified connections in a limited state where appropriate. Deny the sensitive operation on invalid or expired credentials, wrong application context, or insufficient permissions; do not silently fall back to an anonymous read. Return structured failures without raw credentials or unnecessary account details.

CONCEPTUAL SERVER POLICY — not an executable patch

subject := VERIFY_WITH_APPROPRIATE_PROVIDER_MECHANISM
if verification is invalid or inconclusive:
    DENY_PROTECTED_READ

if subject.context is not EXPECTED_APPLICATION_CONTEXT:
    DENY_PROTECTED_READ

for each requested object:
    if POLICY does not permit subject to read that object:
        DENY_WITHOUT_DISCLOSING_OBJECT_DETAILS

return only POLICY_APPROVED_FIELDS

This identifies where a patch belongs; it is not a drop-in patch for an unseen server implementation.

Patch 5: contain collection and test the boundary

Rate-limit by principal, client, and operation; bound batch sizes; record unusual target expansion and repetitive polling. Protect downstream exports with channel access controls, retention limits, and deletion rules. Use session-keyed observations and distinguish stale, partial, and failed snapshots from confirmed state changes.

Authorized regression caseRequired result
Missing, malformed, expired, or cancelled credential, where applicable to that credential typeNo protected player data; cancellation and expiry enforced according to provider semantics.
Valid identity with wrong deployment or application contextDenial before reading protected objects.
Ordinary identity requesting an unrelated private player or sessionDenial under the documented visibility policy.
Valid permitted public discoveryOnly the intended discoverable session fields.
Mixed authorized and unauthorized batch targetsEvery target checked; no unauthorized fields leaked.
Verification timeout or provider outageSensitive reads remain unavailable; failure is distinguishable from an empty result.
Token revocation or session terminationAccess ceases according to the provider’s lifecycle and documented application policy.
Partial monitoring snapshotMark uncertain; do not write fabricated join or leave times.

Conclusion

My main takeaway is that platform identity, EOS permissions, and player-data visibility must be assessed as separate boundaries. My client documents an authenticated path from one account to session references and potential account enrichment. I reported the issue, and the affected access path has since been patched: the service now checks the requesting client ID, and my research client receives HTTP 403 Forbidden for the affected requests.

For developers, the corrective direction is concrete: verify identity with the correct provider mechanism, enforce the intended object and field permissions on the server, keep privileged credentials off player-controlled systems, and reject sensitive reads when validation is unresolved.

Research scope and evidence

This report describes my authenticated information-exposure research, rather than a forged-ticket login or account takeover. I reported the issue to Studio Wildcard; the patch outcome reflects my disclosure and follow-up observations. I am not publishing an exact patch date, runtime captures, vendor correspondence, the effective EOS policy, or server implementation.

My caller uses the product user ID returned by the token exchange. This report does not establish acceptance of invalid, expired, cancelled, or wrong-application tickets, arbitrary user-ID substitution, cross-deployment access, or a game-server admission bypass. The public C# example is conceptual and does not call live services.

Source map

  • SteamClientExample.cs, GetFreshTicketCtx (line 90): active ticket acquisition wrapper.
  • Program.cs, InitialPlayerListUpdate (line 217), FetchAndPrintMatchmakingData (line 426), and PeriodicPlayerCheck (line 965): orchestration and snapshot monitoring.
  • EpicGamesAuth.cs, GetEosAuthTokenAsync (line 9): configured external-auth exchange.
  • MatchmakingService.cs, GetMatchmakingDataAsync (line 24), ParsePlayers (line 109), GetUserDetailsAsync (line 156), and ParsePlayerDetails (line 190): lookup and enrichment path.
  • SteamAuth.cs, GetSessionTicket (line 45): alternative wrapper, not the active initialization path in my entry point.
  • HttpClientHelper.cs, POST and GET helpers (lines 11 and 36): HTTP request construction.
  • PlayerInfo.cs (line 6): account and session-history data model.

Line numbers refer to my original research files, not the edited examples in the explorer. The examples retain the reviewed class and key method names; model properties, signatures, and JSON shapes are simplified. Live service calls and credentials are omitted.

Contact

Let’s talk.

For engineering opportunities and project conversations.

lee.pareja1@outlook.com

Start a conversation

Compose email
Legioner Solutions / Proof of concept

Illustrative pseudocode, not production source. Helpers and data are placeholders. Examples do not run here or establish that a control is implemented in the production system.