Skip to content

Identity features pass on Cloudflare from the shared app #1596

Description

@aram356

Summary

Identity features pass on Cloudflare from the shared app: providers, abuse protection, SharedID creation and write-back, the mirror cookie and the EC route.

Part of #1558, server-side identity on every adapter. Plan ID T17, planned in wave 14 (after the MVP: Cloudflare); priority P2, size M. Design: spec; order and dependencies: plan.

Requirements

ID-4 Providers on every supported adapter

Lookups start at navigation alongside the origin fetch, share one deadline, and are awaited before finalization, so their IDs reach the record and the cookies of the same document response. A page load writes the record once.

ID-7 Abuse resistance

Where a trusted JA4 is available, record creation is refused when the JA4's TLS client family contradicts the User-Agent's browser family; a JA4 the rule does not recognize, or no trusted JA4 at all, does not block (DEC-18). The family rule and its data live in core with T12, which owns keeping them current; JA4 comes only from EdgeZero's ctx.ja4() (EZI-3).

ID-9 EC route

A same-origin route, /_ts/api/v1/ec by default, lets the script Trusted Server injects into proxied pages report consent and receive identity, so a reader who consents after the page loads gets an EC on that page view instead of the next navigation. A POST whose Origin matches the page mints the EC when the reader has none and consent allows, creates the record, runs the providers as a navigation does, and sets ts-ec, the SharedID cookie and the mirror cookie on its own response. It carries the consent strings the script reads from the CMP, the TCF string from __tcfapi and the GPP string from __gpp (Sourcepoint's capture from #1535 where present), which replace the consent cookies for that request; a withdrawal expires ts-ec, the SharedID cookie and the mirror cookie and tombstones the record in that response. The POST answers with JSON holding exactly what WB-4 would put in the mirror cookie for that request, and the SharedID only under SID-2 and SID-6; the script merges it into Prebid, and nothing else in the record reaches the page. A plain GET answers Prebid's SharedID pixelUrl with an empty 204: it never mints, calls a provider or changes consent, and only re-sets an existing SharedID cookie from the record (WB-3). The script sends consent changes in order with at most one call in flight. While a call is in flight, a newer change replaces a waiting one, except a waiting withdrawal, which is never replaced: later changes wait behind it. So every withdrawal reaches the server, after any grant in progress and before any later grant. The script discards a result that arrives while a newer consent change is waiting, and a withdrawal removes the IDs it merged into Prebid. Every response carries Cache-Control: no-store; a rate limit on the route waits with ID-6 (DEC-15). The route is served from the page's own host, which WebKit's cap on cookies from non-document responses exempts when the IP matches the page's (read from WebKit's source; the Accept tests it).

WB-3 SharedID cookie

