# 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 1–320 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.
