Files
OpenFUT/fifa17-recon/tools/auth_schema_reflection.md
T
funman300 6ddd5e9d47 fifa17-recon: offline FUT squad-shell working + full card-system RE
Milestone: FIFA 17 Ultimate Team boots end-to-end on our offline backend
past every EA gate into the hub and a live Squads editor (correct 4-4-2,
5-star squad, no freezes).

Key findings this session:
- userMassInfo MUST stay {} (any content desyncs the massinfo parser
  0x180174630 -> tokenizer busy-loop freeze). Deliver the squad via
  GET /squad/0 (fetched on Squads-tab entry) instead.
- Player cards render generic because the card view-model (0x1800d7920)
  reads identity/rating/face from a resolved record at item+0x10, filled
  by a lookup (0x18011cca0) in the FUT item-definition std::map at
  CardsDb+0x160c0 -- which is EMPTY offline -> default blank record.
- Version advertising (itemDbVersion/checkServerDbVersion) is proven inert
  (JSON fields routed to the skip handler). Owned items don't auto-trigger
  a definition fetch. In-place map overwrite is dead (map stays empty).
- Definition-serving endpoints (item/resource, defid, item?idList) built +
  ready; the fetch trigger lives in the packed FIFA17.exe.

New: docs/CARD_SYSTEM.md (findings + ordered next-steps plan for real
player cards: patch-POC, dbdata extractor, drive FIFA17.exe fetch, or
live-memory store injection). Plus tools: fut_seed.py (squad ladder +
definition serving), fifadrive.sh, vgamepad.py, and the login-RE toolset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
2026-08-01 20:24:30 -07:00

528 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# FIFA 17 Blaze — Authentication component (0x0001) reflection-reversed schema
**Date:** 2026-07-30 · **Target:** live `FIFA17.exe` PID 19517 (alive throughout, still at the
main menu afterwards) · **Method:** `/proc/<pid>/mem` reflection walk + targeted disassembly +
in-process function calls via gdb.
Companion doc: `fifa17-recon/tools/preauth_schema_reflection.md` (Util component / PreAuthResponse).
---
## 0. Clean-room provenance
Every fact below came from one of three allowed sources:
1. **The client's own runtime reflection metadata**, read out of the process we own. FIFA 17's
BlazeSDK ships full TDF type descriptors (class names, member names, wire tags, struct offsets)
in `.data`. This is the bulk of §3–§6.
2. **Disassembly of code in the binary we own** (`getCommandName` / `getErrorName` /
`getNotificationName` switches, TDF descriptor initialisers, notification dispatcher).
3. **Calling the client's own name-lookup functions in-process** (§2). This is reading the
binary's answer to its own question — no external artefact involved.
**No EA/FIFA leaked source was consulted at any point.** No third-party BlazeSDK reimplementation
was consulted for this document either (unlike the Fire2 header note in the preAuth doc, the
findings here are all first-party).
---
## 1. Headline result: the client sent `logout`, not `login`
The observed live frame
```
RX #9 Authentication::cmd:0x0046 msgType=MESSAGE msgNum=16 userIdx=0 payload=0B
```
**`Authentication` command `0x0046` (70) = `logout`.** (§2, verified twice.)
`logout` has **no request TDF and no response TDF** — there is no `Blaze::Authentication::LogoutRequest`
or `LogoutResponse` anywhere in the client's type index, which is exactly why the payload was 0 bytes.
Our empty REPLY was therefore *correct on the wire*.
This reframes the whole gate:
- The client **never sent `Authentication::login` (cmd 0x000A)**, never sent `getAuthToken` (0x0024),
never sent `listUserEntitlements2` (0x001D), never sent `listPersonas` (0x0064).
- It went `preAuth``ping` → 6× `fetchClientConfig`**`logout`** → transport-ping loop.
- A client that logs out without ever logging in has decided *before touching Blaze auth* that it has
no credential to present. That is the Origin/LSX layer (layer 1 in the repack recon), not Blaze.
**Implication for the workflow:** implementing `Authentication::login` server-side is necessary but
**not sufficient and not the current blocker**. The client must first obtain an auth code from the
in-process Origin/LSX stub on `127.0.0.1:4216` (`GetAuthCode``AuthCodeResponse`, and
`OriginIsOnline` / `GetInternetConnectedState` must report ONLINE). Until that succeeds the client
will keep skipping straight to `logout`. See §7 for the ordering this implies.
---
## 2. Authentication component RPC table — **complete**
### How it was recovered
Each Blaze component has a 7-pointer `ComponentDescription`-style table in `.rdata`. Layout
established by diffing the Util table (whose command map we already knew) against Authentication's:
| Offset | Meaning | Util | Authentication |
|---|---|---|---|
| `+0x00` | shared helper | `0x146f022d0` | `0x146f022d0` |
| `+0x08` | `getComponentName()` | `0x146df7370``"UtilComponent"` | `0x146e0ec00``"AuthenticationComponent"` |
| `+0x10` | **`getCommandName(u16)`** | `0x146df6dc0` | **`0x146e0d2a0`** |
| `+0x18` | shared helper | `0x145bf3580` | `0x145bf3580` |
| `+0x20` | `getErrorName(u32)` | `0x146df7380` | `0x146e0ec20` |
| `+0x28` | `getRestResourceInfo(u16)` | `0x146f82070` (null stub) | `0x146e0f2e0` |
| `+0x30` | shared helper | `0x1465734f0` | `0x1465734f0` |
Table addresses: Util `0x143895820`, Authentication `0x14389d628`, plus one more at `0x143891940`
(Redirector) and six in `0x1438f5xxx` (FIFA-custom components).
Util's `getCommandName` is clean code — `movzx eax,dx; dec eax; cmp eax,0x1b; ja default;`
jump table of 28 image-relative RVAs at `0x141b17af4`. Decoding it statically **reproduces the
previously-known Util table exactly** (`fetchClientConfig=1, ping=2, preAuth=7, postAuth=8`, …),
which validates the whole approach.
**Authentication's `getCommandName` at `0x146e0d2a0` is Denuvo-mutated** — it begins
`push rcx; lea rcx,[rip+…]; jmp 0x14e183f34` into the protector arena. Consequently:
- the 33-name alphabetical string pool at `0x14389d690``0x14389d928` has **zero** references
anywhere in the address space (no `lea`, no qword pointer, no image-relative u32 — checked across
every readable region including the heap);
- so the command map cannot be recovered statically.
It was recovered instead by **calling the function in the live process**:
```
gdb --batch -p <pid> -ex 'p ((char*(*)(long,long,int))0x146e0d2a0)(0,0,<id>)'
```
Note the calling-convention detail: the target is **Windows x64** code (`this``rcx`, arg→`rdx`) but
gdb marshals with the **SysV** ABI (`rdi, rsi, rdx, …`), so the command id must be passed as the
**third** gdb argument to land in `rdx`. Validated against Util first (returned `preAuth` for 7,
`ping` for 2, `fetchClientConfig` for 1, `postAuth` for 8), then applied to Authentication.
Two independent cross-checks on the Authentication result:
- id `0x000B``"trustedLogin"`, which matches the static REST-binding struct at `0x143896a80`
(`{u16 component=0x0001, u16 command=0x000B, …, const char* name="trustedLogin", …, "GET"}`).
- every one of the 33 names in the static string pool is accounted for, with no leftovers.
### The table
Ids 1320 were swept; everything not listed returns the empty default string.
| Cmd | RPC | Cmd | RPC |
|---|---|---|---|
| `0x000A` (10) | **`login`** | `0x003C` (60) | `expressLogin` |
| `0x000B` (11) | `trustedLogin` | **`0x0046` (70)** | **`logout`** ← *observed on the wire* |
| `0x0014` (20) | `updateAccount` | `0x005A` (90) | `getPersona` |
| `0x0015` (21) | `upgradeAccount` | `0x0064` (100) | `listPersonas` |
| `0x001D` (29) | `listUserEntitlements2` | `0x0065` (101) | `expressCreateAccount` |
| `0x001E` (30) | `getAccount` | `0x00E6` (230) | `createWalUserSession` |
| `0x001F` (31) | `grantEntitlement` | `0x00F1` (241) | `acceptLegalDocs` |
| `0x0020` (32) | `listEntitlements` | `0x00F2` (242) | `getEmailOptInSettings` |
| `0x0022` (34) | `getUseCount` | `0x00F6` (246) | `getTermsOfServiceContent` |
| `0x0023` (35) | `decrementUseCount` | `0x0104` (260) | `getOriginPersona` |
| `0x0024` (36) | `getAuthToken` | `0x010E` (270) | `checkEmail` |
| `0x0026` (38) | `getPasswordRules` | `0x0118` (280) | `getPersonaNameSuggestions` |
| `0x0027` (39) | `grantEntitlement2` | `0x0122` (290) | `guestLogin` |
| `0x002B` (43) | `modifyEntitlement2` | | |
| `0x002C` (44) | `consumecode` | | |
| `0x002D` (45) | `passwordForgot` | | |
| `0x002F` (47) | `getPrivacyPolicyContent` | | |
| `0x0030` (48) | `listPersonaEntitlements2` | | |
| `0x0033` (51) | `checkAgeReq` | | |
| `0x0034` (52) | `getOptIn` | | |
| `0x0035` (53) | `enableOptIn` | | |
| `0x0036` (54) | `disableOptIn` | | |
Confidence: **certain** for every row (the client itself produced these strings).
### Request / response type binding
From the reflection type index (`Blaze::Authentication::*`):
| Cmd | RPC | Request TDF | Response TDF |
|---|---|---|---|
| `0x000A` | `login` | `LoginRequest` | **`LoginResponse`** |
| `0x000B` | `trustedLogin` | `TrustedLoginRequest` | `LoginResponse` |
| `0x003C` | `expressLogin` | `ExpressLoginRequest` | `LoginResponse` |
| `0x0122` | `guestLogin` | *(none)* | `LoginResponse` |
| **`0x0046`** | **`logout`** | ***(none — empty)*** | ***(none — empty)*** |
| `0x0024` | `getAuthToken` | *(none)* | `GetAuthTokenResponse` |
| `0x001D` | `listUserEntitlements2` | `ListUserEntitlements2Request` | `Entitlements` |
| `0x0030` | `listPersonaEntitlements2` | `ListPersonaEntitlements2Request` | `Entitlements` |
| `0x0064` | `listPersonas` | *(none)* | `ListPersonasResponse` |
| `0x005A` | `getPersona` | *(none)* | `GetPersonaResponse` |
| `0x001E` | `getAccount` | *(none)* | `AccountInfo` |
| `0x0104` | `getOriginPersona` | `GetOriginPersonaRequest` | `PersonaInfo` |
`logout` having no TDFs on either side is confirmed by absence: there is no `LogoutRequest` /
`LogoutResponse` type anywhere in the client's index.
---
## 3. `Blaze::Authentication::LoginResponse` — the login reply (cmd `0x000A`)
Descriptor `0x14487d170`, member table `0x1448789c0`, 5 members. Serialise in **ascending packed-tag
order** (which is the order the table itself is sorted in).
| Tag | Member | TDF type | Wire type | Offset |
|---|---|---|---|---|
| `ANON` | `isAnonymous` | bool | `0x00` | `+0x12` |
| `NTOS` | `needsLegalDoc` | bool | `0x00` | `+0x13` |
| `SESS` | `userLoginInfo` | `Blaze::Authentication::UserLoginInfo` | `0x03` | `+0x18` |
| `SPAM` | `isOfLegalContactAge` | bool | `0x00` | `+0x10` |
| `UNDR` | `isUnderage` | bool | `0x00` | `+0x11` |
### `Blaze::Authentication::UserLoginInfo` (`SESS`) — 8 members, descriptor `0x14487cb00`
| Tag | Member | Type | Offset |
|---|---|---|---|
| `1CON` | `isFirstConsoleLogin` | bool | `+0x51` |
| `BUID` | `blazeUserId` | int64 | `+0x28` |
| `FRST` | `isFirstLogin` | bool | `+0x50` |
| `KEY ` | `sessionKey` | string | `+0x10` |
| `LLOG` | `lastLoginDateTime` | int64 | `+0x58` |
| `MAIL` | `email` | string | `+0x38` |
| `PDTL` | `personaDetails` | `PersonaDetails` (struct) | `+0x60` |
| `UID ` | `userId` | int64 | `+0x30` |
> Note the trailing-space tags: `KEY` and `UID` are 3-char tags. `heat2.py`'s `encode_tag`
> pads to 4 with the 6-bit "space" code, which is what the wire expects.
### `Blaze::Authentication::PersonaDetails` (`PDTL`) — 6 members, descriptor `0x14487cab0`
| Tag | Member | Type | Offset |
|---|---|---|---|
| `DSNM` | `displayName` | string | `+0x18` |
| `LAST` | `lastAuthenticated` | uint32 | `+0x3c` |
| `PID ` | `personaId` | int64 | `+0x10` |
| `PLAT` | `clientPlatform` | `Blaze::ClientPlatformType` (enum → int) | `+0x38` |
| `STAS` | `status` | `PersonaStatus::Code` (enum → int) | `+0x40` |
| `XREF` | `extId` | uint64 | `+0x30` |
### Request side (for completeness)
`Blaze::Authentication::LoginRequest` (`0x14487ca10`, 3 members):
| Tag | Member | Type |
|---|---|---|
| `AUTH` | `authCode` | string ← *the Origin/LSX auth code* |
| `EXTB` | `externalBlob` | blob |
| `EXTI` | `externalId` | uint64 |
`TrustedLoginRequest` (`0x14487ca60`): `ID ` id:string, `ITYP` idType:string, `TOKN` accessToken:string.
`ExpressLoginRequest` (`0x14487d0d0`): `MAIL` email:string, `PASS` password:string, `PNAM` personaName:string.
`StressLoginRequest` (`0x14487d120`): `MAIL` email:string, `NUID` nucleusId:uint64, `PNAM` personaName:string.
The `AUTH` field being a plain string is the whole hinge: **the client cannot fill it without a
working Origin `GetAuthCode`.**
---
## 4. What the server must return for the client to consider itself authenticated
**Caveat on method.** For `PreAuthResponse` we could read `ConnectionManager::onPreAuthResponse`
directly and prove field-by-field what is and isn't validated. The equivalent login-response handler
lives in the FIFA/OSDK layer, and that code is **Denuvo-mutated** — the same protection that hid
`getCommandName`. So the list below is derived from *structure* (what the types carry, what the
UserSessions notification duplicates, what the client must have in order to name itself), not from
reading the handler. Flagged accordingly.
| Field | Why it matters | Confidence it is required |
|---|---|---|
| `SESS.KEY ` `sessionKey` | The Blaze session credential. Every later component echoes it; a session with an empty key is not a session. Must be non-empty. | **high** |
| `SESS.BUID` `blazeUserId` | The client's own BlazeId. Used as the identity key in UserManager and in every later `lookupUser*`. Must be non-zero. | **high** |
| `SESS.UID ` `userId` | Nucleus user id. Should equal `blazeUserId` for a single-account offline emu unless you have a reason to split them. | **high** |
| `SESS.PDTL.PID ` `personaId` | **Must equal `33068179`** (the `stp-origin_emu.ini` `PersonaId`). A mismatch is exactly what trips `AUTH_ERR_INVALID_PERSONA` / `AUTH_ERR_USER_DOES_NOT_MATCH_PERSONA` / `AUTH_ERR_PERSONA_NOT_FOUND`. | **high** |
| `SESS.PDTL.DSNM` `displayName` | **Must equal `CAGE`.** This is the name the UI renders and the value later persona lookups are matched against. | **high** |
| `SESS.PDTL.STAS` `status` | Persona status enum. Must be the "active" value; `AUTH_ERR_PERSONA_INACTIVE` (19) and `AUTH_ERR_PERSONA_BANNED` (32) exist for the other cases. Send `0`. | medium |
| `SESS.PDTL.PLAT` `clientPlatform` | Should match the `PLAT` you returned in `PreAuthResponse` (`pc`). | medium |
| `SESS.PDTL.XREF` `extId` | External (Origin) id. Safe to mirror `personaId`. | low |
| `SESS.MAIL` `email` | Cosmetic; any well-formed address. | low |
| `ANON` `isAnonymous` | **Must be `false`/0.** An anonymous session is precisely a not-really-logged-in session and will not enable online features. | **high** |
| `NTOS` `needsLegalDoc` | **Must be `false`/0**, otherwise the client will branch into the legal-doc flow (`acceptLegalDocs` = 241, `getTermsOfServiceContent` = 246) instead of proceeding. | **high** |
| `UNDR` `isUnderage` | **Must be `false`/0** — underage gates online play. | **high** |
| `SPAM` `isOfLegalContactAge` | Set `true`/1 for symmetry with `UNDR=false`. | medium |
| `FRST` / `1CON` / `LLOG` | First-login flags and timestamp. Cosmetic; `false/false/<now>` is fine. | low |
Consistency rule that spans both layers: **`personaId` and `personaName` returned over LSX,
returned in `LoginResponse.PDTL`, and pushed in the `UserAuthenticated` notification must be the
same triple** (`33068179` / `CAGE` / `en_US`). Every `AUTH_ERR_*_PERSONA*` code in §6 is a symptom
of these three disagreeing.
---
## 5. The login-success notification the server must push
Authentication (component `0x0001`) has **exactly one** notification. Its `getNotificationName`-slot
function `0x146e0f2e0` is a one-case switch:
```
cmp dx,0xb ; jne default ; lea rax,[0x143896a80] ; ret
```
and `0x143896a80` is **not** a notification name — it is the REST-binding struct for
`trustedLogin` (cmd `0x000B`). So Authentication publishes **no async notifications at all**; the
slot is `getRestResourceInfo`, not `getNotificationName`.
**The login-success notification lives on `UserSessions`, component `0x7802` (30722).**
Component id confirmed independently: that component's error table compares against
`(errorNumber<<16)|0x7802` (`0x17802`, `0x27802`, … `0x177802`).
`UserSessions::getNotificationName` at `0x146de19a0` is **clean, unmutated** code —
`movzx eax,dx; dec eax; cmp eax,0xb; ja default;` with a 12-entry image-relative jump table at
`0x141b03f70`. Decoded statically:
| Notification id | Name | Payload TDF |
|---|---|---|
| `0x0001` | `UserSessionExtendedDataUpdate` | `Blaze::UserSessionExtendedDataUpdate` |
| `0x0002` | `UserAdded` | `Blaze::UserData` |
| `0x0003` | `UserRemoved` | `Blaze::UserIdentification` *(or `UserStatus`)* |
| `0x0004` | *(unused)* | — |
| `0x0005` | `UserUpdated` | `Blaze::UserStatus` |
| `0x0006``0x0007` | *(unused)* | — |
| **`0x0008`** | **`UserAuthenticated`** | **`Blaze::UserSessionLoginInfo`** |
| `0x0009` | `UserUnauthenticated` | `Blaze::UserSessionLogoutInfo` |
| `0x000A``0x000B` | *(unused)* | — |
| `0x000C` | `ServerDraining` | — |
The **id → name** column is certain (decoded from the client's own jump table). The **payload TDF**
column is *inferred*: the notification dispatcher at `0x146de2803` (12-entry jump table at
`0x141b05258`, branches at `0x146de2bc3 / 289c / 2ae9 / 2dd8 / 29e0 / 2cf1 / 2825`) constructs its
TDFs on the stack rather than through the type-descriptor getters, so I could not bind each branch
to a descriptor by xref. The mapping above is a 1:1 name match against the only session types the
client links in, and `UserSessionLoginInfo` is the only type in the entire index that carries a
session key plus a persona — but treat the payload column as **high confidence, not verified**.
### `Blaze::UserSessionLoginInfo` — 16 members, descriptor `0x14486f920`
This is the notification body to push after a successful `login`. Note it is a **superset** of
`UserLoginInfo` with the persona fields flattened in rather than nested.
| Tag | Member | Type | Offset |
|---|---|---|---|
| `1CON` | `isFirstConsoleLogin` | bool | `+0x51` |
| `ALOC` | `accountLocale` | uint32 | `+0x60` |
| `BUID` | `blazeUserId` | int64 | `+0x28` |
| `CGID` | `connectionGroupObjectId` | ObjectId | `+0xb0` |
| `DSNM` | `displayName` | string | `+0x70` |
| `FRST` | `isFirstLogin` | bool | `+0x50` |
| `KEY ` | `sessionKey` | string | `+0x10` |
| `LAST` | `lastAuthenticated` | uint32 | `+0xac` |
| `LLOG` | `lastLoginDateTime` | int64 | `+0x58` |
| `MAIL` | `email` | string | `+0x38` |
| `NASP` | `personaNamespace` | string | `+0x88` |
| `PID ` | `personaId` | int64 | `+0x68` |
| `PLAT` | `clientPlatform` | enum | `+0xa8` |
| `UID ` | `userId` | int64 | `+0x30` |
| `USTP` | `userSessionType` | `Blaze::UserSessionType` (enum) | `+0xc0` |
| `XREF` | `extId` | uint64 | `+0xa0` |
`ALOC` (`accountLocale`) is a packed uint32 locale — use the same encoding the client sent in
`ClientData.LANG` in the PreAuth request, i.e. echo it back rather than inventing one. `NASP`
(`personaNamespace`) should match the `NASP` you returned in `PreAuthResponse`.
### Related UserSessions types (for the rest of the session bring-up)
```
Blaze::UserSessionLogoutInfo (0x14486f970, 2)
BID blazeId int64
USTP userSessionType enum
Blaze::SessionInfo (0x14486ffa0, 5)
BUID blazeUserId int64 DSNM displayName string
KEY sessionKey string MAIL email string
UID userId int64
Blaze::UserIdentification (0x14486ebc0, 9)
AID accountId int64 ALOC accountLocale uint32
EXBB externalBlob blob EXID externalId uint64
ID blazeId int64 NAME name string
NASP personaNamespace string ORIG originPersonaId uint64
PIDI pidId int64
Blaze::UserData (0x1448706b0, 3)
EDAT extendedData Blaze::UserSessionExtendedData
FLGS statusFlags Blaze::UserDataFlags (bitfield)
USER userInfo Blaze::UserIdentification
Blaze::UserStatus (0x14486ed10, 2)
FLGS statusFlags Blaze::UserDataFlags ID blazeId int64
Blaze::UserSessionExtendedData (0x144870390, 12)
ADDR address Blaze::NetworkAddress (union, 5 cases)
BPS bestPingSiteAlias string
CTY country string
CVAR clientData variable
DMAP dataMap map<int64,uint32>
HWFG hardwareFlags Blaze::HardwareFlags (bitfield)
ISP iSP string
PSLM latencyList list<int32>
QDAT qosData Blaze::Util::NetworkQosData
TZ timeZone string
UATT userInfoAttribute uint64
ULST blazeObjectIdList list<ObjectId>
Blaze::UserSessionExtendedDataUpdate (0x1448703e0, 3)
DATA extendedData Blaze::UserSessionExtendedData
SUBS subscribed bool
USID userId int64
```
---
## 6. Authentication error codes — **complete**
Blaze error codes for this component are packed `(errorNumber << 16) | 0x0001`. Recovered by calling
`getErrorName` (`0x146e0ec20`, clean code) in-process across the range; the static `cmp edx,0xNN0001`
chain at `0x146e0ec20`+ corroborates.
| # | Name | # | Name |
|---|---|---|---|
| 1 | `AUTH_ERR_INVALID_TOKEN` | 72 | `AUTH_ERR_TOO_MANY_ENTITLEMENTS` |
| 2 | `AUTH_ERR_TOS_REQUIRED` | 73 | `AUTH_ERR_PAGESIZE_ZERO` |
| 6 | `AUTH_ERR_INVALID_SANDBOX_ID` | 74 | `AUTH_ERR_ENTITLEMENT_TAG_REQUIRED` |
| 10 | `AUTH_ERR_INVALID_COUNTRY` | 75 | `AUTH_ERR_PAGENO_ZERO` |
| 11 | `AUTH_ERR_INVALID_USER` | 76 | `AUTH_ERR_MODIFIED_STATUS_INVALID` |
| 12 | `AUTH_ERR_INVALID_PASSWORD` | 77 | `AUTH_ERR_USECOUNT_INCREMENT` |
| 14 | `AUTH_ERR_EXPIRED_TOKEN` | 78 | `AUTH_ERR_TERMINATION_INVALID` |
| 16 | `AUTH_ERR_TOO_YOUNG` | 79 | `AUTH_ERR_UNKNOWN_ENTITLEMENT` |
| 17 | `AUTH_ERR_NO_ACCOUNT` | 80 | `AUTH_ERR_EXCEEDS_PSU_LIMIT` |
| 19 | `AUTH_ERR_PERSONA_INACTIVE` | 81 | `AUTH_ERR_OPTIN_NAME_REQUIRED` |
| 20 | `AUTH_ERR_INVALID_PMAIL` | 82 | `AUTH_ERR_INVALID_OPTIN` |
| 21 | `AUTH_ERR_INVALID_FIELD` | 83 | `AUTH_ERR_OPTIN_MISMATCH` |
| 22 | `AUTH_ERR_INVALID_EMAIL` | 84 | `AUTH_ERR_NO_SUCH_OPTIN` |
| 23 | `AUTH_ERR_INVALID_STATUS` | 85 | `AUTH_ERR_AUTHID_REQUIRED` |
| 32 | `AUTH_ERR_PERSONA_BANNED` | 86 | `AUTH_ERR_PERSONA_EXTREFID_REQUIRED` |
| 33 | `AUTH_ERR_INVALID_PERSONA` | 87 | `AUTH_ERR_SOURCE_REQUIRED` |
| 34 | `AUTH_ERR_CURRENT_PASSWORD_REQUIRED` | 88 | `AUTH_ERR_APPLICATION_REQUIRED` |
| 41 | `AUTH_ERR_DEACTIVATED` | 89 | `AUTH_ERR_TOKEN_REQUIRED` |
| 43 | `AUTH_ERR_BANNED` | 90 | `AUTH_ERR_PARAMETER_TOO_LENGTH` |
| 44 | `AUTH_ERR_DISABLED` | 91 | `AUTH_ERR_NO_SUCH_PERSONA_REFERENCE` |
| 50 | `AUTH_ERR_NEED_PCCDKEY` | 93 | `AUTH_ERR_INVALID_SOURCE` |
| 51 | `AUTH_ERR_CODE_ALREADY_USED` | 94 | `AUTH_ERR_NO_SUCH_AUTH_DATA` |
| 52 | `AUTH_ERR_INVALID_CODE` | 101 | `AUTH_ERR_USER_INACTIVE` |
| 53 | `AUTH_ERR_CODE_ALREADY_DISABLED` | 102 | `AUTH_ERR_UNEXPECTED_ACTIVATION` |
| 54 | `AUTH_ERR_NO_ASSOCIATED_PRODUCT` | 103 | `AUTH_ERR_NAME_MISMATCH` |
| 55 | `AUTH_ERR_INVALID_MAPPING_ERROR` | 105 | `AUTH_ERR_INVALID_NAMESPACE` |
| 56 | `AUTH_ERR_NO_SUCH_GROUP_NAME` | 198 | `AUTH_ERR_FIELD_MIN_LOWER_CHARS` |
| 57 | `AUTH_ERR_MISSING_PERSONAID` | 199 | `AUTH_ERR_FIELD_MIN_UPPER_CHARS` |
| 58 | `AUTH_ERR_USER_DOES_NOT_MATCH_PERSONA` | 200 | `AUTH_ERR_FIELD_MIN_DIGITS` |
| 59 | `AUTH_ERR_WHITELIST` | 201 | `AUTH_ERR_FIELD_INVALID_CHARS` |
| 60 | `AUTH_ERR_LINK_PERSONA` | 202 | `AUTH_ERR_FIELD_TOO_SHORT` |
| 61 | `AUTH_ERR_NO_SUCH_GROUP` | 203 | `AUTH_ERR_FIELD_TOO_LONG` |
| 63 | `AUTH_ERR_NO_SUCH_ENTITLEMENT` | 204 | `AUTH_ERR_FIELD_MUST_BEGIN_WITH_LETTER` |
| 64 | `AUTH_ERR_GROUP_NAME_DOES_NOT_MATCH` | 205 | `AUTH_ERR_FIELD_MISSING` |
| 66 | `AUTH_ERR_USECOUNT_ZERO` | 206 | `AUTH_ERR_FIELD_INVALID` |
| 67 | `AUTH_ERR_ENTITLEMETNTAG_EMPTY` *(sic)* | 207 | `AUTH_ERR_FIELD_NOT_ALLOWED` |
| 70 | `AUTH_ERR_GROUPNAME_REQUIRED` | 208 | `AUTH_ERR_FIELD_NEEDS_SPECIAL_CHARS` |
| 71 | `AUTH_ERR_GROUPNAME_INVALID` | 209 | `AUTH_ERR_FIELD_ALREADY_EXISTS` |
| | | 210 | `AUTH_ERR_FIELD_NEEDS_CONSENT` |
| | | 211 | `AUTH_ERR_FIELD_TOO_YOUNG` |
| | | 212 | `AUTH_ERR_ASSOCIATION_TOO_YOUNG` |
---
## 7. Supporting types for the post-login flow
```
Blaze::Authentication::GetAuthTokenResponse (0x14487d080, 1)
AUTH authToken string
Blaze::Authentication::GetUserAccessTokenResponse (0x14487db80, 2)
ATOK accessToken string CLID clientId string
Blaze::Authentication::ListPersonasResponse (0x14487d210, 1)
PINF list list<Blaze::Authentication::PersonaDetails>
Blaze::Authentication::GetPersonaResponse (0x14487d1c0, 2)
PINF personaInfo Blaze::Authentication::PersonaInfo
UID userId int64
Blaze::Authentication::PersonaInfo (0x14487c7c0, 7)
DSNM displayName string DTCR dateCreated string
LADT lastAuthenticated uint32 NSNM nameSpaceName string
PID personaId int64 STAS status enum
STRC statusReasonCode enum
Blaze::Authentication::AccountInfo (0x14487c810, 16)
AMU anonymousUser:bool ASRC authenticationSource:string
CO country:string DOB dOB:string
DTCR dateCreated:string GOPT globalOptin:int8
LATH lastAuth:string LN language:string
MAIL email:string PML parentalEmail:string
RC reasonCode:enum STAS status:enum
STAT emailStatus:enum TPOT thirdPartyOptin:int8
UDU underageUser:bool UID userId:int64
Blaze::Authentication::Entitlements (0x14487d4e0, 1)
NLST entitlements list<Blaze::Authentication::Entitlement>
Blaze::Authentication::Entitlement (0x14487d490, 16)
DEVI deviceUri:string GDAY grantDate:string GNAM groupName:string
ID id:uint64 ISCO isConsumable:bool PID personaId:int64
PJID projectId:string PRCA productCatalog:enum PRID productId:string
STAT status:enum STRC statusReasonCode:enum TAG entitlementTag:string
TDAY terminationDate:string TYPE entitlementType:enum
UCNT useCount:uint32 VER version:uint32
Blaze::Authentication::UserProfileInfo (0x14487c860, 8)
CITY city:string CTRY country:string STAT state:string STRT street:string
ZIP zipCode:string GNDR gender:enum UID userId:int64
ELEM profileInfoElementsByCategory: map<string, map<string,string>>
(the binary's own map<> name string is written value-first — read it backwards,
same caveat as documented for QoS maps in the preAuth doc)
```
When `listUserEntitlements2` (`0x001D`) does eventually get called, the entitlement the retail exe
requires is `TAG = "ONLINE_ACCESS"` with `PRID` tied to offer id `1027460`, `STAT` = active, and
`PID` = `33068179`. `AUTH_ERR_NO_SUCH_ENTITLEMENT` (63) and `AUTH_ERR_ENTITLEMENT_TAG_REQUIRED` (74)
are the failure modes.
---
## 8. Recommended server behaviour, in order
1. **Fix layer 1 first.** Nothing in this document is reachable until the in-process Origin/LSX stub
on `127.0.0.1:4216` answers `OriginIsOnline` / `GetInternetConnectedState` with *online* and
`GetAuthCode` with a non-empty code. Right now the client's `logout` at cmd `0x0046` is the
observable proof that it never got one. This is the single highest-value next step.
2. Keep replying to `Util::fetchClientConfig` (9/1) — an empty `FetchConfigResponse { CONF: {} }` is
structurally valid, but see the preAuth doc for the tunables the client reads out of `CONF`.
3. When `Authentication::login` (`0x0001`/`0x000A`) finally arrives, reply `LoginResponse` per §3
with the §4 field values, keyed to persona `33068179` / `CAGE`.
4. Immediately after the login REPLY, push a **NOTIFICATION** frame (msgType=2) on component
`0x7802`, notification id `0x0008` (`UserAuthenticated`), payload `Blaze::UserSessionLoginInfo`
per §5 — same session key, same BlazeId, same persona triple.
5. Expect `Util::postAuth` (9/8) next; `PostAuthResponse` is documented in the preAuth doc.
6. Answer `logout` (`0x0046`) with an empty REPLY — which is already correct.
---
## 9. Tooling produced (all in the session scratchpad)
| Script | Purpose |
|---|---|
| `stubscan.py` | list every `lea rax,[rip+str]; ret` name stub in a VA window, in address order |
| `authcmd.py` | locate RPC-name strings and every reference to them |
| `refscan.py` | find all references (rip-rel lea / qword / image-relative u32) into a VA window |
| `heapref.py` | same, but across *every* readable region including the heap |
| `cmdinfo.py` | scan the metadata arena for `{u16 component, u16 command, …, name}` REST structs |
| `comptabs.py` | enumerate Blaze `ComponentDescription` 7-pointer tables |
| `qdump.py` / `pooldump.py` / `dumpregion.py` | annotated qword dump / string-pool dump / raw region dump |
| `jt.py` | decode an MSVC image-relative jump table of `lea/ret` name stubs |
| `rpcmap.py` / `factref.py` / `vt2.py` | descriptor-getter and factory xref mapping (all three came up empty for Authentication — recorded as negative results) |
| `authcmds*.gdb`, `autherr.gdb` | in-process `getCommandName` / `getErrorName` sweeps |
Reused from the previous pass: `memtool.py`, `reflect2.py`, `strsearch.py`, `all_types.txt`.
### Negative results worth recording
- The 33-name Authentication command-name pool at `0x14389d690` is **completely unreferenced**
no lea, no pointer, no RVA, anywhere in the process. Do not waste time looking for its xrefs.
- `Blaze::Authentication::LoginRequest`'s vtable (`0x14389ac68`) is referenced only by its own
factory/ctor/dtor. No game code constructs one statically — the login call site is inside
Denuvo-mutated code.
- Every `Blaze::Authentication::*` type-descriptor getter has exactly one caller: the TDF factory
registration run at `0x1451cd0xx`. There is no per-command descriptor table to mine.