When SID-2 and SID-6 allow it, the server sets the SharedID cookie from the record if the request has none or a different value. When neither the record nor the request has one, the server creates a SharedID in the format Prebid writes (a UUID v4), stores it in the record and sets it the same way, because a browser may block Prebid.js from loading at all (DEC-7). It creates and writes back a SharedID only when the configured storage.type is cookie (Prebid's cookie&html5 counts as cookie): with html5 Prebid never reads the cookie, so a server-made SharedID would split the reader's identity (SID-3). It uses Prebid's storage name and the Domain Prebid writes with, is not HttpOnly, and is Secure; SameSite=Lax for one year. It keeps Prebid's <name>_cst companion consistent, since a missing or different one makes Prebid rewrite the cookie from script, which Safari caps. Prebid's extend with a pixelUrl keeps working alongside it.

WB-4 Mirror cookie

__Host-ts-eids-mirror as in #1489: opt-in per partner (DEC-10), dropped per vendor when TCF consent is denied, needs bidstream_enabled and Purpose 4, 3,584-byte budget, host-only. Every mirrored ID expires within 30 days.

SID-1 Cookie name from config

The server reads SharedID from the cookie named by the publisher's Prebid storage.name, held in managed_user_ids[].storage.name. Prebid has no default; _sharedid and _pubcid are common.

SID-2 Opt-out

A _pubcid_optout cookie stops SharedID reads, storage, sending, creation and write-back. Prebid also reads the opt-out from localStorage, which the server cannot see, so the injected script copies a localStorage _pubcid_optout into the cookie (T20).

SID-6 EID consent

Reading SharedID into the record, storing it, sending it and writing it back each need EID permission (TCF Purposes 1 and 4, through the consent module's EID predicate in consent/mod.rs), in addition to EC permission and the SID-2 opt-out. EC permission alone (consent_allows_ec, Purpose 1) is not enough. Where consent is required but missing, permission is denied.

Current state

What the code does today, from the design's survey at the revisions it names:

  • ID-7: Fastly checks only that a JA4 exists plus a browser User-Agent platform; main's known-browser table (ec/device.rs) feeds analytics only and needs an HTTP/2 fingerprint that Axum behind CloudFront lacks; the RC's Axum checks only the User-Agent, with request filters, because it has no trusted JA4 (EZI-3); Cloudflare and Spin create no records yet. CloudFront sends JA4 to the origin only in the CloudFront-Viewer-JA4-Fingerprint header, which an origin request policy must forward. Add JA4 request interface design spec edgezero#469's draft design reports Axum as NotExposed and leaves the trusted proxy header to its section 13.1.
  • ID-9: on main only a real-browser navigation through the publisher proxy mints an EC, and no such route exists. Extract existing Sourcepoint consent synchronization #1535 (RC line) posts Sourcepoint's GPP string to a separate, non-minting /_ts/consent route, wired into each adapter's router.
  • WB-3: no branch writes it.
  • WB-4: in Mirror provider EIDs to a cookie and run pull sync as an identity provider #1489 such an ID never expires (Expiry::Never); closed Add server-side identity providers for six vendors #1461 capped mirrored IDs at 24 hours.
  • SID-1: main reads only sharedId (constants.rs:10), Prebid's module name, on Fastly for the record and on every adapter for /_ts/admin/eids.

Acceptance criteria

  • ID-4: A mock provider that answers within the deadline has its ID in the record and in the mirror cookie of that navigation's response
  • ID-4: One that misses the deadline is skipped and does not delay the response past it
  • ID-7: On each supported adapter a scripted client with a browser User-Agent and a non-browser JA4 creates no record, a current Chrome, Safari and Firefox JA4 that no list names still creates one, and a request with no trusted JA4 is not refused for lacking one
  • ID-9: On Fastly and Axum, a consented same-origin POST with no ts-ec creates the record and sets ts-ec and the SharedID cookie in that response
  • ID-9: Its JSON omits a non-mirrored partner's ID, an ID whose vendor's TCF consent is denied and an expired ID
  • ID-9: A withdrawal issued while a grant call with no EC and delayed providers is in flight leaves no identity cookie, no live record and no ID in Prebid
  • ID-9: With grant A in flight, a withdrawal B and then a grant C reach the server as A, B, C, so B expires the identity cookies and tombstones A's record and C is handled as a first consent with no ts-ec
  • ID-9: A cross-site Origin, a GET and a client ID-7 refuses mint nothing and call no provider
  • ID-9: An unchanged returning reader gets no Set-Cookie
  • ID-9: In Safari, ts-ec and the SharedID cookie set by the route keep their one-year Max-Age on Fastly and on Axum behind CloudFront
  • WB-3: Record present and cookie absent gives both cookies with those attributes
  • WB-3: A consented first visit with no SharedID anywhere gets one created, stored and set, and a reader with _pubcid_optout gets none
  • WB-3: With storage.type = html5 the server neither creates nor sets a SharedID
  • WB-3: An equal cookie with a consistent <name>_cst gives no Set-Cookie
  • WB-3: An equal cookie with a missing or different <name>_cst re-sets only the companion
  • WB-3: Purpose 1 denied or _pubcid_optout expires both
  • WB-3: Purpose 4 denied with Purpose 1 granted gives no Set-Cookie and leaves the browser's cookie alone
  • WB-4: An ID written with no stated expiry is mirrored with an expiry of at most 30 days
  • SID-1: With storage.name = "_pubcid" a _pubcid cookie is read on every supported adapter, and a cookie named sharedId is not
  • SID-2: With _pubcid_optout present nothing SharedID-related is read, stored, sent, created or written, and a localStorage-only opt-out stops creation from the next request on
  • SID-6: On each supported adapter, with a populated record, an absent or different browser cookie, Purpose 1 granted and Purpose 4 denied, no SharedID is read, sent or written back

Dependencies

Blocked by:

Blocks:

Notes from the plan

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions