Skip to content

As publisher I want CMP consent passed to downstream providers #312

Closed

Description

@aram356

Explore if option 2 is possible from #156

Activity

  1. added theissue type on Feb 16, 2026
  2. ChristianPavilonis commented on Feb 17, 2026

    @ChristianPavilonis
    Collaborator

    Consent Forwarding Proposal

    Summary

    Trusted Server will act as a consent interpreter — extracting privacy signals from browser cookies/headers, decoding them into structured data, and forwarding standardized consent information to advertising partners via OpenRTB.

    This enables publishers to:

    • Monitor what consent data flows to partners
    • Audit compliance with privacy regulations
    • Control consent handling without rebuilding integrations

    Key Decisions

    1. Interpreter Mode (Not Proxy Mode)

    Decision: Decode consent strings and forward structured data, rather than just passing raw strings through.

    Why: Raw proxy mode provides no observability. Publishers adopt server-side consent management specifically to gain visibility and control over how their users' privacy choices are handled.

    Tradeoff: Requires maintaining consent decoders (TCF, GPP, US Privacy), but this is offset by the value of consent visibility and the ability to enforce publisher policies.

    2. Standards-Aligned OpenRTB Forwarding

    Decision: Use OpenRTB 2.6 canonical field placement for consent signals.

    Fields:

    • user.consent — IAB TCF consent string
    • regs.gdpr — GDPR applies flag (0 or 1)
    • regs.us_privacy — US Privacy String (legacy CCPA format)
    • regs.gpp — IAB Global Privacy Platform string
    • regs.gpp_sid — GPP section IDs (which privacy frameworks apply)

    Why: These are the standard locations defined by OpenRTB 2.6 and used by Prebid Server and most SSPs. Avoids custom extensions and maximizes partner compatibility.

    3. No Geolocation-Based Consent Synthesis

    Decision: Only forward consent signals that actually exist (from cookies or headers). Do not synthesize consent strings based solely on user location.

    Why:

    • Synthesizing consent creates false compliance claims
    • Legal risk for publishers (claiming notice was given when it wasn't)
    • No actual user interaction to base consent on

    Behavior: If no consent signal exists, omit the consent fields from OpenRTB rather than sending placeholder values.

    4. GPP String Authority for Section IDs

    Decision: Derive gpp_sid (section IDs) from the decoded __gpp string content. Treat __gpp_sid cookie as a transport hint to validate, not the authoritative source.

    Why: Prevents mismatch/spoofing where section IDs in one cookie don't match the actual GPP string content.

    Data Flow

    Browser → Trusted Server

    The browser sends consent via standard cookies:

    graph LR
        Browser["Browser with CMP"] -->|euconsent-v2| TS["Trusted Server"]
        Browser -->|__gpp| TS
        Browser -->|__gpp_sid| TS
        Browser -->|us_privacy| TS
        Browser -->|Sec-GPC header| TS
    
    Loading

    Trusted Server Processing

    graph TD
        Cookies["Consent Cookies"] --> Extract["Extract Raw Strings"]
        Extract --> Decode["Decode Consent Data"]
        Decode --> Context["ConsentContext<br/>(structured data)"]
        Context --> ORTB["Map to OpenRTB Fields"]
        ORTB --> Partners["Forward to Partners"]
        
        Decode --> Log["Observability Layer<br/>(tracing, metrics)"]
        Context --> KV["KV Store<br/>(consent persistence)"]
    
    Loading

    Trusted Server → Partners

    OpenRTB bid requests include standardized consent fields:

    {
      "user": {
        "id": "synthetic-123",
        "consent": "CPXxRfAPXxRfAAfKABENB..."
      },
      "regs": {
        "gdpr": 1,
        "us_privacy": "1YNN",
        "gpp": "DBABM~CPXxRfAPXx...",
        "gpp_sid": [2, 6]
      }
    }

    Configuration

    Publishers control consent behavior via trusted-server.toml:

    Basic Settings

    [consent]
    # "interpreter" = decode + forward structured data (recommended)
    # "proxy" = forward raw strings only
    mode = "interpreter"
    
    # Check if consent data has expired based on age
    check_expiration = true
    
    # Maximum age in days before consent is considered expired
    max_consent_age_days = 395

    GDPR Scope

    [consent.gdpr]
    # Regions where GDPR applies (ISO 3166-1 alpha-2 codes)
    # Used for observability/logging, not for synthesizing consent
    applies_in = [
        "AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR",
        "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL",
        "PL", "PT", "RO", "SK", "SI", "ES", "SE",  # EU members
        "IS", "LI", "NO",                             # EEA
        "GB",                                          # UK GDPR
    ]

    US Privacy States

    [consent.us_states]
    # States with active comprehensive privacy laws
    # Config-driven to avoid recompile when new laws pass
    privacy_states = [
        "CA", "VA", "CO", "CT", "UT", "MT", "OR", "TX",
        "FL", "DE", "IA", "NE", "NH", "NJ", "TN", "MN",
        "MD", "IN", "KY", "RI"
    ]

    GPC (Global Privacy Control) Handling

    [consent.us_privacy_defaults]
    # How to construct US Privacy string when only Sec-GPC header exists
    notice_given = true        # Has publisher actually shown CCPA notice?
    lspa_covered = false       # Is publisher subject to LSPA?
    gpc_implies_optout = true  # Should GPC header trigger opt-out?

    Note: GPC handling is publisher policy, not a protocol requirement. Publishers configure this based on their actual compliance posture.

    Conflict Resolution

    When both TCF and GPP strings exist but disagree:

    [consent.conflict_resolution]
    # How to handle disagreements between GPP and TC String:
    # "restrictive" = no consent (most privacy-safe, default)
    # "newest" = use timestamp to decide
    # "permissive" = yes consent (requires legal review)
    mode = "restrictive"
    
    # How many days newer must one be to automatically win?
    freshness_threshold_days = 30

    Partner Integration

    [integrations.prebid.consent]
    # How to forward consent to Prebid Server:
    # "openrtb_only" = use OpenRTB body, strip consent cookies (recommended)
    # "cookies_only" = forward cookies, don't populate OpenRTB (legacy)
    # "both" = forward both (not recommended, may conflict)
    mode = "openrtb_only"
    
    # Consent cookies to strip when mode = "openrtb_only"
    consent_cookie_names = [
        "euconsent-v2",
        "__gpp",
        "__gpp_sid",
        "us_privacy",
        "usprivacy"
    ]

    Edge Cases

    No Consent Cookie in EU

    Under TCF, absence of a TC String means consent hasn't been collected yet.

    Behavior:

    • Set regs.gdpr = 1 (GDPR applies)
    • Omit user.consent (no TC String to forward)
    • Partners should serve non-personalized ads only

    No synthetic consent — the absence IS the signal.

    No Consent Signal in US State

    When a user is in California but has no us_privacy cookie, no __gpp cookie, and no Sec-GPC header.

    Behavior:

    • Omit regs.us_privacy, regs.gpp, regs.gpp_sid
    • Let downstream partner handle "no signal" state per their policy
    • Do NOT synthesize a placeholder string based on geolocation alone

    Both TCF and GPP Strings Present

    When cookies contain both euconsent-v2 (standalone TCF) and __gpp (which may also contain a TCF section).

    Precedence rules:

    1. Validity check — decode both, reject malformed strings
    2. Freshness check — if one is >30 days newer, use it
    3. Completeness check — prefer GPP if it has complete TCF section
    4. Conflict resolution — apply configured mode (restrictive/newest/permissive)
    5. Forward both raw strings in OpenRTB for partner transparency

    Expired Consent

    TC String is present but last_updated timestamp is older than configured max_consent_age_days.

    Behavior:

    • Still forward the raw string (proxy mode compatibility)
    • Flag as expired in structured data
    • If check_expiration = true, treat as no consent
    • If check_expiration = false, forward as normal

    GPC Header Without Cookie

    Browser sends Sec-GPC: 1 but no us_privacy cookie.

    Current issue: Existing code hardcodes Sec-GPC: 1 → us_privacy: "1YYN", making unsupported legal claims (notice given, not LSPA covered).

    Fix: Make this publisher-configurable via [consent.us_privacy_defaults] to reflect actual compliance posture.

    Open Questions

    1. TCF Decoder Scope

    Question: How much of the TC String do we need to decode?

    The full TCF v2 spec includes:

    • Core consent (purposes + vendor consents)
    • Publisher restrictions
    • Disclosed vendors segment
    • Allowed vendors segment

    For MVP: Do we only need core consent, or full decoding?

    2. Dual String Precedence Threshold

    Question: Should the freshness threshold be 30 days or different?

    Current design uses 30-day window for "GPP wins" vs "TC String wins" decisions. Is this the right balance between cookie sync delays and genuine preference updates?

    3. Conflict Resolution Defaults

    Question: Should conflict resolution mode default to "restrictive" or allow per-partner configuration?

    Most privacy-safe is "restrictive" (deny when unclear), but some publishers may need per-partner flexibility.

    4. KV Store Consent Freshness

    Question: When cookie AND KV store both have consent data, should we:

    • Always use cookie (fresher)?
    • Update KV store on every request?
    • Only update KV on consent changes?
    • How to detect "consent changed" efficiently?

    Implementation Phases

    Phase 1: Extraction + Decoding

    • Read consent cookies (euconsent-v2, __gpp, us_privacy)
    • Read Sec-GPC header
    • Decode TC String v2
    • Decode GPP String
    • Parse US Privacy string
    • Create ConsentContext struct
    • Unit tests with known-good consent strings

    Phase 2: OpenRTB Integration

    • Add consent fields to OpenRTB structs
    • Wire consent into auction flow
    • Replace hardcoded Sec-GPC → "1YYN" with config
    • Update Prebid integration
    • Update existing tests

    Phase 3: Configuration + Observability

    • Add [consent] config to TOML
    • Implement jurisdiction detection
    • Implement expiration checking
    • Add consent event logging
    • Handle no-signal scenarios

    Phase 4: Partner Integrations

    • Update APS integration
    • Update Equativ integration
    • Add cookie stripping for openrtb_only mode
    • Integration tests

    Phase 5: KV Store Integration

    Standards References

  3. ChristianPavilonis commented on Feb 18, 2026

    @ChristianPavilonis
    Collaborator

    to Enable consent info in prebidjs:

        consentManagement: {
          gdpr: {
            cmpApi: 'iab',
            defaultGdprScope: true,
            rules: [
              {
                purpose: 'storage',
                enforcePurpose: true,
                enforceVendor: true,
              },
              {
                purpose: 'basicAds',
                enforcePurpose: true,
                enforceVendor: true,
              },
              {
                purpose: 'personalizedAds',
                enforcePurpose: true,
                enforceVendor: true,
              },
              {
                purpose: 'measurement',
                enforcePurpose: true,
                enforceVendor: true,
              },
              {
                purpose: 'transmitPreciseGeo',
                enforcePurpose: true,
              },
            ],
          },
        },
    

    OpenRTB Prebid Consent Fields Reference

    A reference for every consent-related field in an OpenRTB 2.6 bid request as sent by Prebid, with tcfControl and all consent management modules enabled.

    Field Map

    graph TD
        BidRequest["OpenRTB Bid Request"]
    
        BidRequest --> Regs["regs"]
        Regs --> RegsExt["regs.ext"]
        RegsExt --> GDPR["gdpr: 0 | 1"]
    
        BidRequest --> User["user"]
        User --> UserExt["user.ext"]
        UserExt --> Consent["consent (TC String v2)"]
        UserExt --> EIDs["eids[]"]
        UserExt --> CPS["ConsentedProvidersSettings"]
        CPS --> AC["consented_providers (AC String)"]
    
        BidRequest --> Source["source"]
        Source --> SChain["ext.schain"]
    
    Loading

    1. GDPR Applicability — regs.ext.gdpr

    Value Meaning
    1 GDPR applies — bidders must respect the TC String
    0 GDPR does not apply
    absent Unknown — bidders should assume GDPR may apply

    When gdpr: 1, every downstream partner must have legal basis (consent or legitimate interest) before processing personal data. Bidders without valid legal basis should be excluded from the auction.

    2. TCF v2 Consent String — user.ext.consent

    The IAB Transparency & Consent Framework v2 consent string. Base64url-encoded binary that contains:

    Core String Segments

    Segment Description
    Version TCF spec version (currently 2)
    Created / LastUpdated Timestamps of consent collection
    CMP ID & Version Which CMP collected consent
    Consent Screen Screen number in the CMP flow
    Consent Language ISO 639-1 language code
    Vendor List Version GVL version the CMP was using
    Purpose Consents Bitfield for Purposes 1-12
    Purpose LI Transparency Bitfield for legitimate interest per purpose
    Vendor Consents Bitfield of consented GVL vendor IDs
    Vendor LI Bitfield of vendor legitimate interest
    Publisher Restrictions Per-vendor purpose restriction overrides

    TCF Purposes Relevant to Prebid

    Purpose Name What It Gates in Prebid (tcfControl)
    1 Store/access info on device Usersyncs, user ID modules, cookie/localStorage access
    2 Basic ads Whether bid adapters can participate in the auction
    3 Create ad profile Ad profile building (not directly enforced by Prebid)
    4 Personalized ads User FPD transmission; EID transmission if eidsRequireP4Consent
    7 Measurement Whether analytics adapters can fire
    SF1 Use precise geolocation May cause geo data to be truncated

    Disclosed Vendors Segment

    The TC String may end with a .YAAAAAAAAAAA suffix — this is the Disclosed Vendors segment encoding which vendors were shown to the user in the CMP UI.

    Decoding

    Decode with:

    • IAB GPP decoder
    • @iabtechlab/iabtcf-decoder (npm)
    • Our own Rust decoder (see [[TCF v2 Decoder Crate Design Questions]])

    3. Google Additional Consent — user.ext.ConsentedProvidersSettings

    "consented_providers": "2~2628.2316.3119.2677~dv."

    Google's Additional Consent (AC) string covers ad tech providers not in the IAB Global Vendor List but participating in the Google ecosystem.

    Part Example Meaning
    Version 2 AC specification version
    Provider IDs 2628.2316.3119.2677 Dot-separated list of consented Google ATP IDs
    dv Disclosure Viewed Indicates the user saw the CMP disclosure

    Provider IDs map to Google's ATP list. This is required for Google Ad Manager / AdX demand.

    4. Extended User IDs — user.ext.eids

    EIDs carry cross-site or cross-device identifiers. The tcfControl module restricts transmission based on TCF consent:

    Source ID Type Storage Consent (P1) Transmit Consent
    pubcid.org Publisher Common ID Required to set P4 or any P2-P10
    id5-sync.com ID5 Required to set P4 or any P2-P10
    adserver.org Trade Desk Unified ID (TDID) Required to set P4 or any P2-P10
    crwdcntrl.net Lotame Panorama ID Required to set P4 or any P2-P10

    Each EID entry has the structure:

    {
      "source": "id5-sync.com",
      "uids": [{
        "id": "ID5*CSYv...",
        "atype": 1,
        "ext": {
          "linkType": 2,
          "pba": "...",
          "abTestingControlGroup": false
        }
      }]
    }
    • atype: 1 — cookie/device-based ID
    • linkType: 2 — probabilistic match (ID5-specific)
    • abTestingControlGroup — ID5 A/B test holdout flag

    [!important] tcfControl Enforcement
    If Purpose 1 consent is not granted, all EIDs should be blocked from being set. If the relevant transmit purpose is missing, EIDs are stripped from the bid request before it reaches adapters. The eidsRequireP4Consent flag in tcfControl config controls whether P4 specifically is required or any P2-P10 suffices.

    5. Supply Chain — source.ext.schain

    {
      "ver": "1.0",
      "complete": 1,
      "nodes": [{
        "asi": "playbuzz.com",
        "sid": "0016M00002GucDQQAZ",
        "hp": 1,
        "domain": "ts.prospecta.com"
      }]
    }
    Field Meaning
    complete: 1 Full chain declared, no unknown intermediaries
    asi Advertising System Identifier (seller's domain in ads.txt)
    sid Seller's account ID within that system
    hp: 1 Payment flows through this node

    Note

    The schain is also duplicated per-impression in imp[].ext.schain. This is redundant but common in Exco/Playbuzz requests.

    6. Blocked Categories — bcat

    Content safety restrictions that complement consent by limiting ad categories:

    Category Description
    IAB25-* Non-standard content (gambling, dating)
    IAB9-7, IAB9-WS* Alcohol
    IAB17-1/16/18/22 Sensitive sports content
    IAB7-39 Health
    IAB26-WS1 Political

    7. Per-Impression Fields — imp[].ext

    Each impression carries consent-adjacent signals:

    Field Description
    gpid Global Placement ID — ad slot identifier for targeting
    tid Transaction ID — unique per-impression auction identifier
    ap Encrypted auction parameters (base64, opaque to us)
    schain Per-impression supply chain (see above)

    tcfControl Configuration

    The Prebid tcfControl module (added in crates/js/lib/src/integrations/prebid/index.ts) enforces these rules:

    consentManagement: {
      gdpr: {
        cmpApi: 'iab',
        defaultGdprScope: true,
        rules: [
          { purpose: 'storage', enforcePurpose: true, enforceVendor: true },
          { purpose: 'basicAds', enforcePurpose: true, enforceVendor: true },
          { purpose: 'personalizedAds', enforcePurpose: true, enforceVendor: true },
          { purpose: 'measurement', enforcePurpose: true, enforceVendor: true },
          { purpose: 'transmitPreciseGeo', enforcePurpose: true },
        ],
      },
    },

    With debug: true, Prebid logs enforcement decisions to the console — look for TCF2 enforcement messages showing which adapters/modules are allowed or blocked per purpose.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions