Repository: prebid/prebid-server
Pull request #2841 (2023-06-13 12:10:45)

record  `PBS-auction-response` preparation time
PR makes changes, to record `AuctionResponse` preparation time

onkarvhanumante: changes from #2874  PR are merged so unblocking this PR 

@guscarreon refactor PR introduced some conflicts on PR and resolving them dismissed your previous approval. let me know if any changes are needed
onkarvhanumante: changes from #2874 [refactor `requestBid` and `getAllBids` signature](https://github.com/prebid/prebid-server/pull/2874) PR are merged so unblocking this PR 

@guscarreon refactor PR introduced some conflicts on PR and resolving them dismissed your previous approval. let me know if any changes are needed

Patch:
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
index 87c5573e..565bb9b8 100644
--- a/.github/workflows/semgrep.yml
+++ b/.github/workflows/semgrep.yml
@@ -1,8 +1,9 @@
 name: Adapter semgrep checks
 on:
-  pull_request:
+  pull_request_target:
     paths: ["adapters/*/*.go"]
-permissions: write-all
+permissions: 
+    pull-requests: write
 jobs:
   semgrep-check:
     runs-on: ubuntu-latest
@@ -11,6 +12,8 @@ jobs:
         uses: actions/checkout@v3
         with:
           fetch-depth: 0
+          ref: ${{github.event.pull_request.head.ref}}
+          repository: ${{github.event.pull_request.head.repo.full_name}}
 
       - name: Calculate diff
         id: calculate_diff
diff --git a/adapters/mobilefuse/mobilefuse.go b/adapters/mobilefuse/mobilefuse.go
index f46fc591..07ecd61b 100644
--- a/adapters/mobilefuse/mobilefuse.go
+++ b/adapters/mobilefuse/mobilefuse.go
@@ -27,6 +27,10 @@ type BidExt struct {
 	Mf ExtMf `json:"mf"`
 }
 
+type ExtSkadn struct {
+	Skadn json.RawMessage `json:"skadn"`
+}
+
 // Builder builds a new instance of the MobileFuse adapter for the given bidder with the given config.
 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
 	template, err := template.New("endpointTemplate").Parse(config.Endpoint)
@@ -79,7 +83,7 @@ func (adapter *MobileFuseAdapter) MakeBids(incomingRequest *openrtb2.BidRequest,
 
 	for _, seatbid := range incomingBidResponse.SeatBid {
 		for i := range seatbid.Bid {
-			bidType := adapter.getBidType(seatbid.Bid[i])
+			bidType := getBidType(seatbid.Bid[i])
 			seatbid.Bid[i].Ext = nil
 
 			outgoingBidResponse.Bids = append(outgoingBidResponse.Bids, &adapters.TypedBid{
@@ -95,7 +99,7 @@ func (adapter *MobileFuseAdapter) MakeBids(incomingRequest *openrtb2.BidRequest,
 func (adapter *MobileFuseAdapter) makeRequest(bidRequest *openrtb2.BidRequest) (*adapters.RequestData, []error) {
 	var errs []error
 
-	mobileFuseExtension, errs := adapter.getFirstMobileFuseExtension(bidRequest)
+	mobileFuseExtension, errs := getFirstMobileFuseExtension(bidRequest)
 
 	if errs != nil {
 		return nil, errs
@@ -107,10 +111,9 @@ func (adapter *MobileFuseAdapter) makeRequest(bidRequest *openrtb2.BidRequest) (
 		return nil, append(errs, err)
 	}
 
-	validImps := adapter.getValidImps(bidRequest, mobileFuseExtension)
+	validImps, err := getValidImps(bidRequest, mobileFuseExtension)
 
-	if len(validImps) == 0 {
-		err := fmt.Errorf("No valid imps")
+	if err != nil {
 		errs = append(errs, err)
 		return nil, errs
 	}
@@ -135,7 +138,7 @@ func (adapter *MobileFuseAdapter) makeRequest(bidRequest *openrtb2.BidRequest) (
 	}, errs
 }
 
-func (adapter *MobileFuseAdapter) getFirstMobileFuseExtension(request *openrtb2.BidRequest) (*openrtb_ext.ExtImpMobileFuse, []error) {
+func getFirstMobileFuseExtension(request *openrtb2.BidRequest) (*openrtb_ext.ExtImpMobileFuse, []error) {
 	var mobileFuseImpExtension openrtb_ext.ExtImpMobileFuse
 	var errs []error
 
@@ -178,23 +181,42 @@ func (adapter *MobileFuseAdapter) getEndpoint(ext *openrtb_ext.ExtImpMobileFuse)
 	return url, nil
 }
 
-func (adapter *MobileFuseAdapter) getValidImps(bidRequest *openrtb2.BidRequest, ext *openrtb_ext.ExtImpMobileFuse) []openrtb2.Imp {
+func getValidImps(bidRequest *openrtb2.BidRequest, ext *openrtb_ext.ExtImpMobileFuse) ([]openrtb2.Imp, error) {
 	var validImps []openrtb2.Imp
 
 	for _, imp := range bidRequest.Imp {
 		if imp.Banner != nil || imp.Video != nil || imp.Native != nil {
 			imp.TagID = strconv.Itoa(ext.PlacementId)
-			imp.Ext = nil
+
+			var extSkadn ExtSkadn
+			err := json.Unmarshal(imp.Ext, &extSkadn)
+			if err != nil {
+				return nil, err
+			}
+
+			if extSkadn.Skadn != nil {
+				imp.Ext, err = json.Marshal(map[string]json.RawMessage{"skadn": extSkadn.Skadn})
+				if err != nil {
+					return nil, err
+				}
+			} else {
+				imp.Ext = nil
+			}
+
 			validImps = append(validImps, imp)
 
 			break
 		}
 	}
 
-	return validImps
+	if len(validImps) == 0 {
+		return nil, fmt.Errorf("No valid imps")
+	}
+
+	return validImps, nil
 }
 
-func (adapter *MobileFuseAdapter) getBidType(bid openrtb2.Bid) openrtb_ext.BidType {
+func getBidType(bid openrtb2.Bid) openrtb_ext.BidType {
 	if bid.Ext != nil {
 		var bidExt BidExt
 		err := json.Unmarshal(bid.Ext, &bidExt)
diff --git a/config/bidderinfo.go b/config/bidderinfo.go
index 6f62488c..b4d79001 100644
--- a/config/bidderinfo.go
+++ b/config/bidderinfo.go
@@ -23,6 +23,7 @@ type BidderInfos map[string]BidderInfo
 
 // BidderInfo specifies all configuration for a bidder except for enabled status, endpoint, and extra information.
 type BidderInfo struct {
+	AliasOf          string `yaml:"aliasOf" mapstructure:"aliasOf"`
 	Disabled         bool   `yaml:"disabled" mapstructure:"disabled"`
 	Endpoint         string `yaml:"endpoint" mapstructure:"endpoint"`
 	ExtraAdapterInfo string `yaml:"extra_info" mapstructure:"extra_info"`
@@ -266,20 +267,35 @@ func (infos BidderInfos) validate(errs []error) []error {
 		if bidder.IsEnabled() {
 			errs = validateAdapterEndpoint(bidder.Endpoint, bidderName, errs)
 
-			validateInfoErr := validateInfo(bidder, bidderName)
-			if validateInfoErr != nil {
-				errs = append(errs, validateInfoErr)
+			if err := validateInfo(bidder, bidderName); err != nil {
+				errs = append(errs, err)
 			}
 
-			validateSyncerErr := validateSyncer(bidder)
-			if validateSyncerErr != nil {
-				errs = append(errs, validateSyncerErr)
+			if err := validateSyncer(bidder); err != nil {
+				errs = append(errs, err)
+			}
+
+			if err := validateAliases(bidder, infos, bidderName); err != nil {
+				errs = append(errs, err)
 			}
 		}
 	}
 	return errs
 }
 
+func validateAliases(bidder BidderInfo, infos BidderInfos, bidderName string) error {
+	if len(bidder.AliasOf) > 0 {
+		if parentBidder, ok := infos[bidder.AliasOf]; ok {
+			if len(parentBidder.AliasOf) > 0 {
+				return fmt.Errorf("bidder: %s cannot be an alias of an alias: %s", bidder.AliasOf, bidderName)
+			}
+		} else {
+			return fmt.Errorf("bidder: %s not found for an alias: %s", bidder.AliasOf, bidderName)
+		}
+	}
+	return nil
+}
+
 var testEndpointTemplateParams = macros.EndpointTemplateParams{
 	Host:        "anyHost",
 	PublisherID: "anyPublisherID",
diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go
index fa154bbc..ef3c40e8 100644
--- a/endpoints/cookie_sync.go
+++ b/endpoints/cookie_sync.go
@@ -90,8 +90,10 @@ func (c *cookieSyncEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ ht
 		c.handleError(w, err, http.StatusBadRequest)
 		return
 	}
+	decoder := usersync.Base64Decoder{}
 
-	cookie := usersync.ParseCookieFromRequest(r, &c.config.HostCookie)
+	cookie := usersync.ReadCookie(r, decoder, &c.config.HostCookie)
+	usersync.SyncHostCookie(r, cookie, &c.config.HostCookie)
 
 	result := c.chooser.Choose(request, cookie)
 	switch result.Status {
diff --git a/endpoints/getuids.go b/endpoints/getuids.go
index ad984a8d..f420c64f 100644
--- a/endpoints/getuids.go
+++ b/endpoints/getuids.go
@@ -18,9 +18,11 @@ type userSyncs struct {
 // returns all the existing syncs for the user
 func NewGetUIDsEndpoint(cfg config.HostCookie) httprouter.Handle {
 	return httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
-		pc := usersync.ParseCookieFromRequest(r, &cfg)
+		cookie := usersync.ReadCookie(r, usersync.Base64Decoder{}, &cfg)
+		usersync.SyncHostCookie(r, cookie, &cfg)
+
 		userSyncs := new(userSyncs)
-		userSyncs.BuyerUIDs = pc.GetUIDs()
+		userSyncs.BuyerUIDs = cookie.GetUIDs()
 		json.NewEncoder(w).Encode(userSyncs)
 	})
 }
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index a37c6874..9e06272b 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -182,12 +182,15 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 	}
 	defer cancel()
 
-	usersyncs := usersync.ParseCookieFromRequest(r, &(deps.cfg.HostCookie))
+	// Read UserSyncs/Cookie from Request
+	usersyncs := usersync.ReadCookie(r, usersync.Base64Decoder{}, &deps.cfg.HostCookie)
+	usersync.SyncHostCookie(r, usersyncs, &deps.cfg.HostCookie)
 	if usersyncs.HasAnyLiveSyncs() {
 		labels.CookieFlag = metrics.CookieFlagYes
 	} else {
 		labels.CookieFlag = metrics.CookieFlagNo
 	}
+
 	labels.PubID = getAccountID(reqWrapper.Site.Publisher)
 	// Look up account now that we have resolved the pubID value
 	account, acctIDErrs := accountService.GetAccount(ctx, deps.cfg, deps.accounts, labels.PubID, deps.metricsEngine)
@@ -251,6 +254,11 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 	}
 
 	auctionResponse, err := deps.ex.HoldAuction(ctx, auctionRequest, nil)
+	defer func() {
+		if !auctionRequest.BidderResponseStartTime.IsZero() {
+			deps.metricsEngine.RecordOverheadTime(metrics.MakeAuctionResponse, time.Since(auctionRequest.BidderResponseStartTime))
+		}
+	}()
 	var response *openrtb2.BidResponse
 	if auctionResponse != nil {
 		response = auctionResponse.BidResponse
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index c5aa88dd..087d0a5d 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -209,7 +209,11 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 		defer cancel()
 	}
 
-	usersyncs := usersync.ParseCookieFromRequest(r, &(deps.cfg.HostCookie))
+	// Read Usersyncs/Cookie
+	decoder := usersync.Base64Decoder{}
+	usersyncs := usersync.ReadCookie(r, decoder, &deps.cfg.HostCookie)
+	usersync.SyncHostCookie(r, usersyncs, &deps.cfg.HostCookie)
+
 	if req.Site != nil {
 		if usersyncs.HasAnyLiveSyncs() {
 			labels.CookieFlag = metrics.CookieFlagYes
@@ -248,6 +252,11 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 		Activities:                 activities,
 	}
 	auctionResponse, err := deps.ex.HoldAuction(ctx, auctionRequest, nil)
+	defer func() {
+		if !auctionRequest.BidderResponseStartTime.IsZero() {
+			deps.metricsEngine.RecordOverheadTime(metrics.MakeAuctionResponse, time.Since(auctionRequest.BidderResponseStartTime))
+		}
+	}()
 	ao.RequestWrapper = req
 	ao.Account = account
 	var response *openrtb2.BidResponse
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index d3731062..565baf02 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -275,7 +275,11 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 		defer cancel()
 	}
 
-	usersyncs := usersync.ParseCookieFromRequest(r, &(deps.cfg.HostCookie))
+	// Read Usersyncs/Cookie
+	decoder := usersync.Base64Decoder{}
+	usersyncs := usersync.ReadCookie(r, decoder, &deps.cfg.HostCookie)
+	usersync.SyncHostCookie(r, usersyncs, &deps.cfg.HostCookie)
+
 	if bidReqWrapper.App != nil {
 		labels.Source = metrics.DemandApp
 		labels.PubID = getAccountID(bidReqWrapper.App.Publisher)
@@ -310,6 +314,11 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	}
 
 	auctionResponse, err := deps.ex.HoldAuction(ctx, auctionRequest, &debugLog)
+	defer func() {
+		if !auctionRequest.BidderResponseStartTime.IsZero() {
+			deps.metricsEngine.RecordOverheadTime(metrics.MakeAuctionResponse, time.Since(auctionRequest.BidderResponseStartTime))
+		}
+	}()
 	vo.RequestWrapper = bidReqWrapper
 	var response *openrtb2.BidResponse
 	if auctionResponse != nil {
diff --git a/endpoints/setuid.go b/endpoints/setuid.go
index a4d04749..905fa26b 100644
--- a/endpoints/setuid.go
+++ b/endpoints/setuid.go
@@ -3,21 +3,26 @@ package endpoints
 import (
 	"context"
 	"errors"
+	"fmt"
 	"net/http"
 	"net/url"
 	"strconv"
 	"strings"
-	"time"
 
 	"github.com/julienschmidt/httprouter"
+	gpplib "github.com/prebid/go-gpp"
+	gppConstants "github.com/prebid/go-gpp/constants"
 	accountService "github.com/prebid/prebid-server/account"
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/metrics"
+	gppPrivacy "github.com/prebid/prebid-server/privacy/gpp"
 	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/usersync"
 	"github.com/prebid/prebid-server/util/httputil"
+	stringutil "github.com/prebid/prebid-server/util/stringutil"
 )
 
 const (
@@ -28,8 +33,11 @@ const (
 	chromeiOSStrLen = len(chromeiOSStr)
 )
 
+const uidCookieName = "uids"
+
 func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]usersync.Syncer, gdprPermsBuilder gdpr.PermissionsBuilder, tcf2CfgBuilder gdpr.TCF2ConfigBuilder, pbsanalytics analytics.PBSAnalyticsModule, accountsFetcher stored_requests.AccountFetcher, metricsEngine metrics.MetricsEngine) httprouter.Handle {
-	cookieTTL := time.Duration(cfg.HostCookie.TTL) * 24 * time.Hour
+	encoder := usersync.Base64Encoder{}
+	decoder := usersync.Base64Decoder{}
 
 	// convert map of syncers by bidder to map of syncers by key
 	// - its safe to assume that if multiple bidders map to the same key, the syncers are interchangeable.
@@ -46,13 +54,14 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use
 
 		defer pbsanalytics.LogSetUIDObject(&so)
 
-		pc := usersync.ParseCookieFromRequest(r, &cfg.HostCookie)
-		if !pc.AllowSyncs() {
+		cookie := usersync.ReadCookie(r, decoder, &cfg.HostCookie)
+		if !cookie.AllowSyncs() {
 			w.WriteHeader(http.StatusUnauthorized)
 			metricsEngine.RecordSetUid(metrics.SetUidOptOut)
 			so.Status = http.StatusUnauthorized
 			return
 		}
+		usersync.SyncHostCookie(r, cookie, &cfg.HostCookie)
 
 		query := r.URL.Query()
 
@@ -101,9 +110,23 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use
 			return
 		}
 
+		gdprRequestInfo, err := extractGDPRInfo(query)
+		if err != nil {
+			// Only exit if non-warning
+			if !errortypes.IsWarning(err) {
+				w.WriteHeader(http.StatusBadRequest)
+				w.Write([]byte(err.Error()))
+				metricsEngine.RecordSetUid(metrics.SetUidBadRequest)
+				so.Errors = []error{err}
+				so.Status = http.StatusBadRequest
+				return
+			}
+			w.Write([]byte("Warning: " + err.Error()))
+		}
+
 		tcf2Cfg := tcf2CfgBuilder(cfg.GDPR.TCF2, account.GDPR)
 
-		if shouldReturn, status, body := preventSyncsGDPR(query.Get("gdpr"), query.Get("gdpr_consent"), gdprPermsBuilder, tcf2Cfg); shouldReturn {
+		if shouldReturn, status, body := preventSyncsGDPR(gdprRequestInfo, gdprPermsBuilder, tcf2Cfg); shouldReturn {
 			w.WriteHeader(status)
 			w.Write([]byte(body))
 			switch status {
@@ -121,18 +144,28 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use
 		so.UID = uid
 
 		if uid == "" {
-			pc.Unsync(syncer.Key())
+			cookie.Unsync(syncer.Key())
 			metricsEngine.RecordSetUid(metrics.SetUidOK)
 			metricsEngine.RecordSyncerSet(syncer.Key(), metrics.SyncerSetUidCleared)
 			so.Success = true
-		} else if err = pc.TrySync(syncer.Key(), uid); err == nil {
+		} else if err = cookie.Sync(syncer.Key(), uid); err == nil {
 			metricsEngine.RecordSetUid(metrics.SetUidOK)
 			metricsEngine.RecordSyncerSet(syncer.Key(), metrics.SyncerSetUidOK)
 			so.Success = true
 		}
 
 		setSiteCookie := siteCookieCheck(r.UserAgent())
-		pc.SetCookieOnResponse(w, setSiteCookie, &cfg.HostCookie, cookieTTL)
+
+		// Write Cookie
+		encodedCookie, err := cookie.PrepareCookieForWrite(&cfg.HostCookie, encoder)
+		if err != nil {
+			w.WriteHeader(http.StatusBadRequest)
+			metricsEngine.RecordSetUid(metrics.SetUidBadRequest)
+			so.Errors = []error{err}
+			so.Status = http.StatusBadRequest
+			return
+		}
+		usersync.WriteCookie(w, encodedCookie, &cfg.HostCookie, setSiteCookie)
 
 		switch responseFormat {
 		case "i":
@@ -148,6 +181,138 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use
 	})
 }
 
+// extractGDPRInfo looks for the GDPR consent string and GDPR signal in the GPP query params
+// first and the 'gdpr' and 'gdpr_consent' query params second. If found in both, throws a
+// warning. Can also throw a parsing or validation error
+func extractGDPRInfo(query url.Values) (reqInfo gdpr.RequestInfo, err error) {
+
+	reqInfo, err = parseGDPRFromGPP(query)
+	if err != nil {
+		return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err
+	}
+
+	legacySignal, legacyConsent, err := parseLegacyGDPRFields(query, reqInfo.GDPRSignal, reqInfo.Consent)
+	isWarning := errortypes.IsWarning(err)
+
+	if err != nil && !isWarning {
+		return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err
+	}
+
+	// If no GDPR data in the GPP fields, use legacy instead
+	if reqInfo.Consent == "" && reqInfo.GDPRSignal == gdpr.SignalAmbiguous {
+		reqInfo.GDPRSignal = legacySignal
+		reqInfo.Consent = legacyConsent
+	}
+
+	if reqInfo.Consent == "" && reqInfo.GDPRSignal == gdpr.SignalYes {
+		return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, errors.New("GDPR consent is required when gdpr signal equals 1")
+	}
+
+	return reqInfo, err
+}
+
+// parseGDPRFromGPP parses and validates the "gpp_sid" and "gpp" query fields.
+func parseGDPRFromGPP(query url.Values) (gdpr.RequestInfo, error) {
+	var gdprSignal gdpr.Signal = gdpr.SignalAmbiguous
+	var gdprConsent string = ""
+	var err error
+
+	gdprSignal, err = parseSignalFromGppSidStr(query.Get("gpp_sid"))
+	if err != nil {
+		return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err
+	}
+
+	gdprConsent, err = parseConsentFromGppStr(query.Get("gpp"))
+	if err != nil {
+		return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err
+	}
+
+	return gdpr.RequestInfo{
+		Consent:    gdprConsent,
+		GDPRSignal: gdprSignal,
+	}, nil
+}
+
+// parseLegacyGDPRFields parses and validates the "gdpr" and "gdpr_consent" query fields which
+// are considered deprecated in favor of the "gpp" and "gpp_sid". The parsed and validated GDPR
+// values contained in "gpp" and "gpp_sid" are passed in the parameters gppGDPRSignal and
+// gppGDPRConsent. If the GPP parameters come with non-default values, this function discards
+// "gdpr" and "gdpr_consent" and returns a warning.
+func parseLegacyGDPRFields(query url.Values, gppGDPRSignal gdpr.Signal, gppGDPRConsent string) (gdpr.Signal, string, error) {
+	var gdprSignal gdpr.Signal = gdpr.SignalAmbiguous
+	var gdprConsent string
+	var warning error
+
+	if gdprQuerySignal := query.Get("gdpr"); len(gdprQuerySignal) > 0 {
+		if gppGDPRSignal == gdpr.SignalAmbiguous {
+			switch gdprQuerySignal {
+			case "0":
+				fallthrough
+			case "1":
+				if zeroOrOne, err := strconv.Atoi(gdprQuerySignal); err == nil {
+					gdprSignal = gdpr.Signal(zeroOrOne)
+				}
+			default:
+				return gdpr.SignalAmbiguous, "", errors.New("the gdpr query param must be either 0 or 1. You gave " + gdprQuerySignal)
+			}
+		} else {
+			warning = &errortypes.Warning{
+				Message:     "'gpp_sid' signal value will be used over the one found in the deprecated 'gdpr' field.",
+				WarningCode: errortypes.UnknownWarningCode,
+			}
+		}
+	}
+
+	if gdprLegacyConsent := query.Get("gdpr_consent"); len(gdprLegacyConsent) > 0 {
+		if len(gppGDPRConsent) > 0 {
+			warning = &errortypes.Warning{
+				Message:     "'gpp' value will be used over the one found in the deprecated 'gdpr_consent' field.",
+				WarningCode: errortypes.UnknownWarningCode,
+			}
+		} else {
+			gdprConsent = gdprLegacyConsent
+		}
+	}
+	return gdprSignal, gdprConsent, warning
+}
+
+func parseSignalFromGppSidStr(strSID string) (gdpr.Signal, error) {
+	gdprSignal := gdpr.SignalAmbiguous
+
+	if len(strSID) > 0 {
+		gppSID, err := stringutil.StrToInt8Slice(strSID)
+		if err != nil {
+			return gdpr.SignalAmbiguous, fmt.Errorf("Error parsing gpp_sid %s", err.Error())
+		}
+
+		if len(gppSID) > 0 {
+			gdprSignal = gdpr.SignalNo
+			if gppPrivacy.IsSIDInList(gppSID, gppConstants.SectionTCFEU2) {
+				gdprSignal = gdpr.SignalYes
+			}
+		}
+	}
+
+	return gdprSignal, nil
+}
+
+func parseConsentFromGppStr(gppQueryValue string) (string, error) {
+	var gdprConsent string
+
+	if len(gppQueryValue) > 0 {
+		gpp, err := gpplib.Parse(gppQueryValue)
+		if err != nil {
+			return "", err
+		}
+
+		if i := gppPrivacy.IndexOfSID(gpp, gppConstants.SectionTCFEU2); i >= 0 {
+			gdprConsent = gpp.Sections[i].GetValue()
+		}
+	}
+
+	return gdprConsent, nil
+}
+
 func getSyncer(query url.Values, syncersByKey map[string]usersync.Syncer) (usersync.Syncer, error) {
 	key := query.Get("bidder")
 
@@ -216,26 +381,7 @@ func checkChromeBrowserVersion(ua string, index int, chromeStrLength int) bool {
 	return result
 }
 
-func preventSyncsGDPR(gdprEnabled string, gdprConsent string, permsBuilder gdpr.PermissionsBuilder, tcf2Cfg gdpr.TCF2ConfigReader) (shouldReturn bool, status int, body string) {
-	if gdprEnabled != "" && gdprEnabled != "0" && gdprEnabled != "1" {
-		return true, http.StatusBadRequest, "the gdpr query param must be either 0 or 1. You gave " + gdprEnabled
-	}
-
-	if gdprEnabled == "1" && gdprConsent == "" {
-		return true, http.StatusBadRequest, "gdpr_consent is required when gdpr=1"
-	}
-
-	gdprSignal := gdpr.SignalAmbiguous
-
-	if i, err := strconv.Atoi(gdprEnabled); err == nil {
-		gdprSignal = gdpr.Signal(i)
-	}
-
-	gdprRequestInfo := gdpr.RequestInfo{
-		Consent:    gdprConsent,
-		GDPRSignal: gdprSignal,
-	}
-
+func preventSyncsGDPR(gdprRequestInfo gdpr.RequestInfo, permsBuilder gdpr.PermissionsBuilder, tcf2Cfg gdpr.TCF2ConfigReader) (shouldReturn bool, status int, body string) {
 	perms := permsBuilder(tcf2Cfg, gdprRequestInfo)
 
 	allowed, err := perms.HostCookiesAllowed(context.Background())
diff --git a/errortypes/errortypes.go b/errortypes/errortypes.go
index d93075b7..aff04822 100644
--- a/errortypes/errortypes.go
+++ b/errortypes/errortypes.go
@@ -183,7 +183,8 @@ func (err *MalformedAcct) Severity() Severity {
 	return SeverityFatal
 }
 
-// Warning is a generic non-fatal error.
+// Warning is a generic non-fatal error. Throughout the codebase, an error can
+// only be a warning if it's of the type defined below
 type Warning struct {
 	Message     string
 	WarningCode int
diff --git a/errortypes/severity.go b/errortypes/severity.go
index 0838b095..5f9cd80d 100644
--- a/errortypes/severity.go
+++ b/errortypes/severity.go
@@ -20,7 +20,10 @@ func isFatal(err error) bool {
 	return !ok || s.Severity() == SeverityFatal
 }
 
-func isWarning(err error) bool {
+// IsWarning returns true if an error is labeled with a Severity of SeverityWarning
+// Throughout the codebase, errors with SeverityWarning are of the type Warning
+// defined in this package
+func IsWarning(err error) bool {
 	s, ok := err.(Coder)
 	return ok && s.Severity() == SeverityWarning
 }
@@ -54,7 +57,7 @@ func WarningOnly(errs []error) []error {
 	errsWarning := make([]error, 0, len(errs))
 
 	for _, err := range errs {
-		if isWarning(err) {
+		if IsWarning(err) {
 			errsWarning = append(errsWarning, err)
 		}
 	}
diff --git a/exchange/adapter_util.go b/exchange/adapter_util.go
index ad1866a6..9551ab3e 100644
--- a/exchange/adapter_util.go
+++ b/exchange/adapter_util.go
@@ -33,6 +33,10 @@ func buildBidders(infos config.BidderInfos, builders map[openrtb_ext.BidderName]
 	var errs []error
 
 	for bidder, info := range infos {
+		if len(info.AliasOf) > 0 {
+			errs = append(errs, fmt.Errorf("This feature is currently under development"))
+			continue
+		}
 		bidderName, bidderNameFound := openrtb_ext.NormalizeBidderName(bidder)
 		if !bidderNameFound {
 			errs = append(errs, fmt.Errorf("%v: unknown bidder", bidder))
diff --git a/exchange/exchange.go b/exchange/exchange.go
index e1a9868d..390db973 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -205,11 +205,12 @@ type AuctionRequest struct {
 	// map of imp id to stored response
 	StoredAuctionResponses stored_responses.ImpsWithBidResponses
 	// map of imp id to bidder to stored response
-	StoredBidResponses    stored_responses.ImpBidderStoredResp
-	BidderImpReplaceImpID stored_responses.BidderImpReplaceImpID
-	PubID                 string
-	HookExecutor          hookexecution.StageExecutor
-	QueryParams           url.Values
+	StoredBidResponses      stored_responses.ImpBidderStoredResp
+	BidderImpReplaceImpID   stored_responses.BidderImpReplaceImpID
+	PubID                   string
+	HookExecutor            hookexecution.StageExecutor
+	QueryParams             url.Values
+	BidderResponseStartTime time.Time
 }
 
 // BidderRequest holds the bidder specific request and all other
@@ -370,6 +371,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog
 		adapterBids, adapterExtra, extraRespInfo = e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, accountDebugAllow, r.GlobalPrivacyControlHeader, debugLog.DebugOverride, alternateBidderCodes, requestExtLegacy.Prebid.Experiment, r.HookExecutor, r.StartTime, bidAdjustmentRules)
 		fledge = extraRespInfo.fledge
 		anyBidsReturned = extraRespInfo.bidsFound
+		r.BidderResponseStartTime = extraRespInfo.bidderResponseStartTime
 	}
 
 	var (
diff --git a/gdpr/consent_parser.go b/gdpr/consent_parser.go
new file mode 100644
index 00000000..3a354886
--- /dev/null
+++ b/gdpr/consent_parser.go
@@ -0,0 +1,67 @@
+package gdpr
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/prebid/go-gdpr/api"
+	"github.com/prebid/go-gdpr/vendorconsent"
+	tcf2 "github.com/prebid/go-gdpr/vendorconsent/tcf2"
+)
+
+// parsedConsent represents a parsed consent string containing notable version information and a convenient
+// metadata object that allows easy examination of encoded purpose and vendor information
+type parsedConsent struct {
+	encodingVersion uint8
+	listVersion     uint16
+	specVersion     uint16
+	consentMeta     tcf2.ConsentMetadata
+}
+
+// parseConsent parses and validates the specified consent string returning an instance of parsedConsent
+func parseConsent(consent string) (*parsedConsent, error) {
+	pc, err := vendorconsent.ParseString(consent)
+	if err != nil {
+		return nil, &ErrorMalformedConsent{
+			Consent: consent,
+			Cause:   err,
+		}
+	}
+	if err = validateVersions(pc); err != nil {
+		return nil, &ErrorMalformedConsent{
+			Consent: consent,
+			Cause:   err,
+		}
+	}
+	cm, ok := pc.(tcf2.ConsentMetadata)
+	if !ok {
+		err = errors.New("Unable to access TCF2 parsed consent")
+		return nil, err
+	}
+	return &parsedConsent{
+		encodingVersion: pc.Version(),
+		listVersion:     pc.VendorListVersion(),
+		specVersion:     getSpecVersion(pc.TCFPolicyVersion()),
+		consentMeta:     cm,
+	}, nil
+}
+
+// validateVersions ensures that certain version fields in the consent string contain valid values.
+// An error is returned if at least one of them is invalid
+func validateVersions(pc api.VendorConsents) (err error) {
+	version := pc.Version()
+	if version != 2 {
+		return fmt.Errorf("invalid encoding format version: %d", version)
+	}
+	return
+}
+
+// getSpecVersion looks at the TCF policy version and determines the corresponding GVL specification
+// version that should be used to calculate legal basis. A zero value is returned if the policy version
+// is invalid
+func getSpecVersion(policyVersion uint8) uint16 {
+	if policyVersion >= 4 {
+		return 3
+	}
+	return 2
+}
diff --git a/gdpr/full_enforcement.go b/gdpr/full_enforcement.go
index b86f7a3f..eefa28d5 100644
--- a/gdpr/full_enforcement.go
+++ b/gdpr/full_enforcement.go
@@ -67,6 +67,9 @@ func (fe *FullEnforcement) applyEnforceOverrides(overrides Overrides) (enforcePu
 // must declare the purpose as either consent or flex and the user must consent in accordance with
 // the purpose configs.
 func (fe *FullEnforcement) consentEstablished(consent tcf2.ConsentMetadata, vi VendorInfo, enforcePurpose bool, enforceVendors bool) bool {
+	if vi.vendor == nil {
+		return false
+	}
 	if !vi.vendor.Purpose(fe.cfg.PurposeID) {
 		return false
 	}
@@ -84,6 +87,9 @@ func (fe *FullEnforcement) consentEstablished(consent tcf2.ConsentMetadata, vi V
 // established, the vendor must declare the purpose as either legit interest or flex and the user
 // must have been provided notice for the legit interest basis in accordance with the purpose configs.
 func (fe *FullEnforcement) legitInterestEstablished(consent tcf2.ConsentMetadata, vi VendorInfo, enforcePurpose bool, enforceVendors bool) bool {
+	if vi.vendor == nil {
+		return false
+	}
 	if !vi.vendor.LegitimateInterest(fe.cfg.PurposeID) {
 		return false
 	}
diff --git a/gdpr/impl.go b/gdpr/impl.go
index 4869a84d..cc88a1fd 100644
--- a/gdpr/impl.go
+++ b/gdpr/impl.go
@@ -2,12 +2,9 @@ package gdpr
 
 import (
 	"context"
-	"errors"
-	"fmt"
 
 	"github.com/prebid/go-gdpr/api"
 	"github.com/prebid/go-gdpr/consentconstants"
-	"github.com/prebid/go-gdpr/vendorconsent"
 	tcf2 "github.com/prebid/go-gdpr/vendorconsent/tcf2"
 	"github.com/prebid/prebid-server/openrtb_ext"
 )
@@ -63,53 +60,27 @@ func (p *permissionsImpl) AuctionActivitiesAllowed(ctx context.Context, bidderCo
 	if _, ok := p.nonStandardPublishers[p.publisherID]; ok {
 		return AllowAll, nil
 	}
-
 	if p.gdprSignal != SignalYes {
 		return AllowAll, nil
 	}
-
 	if p.consent == "" {
 		return p.defaultPermissions(), nil
 	}
-
-	// note the bidder here is guaranteed to be enabled
-	vendorID, vendorFound := p.resolveVendorID(bidderCoreName, bidder)
-	basicEnforcementVendors := p.cfg.BasicEnforcementVendors()
-	_, weakVendorEnforcement := basicEnforcementVendors[string(bidder)]
-
-	if !vendorFound && !weakVendorEnforcement {
-		return DenyAll, nil
-	}
-
-	parsedConsent, vendor, err := p.parseVendor(ctx, vendorID, p.consent)
+	pc, err := parseConsent(p.consent)
 	if err != nil {
 		return p.defaultPermissions(), err
 	}
-
-	// vendor will be nil if not a valid TCF2 consent string
-	if vendor == nil {
-		if weakVendorEnforcement && parsedConsent.Version() == 2 {
-			vendor = vendorTrue{}
-		} else {
-			return p.defaultPermissions(), nil
-		}
-	}
-
-	if !p.cfg.IsEnabled() {
-		return AllowBidRequestOnly, nil
-	}
-
-	consentMeta, ok := parsedConsent.(tcf2.ConsentMetadata)
-	if !ok {
-		err = fmt.Errorf("Unable to access TCF2 parsed consent")
+	vendorID, _ := p.resolveVendorID(bidderCoreName, bidder)
+	vendor, err := p.getVendor(ctx, vendorID, *pc)
+	if err != nil {
 		return p.defaultPermissions(), err
 	}
-
 	vendorInfo := VendorInfo{vendorID: vendorID, vendor: vendor}
+
 	permissions = AuctionPermissions{}
-	permissions.AllowBidRequest = p.allowBidRequest(bidderCoreName, consentMeta, vendorInfo)
-	permissions.PassGeo = p.allowGeo(bidderCoreName, consentMeta, vendor)
-	permissions.PassID = p.allowID(bidderCoreName, consentMeta, vendorInfo)
+	permissions.AllowBidRequest = p.allowBidRequest(bidderCoreName, pc.consentMeta, vendorInfo)
+	permissions.PassGeo = p.allowGeo(bidderCoreName, pc.consentMeta, vendor)
+	permissions.PassID = p.allowID(bidderCoreName, pc.consentMeta, vendorInfo)
 
 	return permissions, nil
 }
@@ -148,34 +119,28 @@ func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, bidder
 	if p.consent == "" {
 		return false, nil
 	}
-
-	parsedConsent, vendor, err := p.parseVendor(ctx, vendorID, p.consent)
+	pc, err := parseConsent(p.consent)
 	if err != nil {
 		return false, err
 	}
-
-	if vendor == nil {
+	vendor, err := p.getVendor(ctx, vendorID, *pc)
+	if err != nil {
 		return false, nil
 	}
+	vendorInfo := VendorInfo{vendorID: vendorID, vendor: vendor}
 
 	if !p.cfg.PurposeEnforced(consentconstants.Purpose(1)) {
 		return true, nil
 	}
-	consentMeta, ok := parsedConsent.(tcf2.ConsentMetadata)
-	if !ok {
-		err := errors.New("Unable to access TCF2 parsed consent")
-		return false, err
-	}
 
-	if p.cfg.PurposeOneTreatmentEnabled() && consentMeta.PurposeOneTreatment() {
+	if p.cfg.PurposeOneTreatmentEnabled() && pc.consentMeta.PurposeOneTreatment() {
 		return p.cfg.PurposeOneTreatmentAccessAllowed(), nil
 	}
 
 	purpose := consentconstants.Purpose(1)
 	enforcer := p.purposeEnforcerBuilder(purpose, bidder)
 
-	vendorInfo := VendorInfo{vendorID: vendorID, vendor: vendor}
-	if enforcer.LegalBasis(vendorInfo, bidder, consentMeta, Overrides{blockVendorExceptions: !vendorException}) {
+	if enforcer.LegalBasis(vendorInfo, bidder, pc.consentMeta, Overrides{blockVendorExceptions: !vendorException}) {
 		return true, nil
 	}
 	return false, nil
@@ -205,7 +170,7 @@ func (p *permissionsImpl) allowGeo(bidder openrtb_ext.BidderName, consentMeta tc
 
 	basicEnforcementVendors := p.cfg.BasicEnforcementVendors()
 	_, weakVendorEnforcement := basicEnforcementVendors[string(bidder)]
-	return consentMeta.SpecialFeatureOptIn(1) && (vendor.SpecialFeature(1) || weakVendorEnforcement)
+	return consentMeta.SpecialFeatureOptIn(1) && ((vendor != nil && vendor.SpecialFeature(1)) || weakVendorEnforcement)
 }
 
 // allowID computes the pass user ID activity legal basis for a given bidder using the enforcement algorithms
@@ -229,53 +194,13 @@ func (p *permissionsImpl) allowID(bidder openrtb_ext.BidderName, consentMeta tcf
 	return false
 }
 
-// parseVendor parses the consent string and fetches the specified vendor's information from the GVL
-func (p *permissionsImpl) parseVendor(ctx context.Context, vendorID uint16, consent string) (parsedConsent api.VendorConsents, vendor api.Vendor, err error) {
-	parsedConsent, err = vendorconsent.ParseString(consent)
+// getVendor retrieves the GVL vendor information for a particular bidder
+func (p *permissionsImpl) getVendor(ctx context.Context, vendorID uint16, pc parsedConsent) (api.Vendor, error) {
+	vendorList, err := p.fetchVendorList(ctx, pc.specVersion, pc.listVersion)
 	if err != nil {
-		err = &ErrorMalformedConsent{
-			Consent: consent,
-			Cause:   err,
-		}
-		return
-	}
-
-	version := parsedConsent.Version()
-	if version != 2 {
-		return
-	}
-
-	policyVersion := parsedConsent.TCFPolicyVersion()
-	specVersion, err := getSpecVersion(policyVersion)
-	if err != nil {
-		err = &ErrorMalformedConsent{
-			Consent: consent,
-			Cause:   err,
-		}
-		return
-	}
-
-	vendorList, err := p.fetchVendorList(ctx, uint16(specVersion), parsedConsent.VendorListVersion())
-	if err != nil {
-		return
-	}
-
-	vendor = vendorList.Vendor(vendorID)
-	return
-}
-
-// getSpecVersion looks at the TCF policy version and determines the corresponding GVL specification
-// version that should be used to calculate legal basis
-func getSpecVersion(policyVersion uint8) (uint16, error) {
-	var specVersion uint16 = 3
-
-	if policyVersion > 4 {
-		return 0, fmt.Errorf("invalid TCF policy version %d", policyVersion)
+		return nil, err
 	}
-	if policyVersion < 4 {
-		specVersion = 2
-	}
-	return specVersion, nil
+	return vendorList.Vendor(vendorID), nil
 }
 
 // AllowHostCookies represents a GDPR permissions policy with host cookie syncing always allowed
@@ -300,25 +225,3 @@ func (a AlwaysAllow) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.B
 func (a AlwaysAllow) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions AuctionPermissions, err error) {
 	return AllowAll, nil
 }
-
-// vendorTrue claims everything.
-type vendorTrue struct{}
-
-func (v vendorTrue) Purpose(purposeID consentconstants.Purpose) bool {
-	return true
-}
-func (v vendorTrue) PurposeStrict(purposeID consentconstants.Purpose) bool {
-	return true
-}
-func (v vendorTrue) LegitimateInterest(purposeID consentconstants.Purpose) bool {
-	return true
-}
-func (v vendorTrue) LegitimateInterestStrict(purposeID consentconstants.Purpose) bool {
-	return true
-}
-func (v vendorTrue) SpecialFeature(featureID consentconstants.SpecialFeature) (hasSpecialFeature bool) {
-	return true
-}
-func (v vendorTrue) SpecialPurpose(purposeID consentconstants.Purpose) (hasSpecialPurpose bool) {
-	return true
-}
diff --git a/metrics/prometheus/prometheus.go b/metrics/prometheus/prometheus.go
index eb387789..f9d0c4d9 100644
--- a/metrics/prometheus/prometheus.go
+++ b/metrics/prometheus/prometheus.go
@@ -178,7 +178,7 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet
 	cacheWriteTimeBuckets := []float64{0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 1}
 	priceBuckets := []float64{250, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000}
 	queuedRequestTimeBuckets := []float64{0, 1, 5, 30, 60, 120, 180, 240, 300}
-	overheadTimeBuckets := []float64{0.00005, 0.0001, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.05}
+	overheadTimeBuckets := []float64{0.05, 0.06, 0.07, 0.08, 0.09, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1}
 
 	metrics := Metrics{}
 	reg := prometheus.NewRegistry()
diff --git a/pbs/usersync.go b/pbs/usersync.go
index 748581af..7b468cb0 100644
--- a/pbs/usersync.go
+++ b/pbs/usersync.go
@@ -58,6 +58,8 @@ func (deps *UserSyncDeps) VerifyRecaptcha(response string) error {
 func (deps *UserSyncDeps) OptOut(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
 	optout := r.FormValue("optout")
 	rr := r.FormValue("g-recaptcha-response")
+	encoder := usersync.Base64Encoder{}
+	decoder := usersync.Base64Decoder{}
 
 	if rr == "" {
 		http.Redirect(w, r, fmt.Sprintf("%s/static/optout.html", deps.ExternalUrl), http.StatusMovedPermanently)
@@ -73,10 +75,18 @@ func (deps *UserSyncDeps) OptOut(w http.ResponseWriter, r *http.Request, _ httpr
 		return
 	}
 
-	pc := usersync.ParseCookieFromRequest(r, deps.HostCookieConfig)
+	// Read Cookie
+	pc := usersync.ReadCookie(r, decoder, deps.HostCookieConfig)
+	usersync.SyncHostCookie(r, pc, deps.HostCookieConfig)
 	pc.SetOptOut(optout != "")
 
-	pc.SetCookieOnResponse(w, false, deps.HostCookieConfig, deps.HostCookieConfig.TTLDuration())
+	// Write Cookie
+	encodedCookie, err := pc.PrepareCookieForWrite(deps.HostCookieConfig, encoder)
+	if err != nil {
+		w.WriteHeader(http.StatusBadRequest)
+		return
+	}
+	usersync.WriteCookie(w, encodedCookie, deps.HostCookieConfig, false)
 
 	if optout == "" {
 		http.Redirect(w, r, deps.HostCookieConfig.OptInURL, http.StatusMovedPermanently)
diff --git a/privacy/gpp/gpp.go b/privacy/gpp/gpp.go
index 2ebf4b97..35000406 100644
--- a/privacy/gpp/gpp.go
+++ b/privacy/gpp/gpp.go
@@ -23,9 +23,10 @@ func IsSIDInList(gppSIDs []int8, sid gppConstants.SectionID) bool {
 	return false
 }
 
-// IndexOfSID returns a zero or non-negative integer that represents the position of the 'sid' value in the
-// 'gpp.SectionTypes' array. If the 'sid' value is not found, returns -1. This logic is used in
-// more than one place in our codebase, therefore it was decided to make it its own function.
+// IndexOfSID returns a zero or non-negative integer that represents the position of
+// the 'sid' value in the 'gpp.SectionTypes' array. If the 'sid' value is not found,
+// returns -1. This logic is used in more than one place in our codebase, therefore
+// it was decided to make it its own function.
 func IndexOfSID(gpp gpplib.GppContainer, sid gppConstants.SectionID) int {
 	for i, id := range gpp.SectionTypes {
 		if id == sid {
diff --git a/server/server.go b/server/server.go
index 31764cc4..9282f0fc 100644
--- a/server/server.go
+++ b/server/server.go
@@ -20,7 +20,7 @@ import (
 
 // Listen blocks forever, serving PBS requests on the given port. This will block forever, until the process is shut down.
 func Listen(cfg *config.Configuration, handler http.Handler, adminHandler http.Handler, metrics *metricsconfig.DetailedMetricsEngine) (err error) {
-	stopSignals := make(chan os.Signal)
+	stopSignals := make(chan os.Signal, 1)
 	signal.Notify(stopSignals, syscall.SIGTERM, syscall.SIGINT)
 
 	// Run the servers. Fan any process-stopper signals out to each server for graceful shutdowns.
diff --git a/usersync/cookie.go b/usersync/cookie.go
index 4f80fa5d..c0eb898c 100644
--- a/usersync/cookie.go
+++ b/usersync/cookie.go
@@ -1,11 +1,10 @@
 package usersync
 
 import (
-	"encoding/base64"
 	"encoding/json"
 	"errors"
-	"math"
 	"net/http"
+	"sort"
 	"time"
 
 	"github.com/prebid/prebid-server/config"
@@ -20,70 +19,155 @@ const uidTTL = 14 * 24 * time.Hour
 
 // Cookie is the cookie used in Prebid Server.
 //
-// To get an instance of this from a request, use ParseCookieFromRequest.
-// To write an instance onto a response, use SetCookieOnResponse.
+// To get an instance of this from a request, use ReadCookie.
+// To write an instance onto a response, use WriteCookie.
 type Cookie struct {
-	uids   map[string]uidWithExpiry
+	uids   map[string]UIDEntry
 	optOut bool
 }
 
-// uidWithExpiry bundles the UID with an Expiration date.
-// After the expiration, the UID is no longer valid.
-type uidWithExpiry struct {
+// UIDEntry bundles the UID with an Expiration date.
+type UIDEntry struct {
 	// UID is the ID given to a user by a particular bidder
 	UID string `json:"uid"`
 	// Expires is the time at which this UID should no longer apply.
 	Expires time.Time `json:"expires"`
 }
 
-// ParseCookieFromRequest parses the UserSyncMap from an HTTP Request.
-func ParseCookieFromRequest(r *http.Request, cookie *config.HostCookie) *Cookie {
-	if cookie.OptOutCookie.Name != "" {
-		optOutCookie, err1 := r.Cookie(cookie.OptOutCookie.Name)
-		if err1 == nil && optOutCookie.Value == cookie.OptOutCookie.Value {
-			pc := NewCookie()
-			pc.SetOptOut(true)
-			return pc
-		}
+// NewCookie returns a new empty cookie.
+func NewCookie() *Cookie {
+	return &Cookie{
+		uids: make(map[string]UIDEntry),
 	}
-	var parsed *Cookie
-	uidCookie, err2 := r.Cookie(uidCookieName)
-	if err2 == nil {
-		parsed = ParseCookie(uidCookie)
-	} else {
-		parsed = NewCookie()
+}
+
+// ReadCookie reads the cookie from the request
+func ReadCookie(r *http.Request, decoder Decoder, host *config.HostCookie) *Cookie {
+	if hostOptOutCookie := checkHostCookieOptOut(r, host); hostOptOutCookie != nil {
+		return hostOptOutCookie
+	}
+
+	// Read cookie from request
+	cookieFromRequest, err := r.Cookie(uidCookieName)
+	if err != nil {
+		return NewCookie()
 	}
-	// Fixes #582
-	if uid, _, _ := parsed.GetUID(cookie.Family); uid == "" && cookie.CookieName != "" {
-		if hostCookie, err := r.Cookie(cookie.CookieName); err == nil {
-			parsed.TrySync(cookie.Family, hostCookie.Value)
+	decodedCookie := decoder.Decode(cookieFromRequest.Value)
+
+	return decodedCookie
+}
+
+// PrepareCookieForWrite ejects UIDs as long as the cookie is too full
+func (cookie *Cookie) PrepareCookieForWrite(cfg *config.HostCookie, encoder Encoder) (string, error) {
+	uuidKeys := sortUIDs(cookie.uids)
+
+	i := 0
+	for len(cookie.uids) > 0 {
+		encodedCookie, err := encoder.Encode(cookie)
+		if err != nil {
+			return encodedCookie, nil
+		}
+
+		// Convert to HTTP Cookie to Get Size
+		httpCookie := &http.Cookie{
+			Name:    uidCookieName,
+			Value:   encodedCookie,
+			Expires: time.Now().Add(cfg.TTLDuration()),
+			Path:    "/",
+		}
+		cookieSize := len([]byte(httpCookie.String()))
+
+		isCookieTooBig := cookieSize > cfg.MaxCookieSizeBytes && cfg.MaxCookieSizeBytes > 0
+		if !isCookieTooBig {
+			return encodedCookie, nil
 		}
+
+		uidToDelete := uuidKeys[i]
+		delete(cookie.uids, uidToDelete)
+
+		i++
 	}
-	return parsed
+	return "", nil
 }
 
-// ParseCookie parses the UserSync cookie from a raw HTTP cookie.
-func ParseCookie(httpCookie *http.Cookie) *Cookie {
-	jsonValue, err := base64.URLEncoding.DecodeString(httpCookie.Value)
-	if err != nil {
-		// corrupted cookie; we should reset
-		return NewCookie()
+// WriteCookie sets the prepared cookie onto the header
+func WriteCookie(w http.ResponseWriter, encodedCookie string, cfg *config.HostCookie, setSiteCookie bool) {
+	ttl := cfg.TTLDuration()
+
+	httpCookie := &http.Cookie{
+		Name:    uidCookieName,
+		Value:   encodedCookie,
+		Expires: time.Now().Add(ttl),
+		Path:    "/",
 	}
 
-	var cookie Cookie
-	if err = json.Unmarshal(jsonValue, &cookie); err != nil {
-		// corrupted cookie; we should reset
-		return NewCookie()
+	if cfg.Domain != "" {
+		httpCookie.Domain = cfg.Domain
+	}
+
+	if setSiteCookie {
+		httpCookie.Secure = true
+		httpCookie.SameSite = http.SameSiteNoneMode
 	}
 
-	return &cookie
+	w.Header().Add("Set-Cookie", httpCookie.String())
 }
 
-// NewCookie returns a new empty cookie.
-func NewCookie() *Cookie {
-	return &Cookie{
-		uids: make(map[string]uidWithExpiry),
+// Sync tries to set the UID for some syncer key. It returns an error if the set didn't happen.
+func (cookie *Cookie) Sync(key string, uid string) error {
+	if !cookie.AllowSyncs() {
+		return errors.New("the user has opted out of prebid server cookie syncs")
+	}
+
+	if checkAudienceNetwork(key, uid) {
+		return errors.New("audienceNetwork uses a UID of 0 as \"not yet recognized\"")
 	}
+
+	// Sync
+	cookie.uids[key] = UIDEntry{
+		UID:     uid,
+		Expires: time.Now().Add(uidTTL),
+	}
+
+	return nil
+}
+
+// sortUIDs is used to get a list of uids sorted from oldest to newest
+// This list is used to eject oldest uids from the cookie
+// This will be incorporated with a more complex ejection framework in a future PR
+func sortUIDs(uids map[string]UIDEntry) []string {
+	if len(uids) > 0 {
+		uuidKeys := make([]string, 0, len(uids))
+		for key := range uids {
+			uuidKeys = append(uuidKeys, key)
+		}
+		sort.SliceStable(uuidKeys, func(i, j int) bool {
+			return uids[uuidKeys[i]].Expires.Before(uids[uuidKeys[j]].Expires)
+		})
+		return uuidKeys
+	}
+	return nil
+}
+
+// SyncHostCookie syncs the request cookie with the host cookie
+func SyncHostCookie(r *http.Request, requestCookie *Cookie, host *config.HostCookie) {
+	if uid, _, _ := requestCookie.GetUID(host.Family); uid == "" && host.CookieName != "" {
+		if hostCookie, err := r.Cookie(host.CookieName); err == nil {
+			requestCookie.Sync(host.Family, hostCookie.Value)
+		}
+	}
+}
+
+func checkHostCookieOptOut(r *http.Request, host *config.HostCookie) *Cookie {
+	if host.OptOutCookie.Name != "" {
+		optOutCookie, err := r.Cookie(host.OptOutCookie.Name)
+		if err == nil && optOutCookie.Value == host.OptOutCookie.Value {
+			hostOptOut := NewCookie()
+			hostOptOut.SetOptOut(true)
+			return hostOptOut
+		}
+	}
+	return nil
 }
 
 // AllowSyncs is true if the user lets bidders sync cookies, and false otherwise.
@@ -96,30 +180,12 @@ func (cookie *Cookie) SetOptOut(optOut bool) {
 	cookie.optOut = optOut
 
 	if optOut {
-		cookie.uids = make(map[string]uidWithExpiry)
-	}
-}
-
-// Gets an HTTP cookie containing all the data from this UserSyncMap. This is a snapshot--not a live view.
-func (cookie *Cookie) ToHTTPCookie(ttl time.Duration) *http.Cookie {
-	j, _ := json.Marshal(cookie)
-	b64 := base64.URLEncoding.EncodeToString(j)
-
-	return &http.Cookie{
-		Name:    uidCookieName,
-		Value:   b64,
-		Expires: time.Now().Add(ttl),
-		Path:    "/",
+		cookie.uids = make(map[string]UIDEntry)
 	}
 }
 
 // GetUID Gets this user's ID for the given syncer key.
-// The first returned value is the user's ID.
-// The second returned value is true if we had a value stored, and false if we didn't.
-// The third returned value is true if that value is "active", and false if it's expired.
-//
-// If no value was stored, then the "isActive" return value will be false.
-func (cookie *Cookie) GetUID(key string) (string, bool, bool) {
+func (cookie *Cookie) GetUID(key string) (uid string, isUIDFound bool, isUIDActive bool) {
 	if cookie != nil {
 		if uid, ok := cookie.uids[key]; ok {
 			return uid.UID, true, time.Now().Before(uid.Expires)
@@ -140,41 +206,6 @@ func (cookie *Cookie) GetUIDs() map[string]string {
 	return uids
 }
 
-// SetCookieOnResponse is a shortcut for "ToHTTPCookie(); cookie.setDomain(domain); setCookie(w, cookie)"
-func (cookie *Cookie) SetCookieOnResponse(w http.ResponseWriter, setSiteCookie bool, cfg *config.HostCookie, ttl time.Duration) {
-	httpCookie := cookie.ToHTTPCookie(ttl)
-	var domain string = cfg.Domain
-
-	if domain != "" {
-		httpCookie.Domain = domain
-	}
-
-	var currSize int = len([]byte(httpCookie.String()))
-	for cfg.MaxCookieSizeBytes > 0 && currSize > cfg.MaxCookieSizeBytes && len(cookie.uids) > 0 {
-		var oldestElem string = ""
-		var oldestDate int64 = math.MaxInt64
-		for key, value := range cookie.uids {
-			timeUntilExpiration := time.Until(value.Expires)
-			if timeUntilExpiration < time.Duration(oldestDate) {
-				oldestElem = key
-				oldestDate = int64(timeUntilExpiration)
-			}
-		}
-		delete(cookie.uids, oldestElem)
-		httpCookie = cookie.ToHTTPCookie(ttl)
-		if domain != "" {
-			httpCookie.Domain = domain
-		}
-		currSize = len([]byte(httpCookie.String()))
-	}
-
-	if setSiteCookie {
-		httpCookie.Secure = true
-		httpCookie.SameSite = http.SameSiteNoneMode
-	}
-	w.Header().Add("Set-Cookie", httpCookie.String())
-}
-
 // Unsync removes the user's ID for the given syncer key from this cookie.
 func (cookie *Cookie) Unsync(key string) {
 	delete(cookie.uids, key)
@@ -199,24 +230,8 @@ func (cookie *Cookie) HasAnyLiveSyncs() bool {
 	return false
 }
 
-// TrySync tries to set the UID for some syncer key. It returns an error if the set didn't happen.
-func (cookie *Cookie) TrySync(key string, uid string) error {
-	if !cookie.AllowSyncs() {
-		return errors.New("The user has opted out of prebid server cookie syncs.")
-	}
-
-	// At the moment, Facebook calls /setuid with a UID of 0 if the user isn't logged into Facebook.
-	// They shouldn't be sending us a sentinel value... but since they are, we're refusing to save that ID.
-	if key == string(openrtb_ext.BidderAudienceNetwork) && uid == "0" {
-		return errors.New("audienceNetwork uses a UID of 0 as \"not yet recognized\".")
-	}
-
-	cookie.uids[key] = uidWithExpiry{
-		UID:     uid,
-		Expires: time.Now().Add(uidTTL),
-	}
-
-	return nil
+func checkAudienceNetwork(key string, uid string) bool {
+	return key == string(openrtb_ext.BidderAudienceNetwork) && uid == "0"
 }
 
 // cookieJson defines the JSON contract for the cookie data's storage format.
@@ -224,8 +239,8 @@ func (cookie *Cookie) TrySync(key string, uid string) error {
 // This exists so that Cookie (which is public) can have private fields, and the rest of
 // the code doesn't have to worry about the cookie data storage format.
 type cookieJson struct {
-	UIDs   map[string]uidWithExpiry `json:"tempUIDs,omitempty"`
-	OptOut bool                     `json:"optout,omitempty"`
+	UIDs   map[string]UIDEntry `json:"tempUIDs,omitempty"`
+	OptOut bool                `json:"optout,omitempty"`
 }
 
 func (cookie *Cookie) MarshalJSON() ([]byte, error) {
@@ -250,10 +265,10 @@ func (cookie *Cookie) UnmarshalJSON(b []byte) error {
 	}
 
 	if cookie.uids == nil {
-		cookie.uids = make(map[string]uidWithExpiry)
+		cookie.uids = make(map[string]UIDEntry)
 	}
 
-	// Audience Network / Facebook Handling
+	// Audience Network Handling
 	if id, ok := cookie.uids[string(openrtb_ext.BidderAudienceNetwork)]; ok && id.UID == "0" {
 		delete(cookie.uids, string(openrtb_ext.BidderAudienceNetwork))
 	}
diff --git a/usersync/decoder.go b/usersync/decoder.go
new file mode 100644
index 00000000..3ff13aa3
--- /dev/null
+++ b/usersync/decoder.go
@@ -0,0 +1,27 @@
+package usersync
+
+import (
+	"encoding/base64"
+	"encoding/json"
+)
+
+type Decoder interface {
+	// Decode takes an encoded string and decodes it into a cookie
+	Decode(v string) *Cookie
+}
+
+type Base64Decoder struct{}
+
+func (d Base64Decoder) Decode(encodedValue string) *Cookie {
+	jsonValue, err := base64.URLEncoding.DecodeString(encodedValue)
+	if err != nil {
+		return NewCookie()
+	}
+
+	var cookie Cookie
+	if err = json.Unmarshal(jsonValue, &cookie); err != nil {
+		return NewCookie()
+	}
+
+	return &cookie
+}
diff --git a/usersync/encoder.go b/usersync/encoder.go
new file mode 100644
index 00000000..eef7e2ef
--- /dev/null
+++ b/usersync/encoder.go
@@ -0,0 +1,23 @@
+package usersync
+
+import (
+	"encoding/base64"
+	"encoding/json"
+)
+
+type Encoder interface {
+	// Encode a cookie into a base 64 string
+	Encode(c *Cookie) (string, error)
+}
+
+type Base64Encoder struct{}
+
+func (e Base64Encoder) Encode(c *Cookie) (string, error) {
+	j, err := json.Marshal(c)
+	if err != nil {
+		return "", err
+	}
+	b64 := base64.URLEncoding.EncodeToString(j)
+
+	return b64, nil
+}


Test patch:
diff --git a/adapters/bidder_test.go b/adapters/bidder_test.go
index f0b833ce..d6373bde 100644
--- a/adapters/bidder_test.go
+++ b/adapters/bidder_test.go
@@ -52,12 +52,12 @@ type mockConversions struct {
 	mock.Mock
 }
 
-func (m mockConversions) GetRate(from string, to string) (float64, error) {
+func (m *mockConversions) GetRate(from string, to string) (float64, error) {
 	args := m.Called(from, to)
 	return args.Get(0).(float64), args.Error(1)
 }
 
-func (m mockConversions) GetRates() *map[string]map[string]float64 {
+func (m *mockConversions) GetRates() *map[string]map[string]float64 {
 	args := m.Called()
 	return args.Get(0).(*map[string]map[string]float64)
 }
diff --git a/adapters/mobilefuse/mobilefusetest/exemplary/skadn.json b/adapters/mobilefuse/mobilefusetest/exemplary/skadn.json
new file mode 100644
index 00000000..e8fd66b1
--- /dev/null
+++ b/adapters/mobilefuse/mobilefusetest/exemplary/skadn.json
@@ -0,0 +1,78 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "1",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        }
+                    ]
+                },
+                "ext": {
+                    "skadn": {
+                        "versions": [
+                            "2.0",
+                            "2.1"
+                        ],
+                        "sourceapp": "11111",
+                        "skadnetids": [
+                            "424m5254lk.skadnetwork",
+                            "4fzdc2evr5.skadnetwork"
+                        ]
+                    },
+                    "bidder": {
+                        "placement_id": 999999,
+                        "pub_id": 1111,
+                        "tagid_src": "ext"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://mfx.mobilefuse.com/openrtb?pub_id=1111&tagid_src=ext",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "1",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    }
+                                ]
+                            },
+                            "tagid": "999999",
+                            "ext": {
+                                "skadn": {
+                                    "versions": [
+                                        "2.0",
+                                        "2.1"
+                                    ],
+                                    "sourceapp": "11111",
+                                    "skadnetids": [
+                                        "424m5254lk.skadnetwork",
+                                        "4fzdc2evr5.skadnetwork"
+                                    ]
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "status": 204,
+                "body": {}
+            }
+        }
+    ],
+    "expectedBidResponses": []
+}
\ No newline at end of file
diff --git a/bidadjustment/apply_test.go b/bidadjustment/apply_test.go
index 27daa9c7..c0eb74ab 100644
--- a/bidadjustment/apply_test.go
+++ b/bidadjustment/apply_test.go
@@ -223,12 +223,12 @@ type mockConversions struct {
 	mock.Mock
 }
 
-func (m mockConversions) GetRate(from string, to string) (float64, error) {
+func (m *mockConversions) GetRate(from string, to string) (float64, error) {
 	args := m.Called(from, to)
 	return args.Get(0).(float64), args.Error(1)
 }
 
-func (m mockConversions) GetRates() *map[string]map[string]float64 {
+func (m *mockConversions) GetRates() *map[string]map[string]float64 {
 	args := m.Called()
 	return args.Get(0).(*map[string]map[string]float64)
 }
diff --git a/config/bidderinfo_test.go b/config/bidderinfo_test.go
index 6005178c..f83d942c 100644
--- a/config/bidderinfo_test.go
+++ b/config/bidderinfo_test.go
@@ -244,6 +244,22 @@ func TestBidderInfoValidationPositive(t *testing.T) {
 				},
 			},
 		},
+		"bidderC": BidderInfo{
+			Endpoint: "http://bidderB.com/openrtb2",
+			Maintainer: &MaintainerInfo{
+				Email: "maintainer@bidderA.com",
+			},
+			Capabilities: &CapabilitiesInfo{
+				Site: &PlatformInfo{
+					MediaTypes: []openrtb_ext.BidType{
+						openrtb_ext.BidTypeVideo,
+						openrtb_ext.BidTypeNative,
+						openrtb_ext.BidTypeBanner,
+					},
+				},
+			},
+			AliasOf: "bidderB",
+		},
 	}
 	errs := bidderInfos.validate(make([]error, 0))
 	assert.Len(t, errs, 0, "All bidder infos should be correct")
@@ -467,6 +483,43 @@ func TestBidderInfoValidationNegative(t *testing.T) {
 				errors.New("syncer could not be created, invalid supported endpoint: incorrect"),
 			},
 		},
+		{
+			"Invalid aliases",
+			BidderInfos{
+				"bidderA": BidderInfo{
+					Endpoint: "http://bidderA.com/openrtb2",
+					Maintainer: &MaintainerInfo{
+						Email: "maintainer@bidderA.com",
+					},
+					Capabilities: &CapabilitiesInfo{
+						Site: &PlatformInfo{
+							MediaTypes: []openrtb_ext.BidType{
+								openrtb_ext.BidTypeVideo,
+							},
+						},
+					},
+					AliasOf: "bidderB",
+				},
+				"bidderB": BidderInfo{
+					Endpoint: "http://bidderA.com/openrtb2",
+					Maintainer: &MaintainerInfo{
+						Email: "maintainer@bidderA.com",
+					},
+					Capabilities: &CapabilitiesInfo{
+						Site: &PlatformInfo{
+							MediaTypes: []openrtb_ext.BidType{
+								openrtb_ext.BidTypeVideo,
+							},
+						},
+					},
+					AliasOf: "bidderC",
+				},
+			},
+			[]error{
+				errors.New("bidder: bidderB cannot be an alias of an alias: bidderA"),
+				errors.New("bidder: bidderC not found for an alias: bidderB"),
+			},
+		},
 		{
 			"Two bidders, one with incorrect url",
 			BidderInfos{
diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go
index 37acf0c2..a69a2cdb 100644
--- a/endpoints/cookie_sync_test.go
+++ b/endpoints/cookie_sync_test.go
@@ -11,7 +11,6 @@ import (
 	"strings"
 	"testing"
 	"testing/iotest"
-	"time"
 
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
@@ -106,7 +105,7 @@ func TestCookieSyncHandle(t *testing.T) {
 	syncer.On("GetSync", syncTypeExpected, privacy.Policies{}).Return(sync, nil).Maybe()
 
 	cookieWithSyncs := usersync.NewCookie()
-	cookieWithSyncs.TrySync("foo", "anyID")
+	cookieWithSyncs.Sync("foo", "anyID")
 
 	testCases := []struct {
 		description              string
@@ -271,7 +270,9 @@ func TestCookieSyncHandle(t *testing.T) {
 
 		request := httptest.NewRequest("POST", "/cookiesync", test.givenBody)
 		if test.givenCookie != nil {
-			request.AddCookie(test.givenCookie.ToHTTPCookie(24 * time.Hour))
+			httpCookie, err := ToHTTPCookie(test.givenCookie)
+			assert.NoError(t, err)
+			request.AddCookie(httpCookie)
 		}
 
 		writer := httptest.NewRecorder()
@@ -1663,7 +1664,7 @@ func TestCookieSyncHandleResponse(t *testing.T) {
 
 		cookie := usersync.NewCookie()
 		if test.givenCookieHasSyncs {
-			if err := cookie.TrySync("foo", "anyID"); err != nil {
+			if err := cookie.Sync("foo", "anyID"); err != nil {
 				assert.FailNow(t, test.description+":set_cookie")
 			}
 		}
diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go
index 27f84aee..d709a702 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -88,7 +88,7 @@ func TestGoodAmpRequests(t *testing.T) {
 				continue
 			}
 
-			test.storedRequest = map[string]json.RawMessage{tagID: test.BidRequest}
+			test.StoredRequest = map[string]json.RawMessage{tagID: test.BidRequest}
 			test.endpointType = AMP_ENDPOINT
 
 			cfg := &config.Configuration{
@@ -165,7 +165,7 @@ func TestAccountErrors(t *testing.T) {
 		if !assert.NoError(t, json.Unmarshal(fileJsonData, &test), "Failed to unmarshal data from file: %s. Error: %v", tt.filename, err) {
 			continue
 		}
-		test.storedRequest = map[string]json.RawMessage{tt.storedReqID: test.BidRequest}
+		test.StoredRequest = map[string]json.RawMessage{tt.storedReqID: test.BidRequest}
 		test.endpointType = AMP_ENDPOINT
 
 		cfg := &config.Configuration{
@@ -2190,7 +2190,7 @@ func TestValidAmpResponseWhenRequestRejected(t *testing.T) {
 			query := request.URL.Query()
 			tagID := query.Get("tag_id")
 
-			test.storedRequest = map[string]json.RawMessage{tagID: test.BidRequest}
+			test.StoredRequest = map[string]json.RawMessage{tagID: test.BidRequest}
 			test.planBuilder = tc.planBuilder
 			test.endpointType = AMP_ENDPOINT
 
diff --git a/endpoints/openrtb2/test_utils.go b/endpoints/openrtb2/test_utils.go
index afe88397..a93325ea 100644
--- a/endpoints/openrtb2/test_utils.go
+++ b/endpoints/openrtb2/test_utils.go
@@ -76,7 +76,7 @@ type testCase struct {
 	ExpectedBidResponse json.RawMessage `json:"expectedBidResponse"`
 
 	// "/openrtb2/amp" endpoint JSON test info
-	storedRequest       map[string]json.RawMessage `json:"mockAmpStoredRequest"`
+	StoredRequest       map[string]json.RawMessage `json:"mockAmpStoredRequest"`
 	StoredResponse      map[string]json.RawMessage `json:"mockAmpStoredResponse"`
 	ExpectedAmpResponse json.RawMessage            `json:"expectedAmpResponse"`
 }
@@ -1265,8 +1265,8 @@ func buildTestEndpoint(test testCase, cfg *config.Configuration) (httprouter.Han
 	testExchange, mockBidServersArray := buildTestExchange(test.Config, adapterMap, mockBidServersArray, mockCurrencyRatesServer, bidderInfos, cfg, met, mockFetcher)
 
 	var storedRequestFetcher stored_requests.Fetcher
-	if len(test.storedRequest) > 0 {
-		storedRequestFetcher = &mockAmpStoredReqFetcher{test.storedRequest}
+	if len(test.StoredRequest) > 0 {
+		storedRequestFetcher = &mockAmpStoredReqFetcher{test.StoredRequest}
 	} else {
 		storedRequestFetcher = &mockStoredReqFetcher{}
 	}
diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go
index 609d8539..9176041e 100644
--- a/endpoints/setuid_test.go
+++ b/endpoints/setuid_test.go
@@ -13,6 +13,7 @@ import (
 
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/openrtb_ext"
@@ -157,7 +158,7 @@ func TestSetUIDEndpoint(t *testing.T) {
 			expectedSyncs:          nil,
 			gdprAllowsHostCookies:  true,
 			expectedStatusCode:     http.StatusBadRequest,
-			expectedBody:           "gdpr_consent is required when gdpr=1",
+			expectedBody:           "GDPR consent is required when gdpr signal equals 1",
 			description:            "Return an error if GDPR is set to 1 but GDPR consent string is missing",
 		},
 		{
@@ -193,6 +194,28 @@ func TestSetUIDEndpoint(t *testing.T) {
 			expectedHeaders:        map[string]string{"Content-Type": "text/html", "Content-Length": "0"},
 			description:            "Should set uid for a bidder that is allowed by the GDPR consent string",
 		},
+		{
+			uri:                    "/setuid?bidder=pubmatic&uid=123&gpp_sid=2,4&gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+			syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"},
+			gdprAllowsHostCookies:  true,
+			existingSyncs:          nil,
+			expectedSyncs:          map[string]string{"pubmatic": "123"},
+			expectedStatusCode:     http.StatusOK,
+			expectedHeaders:        map[string]string{"Content-Type": "text/html", "Content-Length": "0"},
+			description:            "Sets uid for a bidder allowed by GDPR consent string in the GPP query field",
+		},
+		{
+			uri: "/setuid?bidder=pubmatic&uid=123&gpp_sid=2,4&gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA" +
+				"gdpr=1&gdpr_consent=BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw",
+			syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"},
+			gdprAllowsHostCookies:  true,
+			existingSyncs:          nil,
+			expectedSyncs:          map[string]string{"pubmatic": "123"},
+			expectedStatusCode:     http.StatusOK,
+			expectedBody:           "Warning: 'gpp' value will be used over the one found in the deprecated 'gdpr_consent' field.",
+			expectedHeaders:        map[string]string{"Content-Type": "text/plain; charset=utf-8"},
+			description:            "Sets uid for a bidder allowed by GDPR in GPP, throws warning because GDPR legacy values weren't used",
+		},
 		{
 			uri: "/setuid?bidder=pubmatic&uid=123&gdpr=1&gdpr_consent=" +
 				"malformed",
@@ -288,6 +311,597 @@ func TestSetUIDEndpoint(t *testing.T) {
 	}
 }
 
+func TestParseSignalFromGPPSID(t *testing.T) {
+	type testOutput struct {
+		signal gdpr.Signal
+		err    error
+	}
+	testCases := []struct {
+		desc     string
+		strSID   string
+		expected testOutput
+	}{
+		{
+			desc:   "Empty gpp_sid, expect gdpr.SignalAmbiguous",
+			strSID: "",
+			expected: testOutput{
+				signal: gdpr.SignalAmbiguous,
+				err:    nil,
+			},
+		},
+		{
+			desc:   "Malformed gpp_sid, expect gdpr.SignalAmbiguous",
+			strSID: "malformed",
+			expected: testOutput{
+				signal: gdpr.SignalAmbiguous,
+				err:    errors.New(`Error parsing gpp_sid strconv.ParseInt: parsing "malformed": invalid syntax`),
+			},
+		},
+		{
+			desc:   "Valid gpp_sid doesn't come with TCF2, expect gdpr.SignalNo",
+			strSID: "6",
+			expected: testOutput{
+				signal: gdpr.SignalNo,
+				err:    nil,
+			},
+		},
+		{
+			desc:   "Valid gpp_sid comes with TCF2, expect gdpr.SignalYes",
+			strSID: "2",
+			expected: testOutput{
+				signal: gdpr.SignalYes,
+				err:    nil,
+			},
+		},
+	}
+	for _, tc := range testCases {
+		outSignal, outErr := parseSignalFromGppSidStr(tc.strSID)
+
+		assert.Equal(t, tc.expected.signal, outSignal, tc.desc)
+		assert.Equal(t, tc.expected.err, outErr, tc.desc)
+	}
+}
+
+func TestParseConsentFromGppStr(t *testing.T) {
+	type testOutput struct {
+		gdprConsent string
+		err         error
+	}
+	testCases := []struct {
+		desc       string
+		inGppQuery string
+		expected   testOutput
+	}{
+		{
+			desc:       "Empty gpp field, expect empty GDPR consent",
+			inGppQuery: "",
+			expected: testOutput{
+				gdprConsent: "",
+				err:         nil,
+			},
+		},
+		{
+			desc:       "Malformed gpp field value, expect empty GDPR consent and error",
+			inGppQuery: "malformed",
+			expected: testOutput{
+				gdprConsent: "",
+				err:         errors.New(`error parsing GPP header, base64 decoding: illegal base64 data at input byte 8`),
+			},
+		},
+		{
+			desc:       "Valid gpp string comes with TCF2 in its gppConstants.SectionID's, expect non-empty GDPR consent",
+			inGppQuery: "DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+			expected: testOutput{
+				gdprConsent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+				err:         nil,
+			},
+		},
+		{
+			desc:       "Valid gpp string doesn't come with TCF2 in its gppConstants.SectionID's, expect blank GDPR consent",
+			inGppQuery: "DBABjw~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN",
+			expected: testOutput{
+				gdprConsent: "",
+				err:         nil,
+			},
+		},
+	}
+	for _, tc := range testCases {
+		outConsent, outErr := parseConsentFromGppStr(tc.inGppQuery)
+
+		assert.Equal(t, tc.expected.gdprConsent, outConsent, tc.desc)
+		assert.Equal(t, tc.expected.err, outErr, tc.desc)
+	}
+}
+
+func TestParseGDPRFromGPP(t *testing.T) {
+	type testOutput struct {
+		reqInfo gdpr.RequestInfo
+		err     error
+	}
+	type aTest struct {
+		desc     string
+		inUri    string
+		expected testOutput
+	}
+	testGroups := []struct {
+		groupDesc string
+		testCases []aTest
+	}{
+		{
+			groupDesc: "No gpp_sid nor gpp",
+			testCases: []aTest{
+				{
+					desc:  "Input URL is mising gpp_sid and gpp, expect signal ambiguous and no error",
+					inUri: "/setuid?bidder=pubmatic&uid=123",
+					expected: testOutput{
+						reqInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous},
+						err:     nil,
+					},
+				},
+			},
+		},
+		{
+			groupDesc: "gpp only",
+			testCases: []aTest{
+				{
+					desc:  "gpp is malformed, expect error",
+					inUri: "/setuid?gpp=malformed",
+					expected: testOutput{
+						reqInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous},
+						err:     errors.New("error parsing GPP header, base64 decoding: illegal base64 data at input byte 8"),
+					},
+				},
+				{
+					desc:  "gpp with a valid TCF2 value. Expect valid consent string and no error",
+					inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+					expected: testOutput{
+						reqInfo: gdpr.RequestInfo{
+							GDPRSignal: gdpr.SignalAmbiguous,
+							Consent:    "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+						},
+						err: nil,
+					},
+				},
+				{
+					desc:  "gpp does not include TCF2 string. Expect empty consent string and no error",
+					inUri: "/setuid?gpp=DBABjw~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN",
+					expected: testOutput{
+						reqInfo: gdpr.RequestInfo{
+							GDPRSignal: gdpr.SignalAmbiguous,
+							Consent:    "",
+						},
+						err: nil,
+					},
+				},
+			},
+		},
+		{
+			groupDesc: "gpp_sid only",
+			testCases: []aTest{
+				{
+					desc:  "gpp_sid is malformed, expect error",
+					inUri: "/setuid?gpp_sid=malformed",
+					expected: testOutput{
+						reqInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous},
+						err:     errors.New("Error parsing gpp_sid strconv.ParseInt: parsing \"malformed\": invalid syntax"),
+					},
+				},
+				{
+					desc:  "TCF2 found in gpp_sid list. Given that the consent string will be empty, expect an error",
+					inUri: "/setuid?gpp_sid=2,6",
+					expected: testOutput{
+						reqInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalYes},
+						err:     nil,
+					},
+				},
+				{
+					desc:  "TCF2 not found in gpp_sid list. Expect SignalNo and no error",
+					inUri: "/setuid?gpp_sid=6,8",
+					expected: testOutput{
+						reqInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalNo},
+						err:     nil,
+					},
+				},
+			},
+		},
+		{
+			groupDesc: "both gpp_sid and gpp",
+			testCases: []aTest{
+				{
+					desc:  "TCF2 found in gpp_sid list and gpp has a valid GDPR string. Expect no error",
+					inUri: "/setuid?gpp_sid=2,6&gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+					expected: testOutput{
+						reqInfo: gdpr.RequestInfo{
+							GDPRSignal: gdpr.SignalYes,
+							Consent:    "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+						},
+						err: nil,
+					},
+				},
+			},
+		},
+	}
+	for _, tgroup := range testGroups {
+		for _, tc := range tgroup.testCases {
+			// set test
+			testURL, err := url.Parse(tc.inUri)
+			assert.NoError(t, err, "%s - %s", tgroup.groupDesc, tc.desc)
+
+			query := testURL.Query()
+
+			// run
+			outReqInfo, outErr := parseGDPRFromGPP(query)
+
+			// assertions
+			assert.Equal(t, tc.expected.reqInfo, outReqInfo, "%s - %s", tgroup.groupDesc, tc.desc)
+			assert.Equal(t, tc.expected.err, outErr, "%s - %s", tgroup.groupDesc, tc.desc)
+		}
+	}
+}
+
+func TestParseLegacyGDPRFields(t *testing.T) {
+	type testInput struct {
+		uri            string
+		gppGDPRSignal  gdpr.Signal
+		gppGDPRConsent string
+	}
+	type testOutput struct {
+		signal  gdpr.Signal
+		consent string
+		err     error
+	}
+	testCases := []struct {
+		desc     string
+		in       testInput
+		expected testOutput
+	}{
+		{
+			desc: `both "gdpr" and "gdpr_consent" missing from URI, expect SignalAmbiguous, blank consent and no error`,
+			in: testInput{
+				uri: "/setuid?bidder=pubmatic&uid=123",
+			},
+			expected: testOutput{
+				signal:  gdpr.SignalAmbiguous,
+				consent: "",
+				err:     nil,
+			},
+		},
+		{
+			desc: `invalid "gdpr" value, expect SignalAmbiguous, blank consent and error`,
+			in: testInput{
+				uri:           "/setuid?gdpr=2",
+				gppGDPRSignal: gdpr.SignalAmbiguous,
+			},
+			expected: testOutput{
+				signal:  gdpr.SignalAmbiguous,
+				consent: "",
+				err:     errors.New("the gdpr query param must be either 0 or 1. You gave 2"),
+			},
+		},
+		{
+			desc: `valid "gdpr" value but valid GDPRSignal was previously parsed before, expect SignalAmbiguous, blank consent and a warning`,
+			in: testInput{
+				uri:           "/setuid?gdpr=1",
+				gppGDPRSignal: gdpr.SignalYes,
+			},
+			expected: testOutput{
+				signal:  gdpr.SignalAmbiguous,
+				consent: "",
+				err: &errortypes.Warning{
+					Message:     "'gpp_sid' signal value will be used over the one found in the deprecated 'gdpr' field.",
+					WarningCode: errortypes.UnknownWarningCode,
+				},
+			},
+		},
+		{
+			desc: `valid "gdpr_consent" value but valid GDPRSignal was previously parsed before, expect SignalAmbiguous, blank consent and a warning`,
+			in: testInput{
+				uri:            "/setuid?gdpr_consent=someConsent",
+				gppGDPRConsent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+			},
+			expected: testOutput{
+				signal:  gdpr.SignalAmbiguous,
+				consent: "",
+				err: &errortypes.Warning{
+					Message:     "'gpp' value will be used over the one found in the deprecated 'gdpr_consent' field.",
+					WarningCode: errortypes.UnknownWarningCode,
+				},
+			},
+		},
+	}
+	for _, tc := range testCases {
+		// set test
+		testURL, err := url.Parse(tc.in.uri)
+		assert.NoError(t, err, tc.desc)
+
+		query := testURL.Query()
+
+		// run
+		outSignal, outConsent, outErr := parseLegacyGDPRFields(query, tc.in.gppGDPRSignal, tc.in.gppGDPRConsent)
+
+		// assertions
+		assert.Equal(t, tc.expected.signal, outSignal, tc.desc)
+		assert.Equal(t, tc.expected.consent, outConsent, tc.desc)
+		assert.Equal(t, tc.expected.err, outErr, tc.desc)
+	}
+}
+
+func TestExtractGDPRInfo(t *testing.T) {
+	type testOutput struct {
+		requestInfo gdpr.RequestInfo
+		err         error
+	}
+	type testCase struct {
+		desc     string
+		inUri    string
+		expected testOutput
+	}
+	testSuite := []struct {
+		sDesc string
+		tests []testCase
+	}{
+		{
+			sDesc: "no gdpr nor gpp values in query",
+			tests: []testCase{
+				{
+					desc:  "expect blank consent, signalNo and nil error",
+					inUri: "/setuid?bidder=pubmatic&uid=123",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "",
+							GDPRSignal: gdpr.SignalAmbiguous,
+						},
+						err: nil,
+					},
+				},
+			},
+		},
+		{
+			sDesc: "missing gpp, gdpr only",
+			tests: []testCase{
+				{
+					desc:  "Invalid gdpr signal value in query, expect blank request info and error",
+					inUri: "/setuid?gdpr=2",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous},
+						err:         errors.New("the gdpr query param must be either 0 or 1. You gave 2"),
+					},
+				},
+				{
+					desc:  "GDPR equals 0, blank consent, expect blank consent, signalNo and nil error",
+					inUri: "/setuid?gdpr=0",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalNo},
+						err:         nil,
+					},
+				},
+				{
+					desc:  "GDPR equals 1, blank consent, expect blank request info and error",
+					inUri: "/setuid?gdpr=1",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous},
+						err:         errors.New("GDPR consent is required when gdpr signal equals 1"),
+					},
+				},
+				{
+					desc:  "GDPR equals 0, non-blank consent, expect non-blank request info and nil error",
+					inUri: "/setuid?gdpr=0&gdpr_consent=someConsent",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "someConsent",
+							GDPRSignal: gdpr.SignalNo,
+						},
+						err: nil,
+					},
+				},
+				{
+					desc:  "GDPR equals 1, non-blank consent, expect non-blank request info and nil error",
+					inUri: "/setuid?gdpr=1&gdpr_consent=someConsent",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "someConsent",
+							GDPRSignal: gdpr.SignalYes,
+						},
+						err: nil,
+					},
+				},
+			},
+		},
+		{
+			sDesc: "missing gdpr, gpp only",
+			tests: []testCase{
+				{
+					desc:  "Malformed GPP_SID string, expect blank request info and error",
+					inUri: "/setuid?gpp_sid=malformed",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous},
+						err:         errors.New("Error parsing gpp_sid strconv.ParseInt: parsing \"malformed\": invalid syntax"),
+					},
+				},
+				{
+					desc:  "Valid GPP_SID string but invalid GPP string in query, expect blank request info and error",
+					inUri: "/setuid?gpp=malformed&gpp_sid=2",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous},
+						err:         errors.New("error parsing GPP header, base64 decoding: illegal base64 data at input byte 8"),
+					},
+				},
+				{
+					desc:  "SectionTCFEU2 not found in GPP string, expect blank consent and signalAmbiguous",
+					inUri: "/setuid?gpp=DBABBgA~xlgWEYCZAA",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "",
+							GDPRSignal: gdpr.SignalAmbiguous,
+						},
+						err: nil,
+					},
+				},
+				{
+					desc:  "No GPP string, nor SectionTCFEU2 found in SID list in query, expect blank consent and signalAmbiguous",
+					inUri: "/setuid?gpp_sid=3,6",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "",
+							GDPRSignal: gdpr.SignalNo,
+						},
+						err: nil,
+					},
+				},
+				{
+					desc:  "No GPP string, SectionTCFEU2 found in SID list in query, expect blank request info and error",
+					inUri: "/setuid?gpp_sid=2",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous},
+						err:         errors.New("GDPR consent is required when gdpr signal equals 1"),
+					},
+				},
+				{
+					desc:  "SectionTCFEU2 only found in SID list, expect blank request info and error",
+					inUri: "/setuid?gpp=DBABBgA~xlgWEYCZAA&gpp_sid=2",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous},
+						err:         errors.New("GDPR consent is required when gdpr signal equals 1"),
+					},
+				},
+				{
+					desc:  "SectionTCFEU2 found in GPP string but SID list is nil, expect valid consent and SignalAmbiguous",
+					inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+							GDPRSignal: gdpr.SignalAmbiguous,
+						},
+						err: nil,
+					},
+				},
+				{
+					desc:  "SectionTCFEU2 found in GPP string but not in the non-nil SID list, expect valid consent and signalNo",
+					inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA&gpp_sid=6",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+							GDPRSignal: gdpr.SignalNo,
+						},
+						err: nil,
+					},
+				},
+				{
+					desc:  "SectionTCFEU2 found both in GPP string and SID list, expect valid consent and signalYes",
+					inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA&gpp_sid=2,4",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+							GDPRSignal: gdpr.SignalYes,
+						},
+						err: nil,
+					},
+				},
+			},
+		},
+		{
+			sDesc: "GPP values take priority over GDPR",
+			tests: []testCase{
+				{
+					desc:  "SignalNo in gdpr field but SignalYes in SID list, CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA consent in gpp but legacyConsent in gdpr_consent, expect GPP values to prevail",
+					inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA&gpp_sid=2,4&gdpr=0&gdpr_consent=legacyConsent",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+							GDPRSignal: gdpr.SignalYes,
+						},
+						err: &errortypes.Warning{
+							Message:     "'gpp' value will be used over the one found in the deprecated 'gdpr_consent' field.",
+							WarningCode: errortypes.UnknownWarningCode,
+						},
+					},
+				},
+				{
+					desc:  "SignalNo in gdpr field but SignalYes in SID list because SectionTCFEU2 is listed, expect GPP to prevail",
+					inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA&gpp_sid=2,4&gdpr=0",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+							GDPRSignal: gdpr.SignalYes,
+						},
+						err: &errortypes.Warning{
+							Message:     "'gpp_sid' signal value will be used over the one found in the deprecated 'gdpr' field.",
+							WarningCode: errortypes.UnknownWarningCode,
+						},
+					},
+				},
+				{
+					desc:  "No gpp string in URL query, use gdpr_consent and SignalYes found in SID list because SectionTCFEU2 is listed",
+					inUri: "/setuid?gpp_sid=2,4&gdpr_consent=legacyConsent",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "",
+							GDPRSignal: gdpr.SignalAmbiguous,
+						},
+						err: errors.New("GDPR consent is required when gdpr signal equals 1"),
+					},
+				},
+				{
+					desc:  "SectionTCFEU2 not found in GPP string but found in SID list, choose the GDPR_CONSENT and GPP_SID signal",
+					inUri: "/setuid?gpp=DBABBgA~xlgWEYCZAA&gpp_sid=2&gdpr=0&gdpr_consent=legacyConsent",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "",
+							GDPRSignal: gdpr.SignalAmbiguous,
+						},
+						err: errors.New("GDPR consent is required when gdpr signal equals 1"),
+					},
+				},
+				{
+					desc:  "SectionTCFEU2 found in GPP string but not in SID list, choose GDPR signal GPP consent value",
+					inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA&gpp_sid=6&gdpr=1&gdpr_consent=legacyConsent",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+							GDPRSignal: gdpr.SignalNo,
+						},
+						err: &errortypes.Warning{
+							Message:     "'gpp' value will be used over the one found in the deprecated 'gdpr_consent' field.",
+							WarningCode: errortypes.UnknownWarningCode,
+						},
+					},
+				},
+				{
+					desc:  "SectionTCFEU2 not found in GPP, use GDPR_CONSENT value. SignalYes found in gdpr field, but not in the valid SID list, expect SignalNo",
+					inUri: "/setuid?gpp=DBABBgA~xlgWEYCZAA&gpp_sid=6&gdpr=1&gdpr_consent=legacyConsent",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "",
+							GDPRSignal: gdpr.SignalNo,
+						},
+						err: &errortypes.Warning{
+							Message:     "'gpp_sid' signal value will be used over the one found in the deprecated 'gdpr' field.",
+							WarningCode: errortypes.UnknownWarningCode,
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, ts := range testSuite {
+		for _, tc := range ts.tests {
+			// set test
+			testURL, err := url.Parse(tc.inUri)
+			assert.NoError(t, err, tc.desc)
+
+			query := testURL.Query()
+
+			// run
+			outReqInfo, outErr := extractGDPRInfo(query)
+
+			// assertions
+			assert.Equal(t, tc.expected.requestInfo, outReqInfo, tc.desc)
+			assert.Equal(t, tc.expected.err, outErr, tc.desc)
+		}
+	}
+}
+
 func TestSetUIDEndpointMetrics(t *testing.T) {
 	cookieWithOptOut := usersync.NewCookie()
 	cookieWithOptOut.SetOptOut(true)
@@ -425,7 +1039,7 @@ func TestSetUIDEndpointMetrics(t *testing.T) {
 					Status:  400,
 					Bidder:  "pubmatic",
 					UID:     "",
-					Errors:  []error{errors.New("gdpr_consent is required when gdpr=1")},
+					Errors:  []error{errors.New("GDPR consent is required when gdpr signal equals 1")},
 					Success: false,
 				}
 				a.On("LogSetUIDObject", &expected).Once()
@@ -697,7 +1311,7 @@ func makeRequest(uri string, existingSyncs map[string]string) *http.Request {
 	if len(existingSyncs) > 0 {
 		pbsCookie := usersync.NewCookie()
 		for key, value := range existingSyncs {
-			pbsCookie.TrySync(key, value)
+			pbsCookie.Sync(key, value)
 		}
 		addCookie(request, pbsCookie)
 	}
@@ -749,10 +1363,12 @@ func doRequest(req *http.Request, analytics analytics.PBSAnalyticsModule, metric
 }
 
 func addCookie(req *http.Request, cookie *usersync.Cookie) {
-	req.AddCookie(cookie.ToHTTPCookie(time.Duration(1) * time.Hour))
+	httpCookie, _ := ToHTTPCookie(cookie)
+	req.AddCookie(httpCookie)
 }
 
 func parseCookieString(t *testing.T, response *httptest.ResponseRecorder) *usersync.Cookie {
+	decoder := usersync.Base64Decoder{}
 	cookieString := response.Header().Get("Set-Cookie")
 	parser := regexp.MustCompile("uids=(.*?);")
 	res := parser.FindStringSubmatch(cookieString)
@@ -761,7 +1377,7 @@ func parseCookieString(t *testing.T, response *httptest.ResponseRecorder) *users
 		Name:  "uids",
 		Value: res[1],
 	}
-	return usersync.ParseCookie(&httpCookie)
+	return decoder.Decode(httpCookie.Value)
 }
 
 type fakePermissionsBuilder struct {
@@ -830,3 +1446,18 @@ func (s fakeSyncer) SupportsType(syncTypes []usersync.SyncType) bool {
 func (s fakeSyncer) GetSync(syncTypes []usersync.SyncType, privacyPolicies privacy.Policies) (usersync.Sync, error) {
 	return usersync.Sync{}, nil
 }
+
+func ToHTTPCookie(cookie *usersync.Cookie) (*http.Cookie, error) {
+	encoder := usersync.Base64Encoder{}
+	encodedCookie, err := encoder.Encode(cookie)
+	if err != nil {
+		return nil, nil
+	}
+
+	return &http.Cookie{
+		Name:    uidCookieName,
+		Value:   encodedCookie,
+		Expires: time.Now().Add((90 * 24 * time.Hour)),
+		Path:    "/",
+	}, nil
+}
diff --git a/exchange/adapter_util_test.go b/exchange/adapter_util_test.go
index 2dd0fd04..3b818bdd 100644
--- a/exchange/adapter_util_test.go
+++ b/exchange/adapter_util_test.go
@@ -67,6 +67,13 @@ func TestBuildAdapters(t *testing.T) {
 				errors.New("unknown: unknown bidder"),
 			},
 		},
+		{
+			description: "Alias feature disabled",
+			bidderInfos: map[string]config.BidderInfo{"appNexus": {AliasOf: "rubicon"}},
+			expectedErrors: []error{
+				errors.New("This feature is currently under development"),
+			},
+		},
 	}
 
 	cfg := &config.Configuration{}
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 2a35ef57..8e7ba80b 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -379,7 +379,7 @@ func TestDebugBehaviour(t *testing.T) {
 		// Assert no HoldAuction error
 		assert.NoErrorf(t, err, "%s. ex.HoldAuction returned an error: %v \n", test.desc, err)
 		assert.NotNilf(t, outBidResponse.Ext, "%s. outBidResponse.Ext should not be nil \n", test.desc)
-
+		assert.False(t, auctionRequest.BidderResponseStartTime.IsZero())
 		actualExt := &openrtb_ext.ExtBidResponse{}
 		err = json.Unmarshal(outBidResponse.Ext, actualExt)
 		assert.NoErrorf(t, err, "%s. \"ext\" JSON field could not be unmarshaled. err: \"%v\" \n outBidResponse.Ext: \"%s\" \n", test.desc, err, outBidResponse.Ext)
@@ -540,6 +540,7 @@ func TestTwoBiddersDebugDisabledAndEnabled(t *testing.T) {
 		// Assert no HoldAuction err
 		assert.NoErrorf(t, err, "ex.HoldAuction returned an err")
 		assert.NotNilf(t, outBidResponse.Ext, "outBidResponse.Ext should not be nil")
+		assert.False(t, auctionRequest.BidderResponseStartTime.IsZero())
 
 		actualExt := &openrtb_ext.ExtBidResponse{}
 		err = json.Unmarshal(outBidResponse.Ext, actualExt)
@@ -715,6 +716,7 @@ func TestOverrideWithCustomCurrency(t *testing.T) {
 
 		// Assertions
 		assert.NoErrorf(t, err, "%s. HoldAuction error: %v \n", test.desc, err)
+		assert.False(t, auctionRequest.BidderResponseStartTime.IsZero())
 
 		if test.expected.numBids > 0 {
 			// Assert out currency
@@ -1362,6 +1364,7 @@ func TestReturnCreativeEndToEnd(t *testing.T) {
 				continue
 			} else {
 				assert.NoErrorf(t, err, "%s: %s. HoldAuction error: %v \n", testGroup.groupDesc, test.desc, err)
+				assert.False(t, auctionRequest.BidderResponseStartTime.IsZero())
 			}
 
 			// Assert returned bid
@@ -5143,6 +5146,7 @@ func TestOverrideConfigAlternateBidderCodesWithRequestValues(t *testing.T) {
 		// Assertions
 		assert.NoErrorf(t, err, "%s. HoldAuction error: %v \n", test.desc, err)
 		assert.NotNil(t, outBidResponse)
+		assert.False(t, auctionRequest.BidderResponseStartTime.IsZero())
 
 		// So 2 seatBids are expected as,
 		// the default "" and "pubmatic" bids will be in one seat and the extra-bids "groupm"/"appnexus"/"ix" in another seat.
@@ -5569,6 +5573,7 @@ func TestModulesCanBeExecutedForMultipleBiddersSimultaneously(t *testing.T) {
 	_, err := e.HoldAuction(context.Background(), auctionRequest, &DebugLog{})
 	// Assert no HoldAuction err
 	assert.NoErrorf(t, err, "ex.HoldAuction returned an err")
+	assert.False(t, auctionRequest.BidderResponseStartTime.IsZero())
 
 	// check stage outcomes
 	assert.Equal(t, len(exec.GetOutcomes()), len(e.adapterMap), "stage outcomes append operation failed")
diff --git a/gdpr/consent_parser_test.go b/gdpr/consent_parser_test.go
new file mode 100644
index 00000000..4708f689
--- /dev/null
+++ b/gdpr/consent_parser_test.go
@@ -0,0 +1,160 @@
+package gdpr
+
+import (
+	"errors"
+	"testing"
+	"time"
+
+	"github.com/prebid/go-gdpr/consentconstants"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParseConsent(t *testing.T) {
+	validTCF1Consent := "BONV8oqONXwgmADACHENAO7pqzAAppY"
+	validTCF2Consent := "CPuKGCPPuKGCPNEAAAENCZCAAAAAAAAAAAAAAAAAAAAA"
+
+	tests := []struct {
+		name                    string
+		consent                 string
+		expectedEncodingVersion uint8
+		expectedListVersion     uint16
+		expectedSpecVersion     uint16
+		expectedError           error
+	}{
+
+		{
+			name:                    "valid_consent_with_encoding_version_2",
+			consent:                 validTCF2Consent,
+			expectedEncodingVersion: 2,
+			expectedListVersion:     153,
+			expectedSpecVersion:     2,
+		},
+		{
+			name:    "invalid_consent_parsing_error",
+			consent: "",
+			expectedError: &ErrorMalformedConsent{
+				Consent: "",
+				Cause:   consentconstants.ErrEmptyDecodedConsent,
+			},
+		},
+		{
+			name:    "invalid_consent_version_validation_error",
+			consent: validTCF1Consent,
+			expectedError: &ErrorMalformedConsent{
+				Consent: validTCF1Consent,
+				Cause:   errors.New("invalid encoding format version: 1"),
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			parsedConsent, err := parseConsent(tt.consent)
+
+			if tt.expectedError != nil {
+				assert.Equal(t, tt.expectedError, err)
+				assert.Nil(t, parsedConsent)
+			} else {
+				assert.NoError(t, err)
+				assert.NotNil(t, parsedConsent)
+				assert.Equal(t, uint8(2), parsedConsent.encodingVersion)
+				assert.Equal(t, uint16(153), parsedConsent.listVersion)
+				assert.Equal(t, uint16(2), parsedConsent.specVersion)
+				assert.Equal(t, tt.expectedEncodingVersion, parsedConsent.consentMeta.Version())
+				assert.Equal(t, tt.expectedListVersion, parsedConsent.consentMeta.VendorListVersion())
+			}
+		})
+	}
+}
+
+func TestValidateVersions(t *testing.T) {
+	tests := []struct {
+		name          string
+		version       uint8
+		expectedError error
+	}{
+		{
+			name:    "valid_consent_version=2",
+			version: 2,
+		},
+		{
+			name:          "invalid_consent_version<2",
+			version:       1,
+			expectedError: errors.New("invalid encoding format version: 1"),
+		},
+		{
+			name:          "invalid_consent_version>2",
+			version:       3,
+			expectedError: errors.New("invalid encoding format version: 3"),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mcs := mockConsentString{
+				version: tt.version,
+			}
+			err := validateVersions(&mcs)
+			if tt.expectedError != nil {
+				assert.Equal(t, tt.expectedError, err)
+			} else {
+				assert.Nil(t, err)
+			}
+		})
+	}
+}
+
+func TestGetSpecVersion(t *testing.T) {
+	tests := []struct {
+		name                string
+		policyVersion       uint8
+		expectedSpecVersion uint16
+	}{
+		{
+			name:                "policy_version_0_gives_spec_version_2",
+			policyVersion:       0,
+			expectedSpecVersion: 2,
+		},
+		{
+			name:                "policy_version_3_gives_spec_version_2",
+			policyVersion:       3,
+			expectedSpecVersion: 2,
+		},
+		{
+			name:                "policy_version_4_gives_spec_version_3",
+			policyVersion:       4,
+			expectedSpecVersion: 3,
+		},
+		{
+			name:                "policy_version_5_gives_spec_version_3",
+			policyVersion:       5,
+			expectedSpecVersion: 3,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			specVersion := getSpecVersion(tt.policyVersion)
+			assert.Equal(t, tt.expectedSpecVersion, specVersion)
+		})
+	}
+}
+
+type mockConsentString struct {
+	version       uint8
+	policyVersion uint8
+}
+
+func (mcs *mockConsentString) Version() uint8                                  { return mcs.version }
+func (mcs *mockConsentString) Created() time.Time                              { return time.Time{} }
+func (mcs *mockConsentString) LastUpdated() time.Time                          { return time.Time{} }
+func (mcs *mockConsentString) CmpID() uint16                                   { return 0 }
+func (mcs *mockConsentString) CmpVersion() uint16                              { return 0 }
+func (mcs *mockConsentString) ConsentScreen() uint8                            { return 0 }
+func (mcs *mockConsentString) ConsentLanguage() string                         { return "" }
+func (mcs *mockConsentString) VendorListVersion() uint16                       { return 0 }
+func (mcs *mockConsentString) TCFPolicyVersion() uint8                         { return mcs.policyVersion }
+func (mcs *mockConsentString) MaxVendorID() uint16                             { return 0 }
+func (mcs *mockConsentString) PurposeAllowed(id consentconstants.Purpose) bool { return false }
+func (mcs *mockConsentString) VendorConsent(id uint16) bool                    { return false }
diff --git a/gdpr/full_enforcement_test.go b/gdpr/full_enforcement_test.go
index e8ccd1e4..61f6e8b8 100644
--- a/gdpr/full_enforcement_test.go
+++ b/gdpr/full_enforcement_test.go
@@ -898,6 +898,66 @@ func TestLegalBasisWithPubRestrictionRequireLI(t *testing.T) {
 	}
 }
 
+func TestLegalBasisWithoutVendor(t *testing.T) {
+	P1P2P3PurposeConsent := "CPfCRQAPfCRQAAAAAAENCgCAAOAAAAAAAAAAAAAAAAAA"
+	tests := []struct {
+		name       string
+		config     purposeConfig
+		wantResult bool
+	}{
+		{
+			name: "enforce_purpose_&_vendors_off",
+			config: purposeConfig{
+				EnforcePurpose: false,
+				EnforceVendors: false,
+			},
+			wantResult: true,
+		},
+		{
+			name: "enforce_purpose_on,_bidder_is_a_vendor_exception",
+			config: purposeConfig{
+				EnforcePurpose:     true,
+				EnforceVendors:     false,
+				VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}},
+			},
+			wantResult: true,
+		},
+		{
+			name: "enforce_purpose_on",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: false,
+			},
+			wantResult: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// convert consent string to TCF2 object
+			parsedConsent, err := vendorconsent.ParseString(P1P2P3PurposeConsent)
+			if err != nil {
+				t.Fatalf("Failed to parse consent %s\n", P1P2P3PurposeConsent)
+			}
+			consentMeta, ok := parsedConsent.(tcf2.ConsentMetadata)
+			if !ok {
+				t.Fatalf("Failed to convert consent %s\n", P1P2P3PurposeConsent)
+			}
+
+			vendorInfo := VendorInfo{
+				vendorID: 32,
+				vendor:   nil,
+			}
+
+			enforcer := FullEnforcement{cfg: tt.config}
+			enforcer.cfg.PurposeID = consentconstants.Purpose(3)
+
+			result := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, Overrides{})
+			assert.Equal(t, tt.wantResult, result)
+		})
+	}
+}
+
 func getVendorList(t *testing.T) vendorlist.VendorList {
 	GVL := makeVendorList()
 
diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go
index 4a65dbaf..835a580f 100644
--- a/gdpr/impl_test.go
+++ b/gdpr/impl_test.go
@@ -342,6 +342,109 @@ func TestAllowActivities(t *testing.T) {
 	}
 }
 
+func TestAllowActivitiesBidderWithoutGVLID(t *testing.T) {
+	bidderWithoutGVLID := openrtb_ext.BidderPangle
+	purpose2Consent := "CPuDXznPuDXznMOAAAENCZCAAEAAAAAAAAAAAAAAAAAA"
+	noPurposeConsent := "CPuDXznPuDXznMOAAAENCZCAAAAAAAAAAAAAAAAAAAAA"
+
+	tests := []struct {
+		name                    string
+		enforceAlgoID           config.TCF2EnforcementAlgo
+		vendorExceptions        map[openrtb_ext.BidderName]struct{}
+		basicEnforcementVendors map[string]struct{}
+		consent                 string
+		allowBidRequest         bool
+		passID                  bool
+	}{
+		{
+			name:          "full_enforcement_no_exceptions_user_consents_to_purpose_2",
+			enforceAlgoID: config.TCF2FullEnforcement,
+			consent:       purpose2Consent,
+		},
+		{
+			name:             "full_enforcement_vendor_exception_user_consents_to_purpose_2",
+			enforceAlgoID:    config.TCF2FullEnforcement,
+			vendorExceptions: map[openrtb_ext.BidderName]struct{}{bidderWithoutGVLID: {}},
+			consent:          purpose2Consent,
+			allowBidRequest:  true,
+			passID:           true,
+		},
+		{
+			name:    "basic_enforcement_no_exceptions_user_consents_to_purpose_2",
+			consent: purpose2Consent,
+		},
+		{
+			name:             "basic_enforcement_vendor_exception_user_consents_to_purpose_2",
+			vendorExceptions: map[openrtb_ext.BidderName]struct{}{bidderWithoutGVLID: {}},
+			consent:          purpose2Consent,
+			allowBidRequest:  true,
+			passID:           true,
+		},
+		{
+			name:                    "full_enforcement_soft_vendor_exception_user_consents_to_purpose_2", // allow bid request and pass ID
+			enforceAlgoID:           config.TCF2FullEnforcement,
+			basicEnforcementVendors: map[string]struct{}{string(bidderWithoutGVLID): {}},
+			consent:                 purpose2Consent,
+			allowBidRequest:         true,
+			passID:                  true,
+		},
+		{
+			name:                    "basic_enforcement_soft_vendor_exception_user_consents_to_purpose_2", // allow bid request and pass ID
+			enforceAlgoID:           config.TCF2BasicEnforcement,
+			basicEnforcementVendors: map[string]struct{}{string(bidderWithoutGVLID): {}},
+			consent:                 purpose2Consent,
+			allowBidRequest:         true,
+			passID:                  true,
+		},
+		{
+			name:                    "full_enforcement_soft_vendor_exception_user_consents_to_purpose_4",
+			enforceAlgoID:           config.TCF2FullEnforcement,
+			basicEnforcementVendors: map[string]struct{}{string(bidderWithoutGVLID): {}},
+			consent:                 noPurposeConsent,
+			allowBidRequest:         false,
+			passID:                  false,
+		},
+		{
+			name:                    "basic_enforcement_soft_vendor_exception_user_consents_to_purpose_4",
+			enforceAlgoID:           config.TCF2BasicEnforcement,
+			basicEnforcementVendors: map[string]struct{}{string(bidderWithoutGVLID): {}},
+			consent:                 noPurposeConsent,
+			allowBidRequest:         false,
+			passID:                  false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tcf2AggConfig := allPurposesEnabledTCF2Config()
+			tcf2AggConfig.AccountConfig.BasicEnforcementVendorsMap = tt.basicEnforcementVendors
+			tcf2AggConfig.HostConfig.Purpose2.VendorExceptionMap = tt.vendorExceptions
+			tcf2AggConfig.HostConfig.Purpose2.EnforceAlgoID = tt.enforceAlgoID
+			tcf2AggConfig.HostConfig.PurposeConfigs[consentconstants.Purpose(2)] = &tcf2AggConfig.HostConfig.Purpose2
+
+			perms := permissionsImpl{
+				cfg:                   &tcf2AggConfig,
+				consent:               tt.consent,
+				gdprSignal:            SignalYes,
+				hostVendorID:          2,
+				nonStandardPublishers: map[string]struct{}{},
+				vendorIDs:             map[openrtb_ext.BidderName]uint16{},
+				fetchVendorList: listFetcher(map[uint16]map[uint16]vendorlist.VendorList{
+					2: {
+						153: parseVendorListDataV2(t, MarshalVendorList(vendorList{GVLSpecificationVersion: 2, VendorListVersion: 153, Vendors: map[string]*vendor{}})),
+					},
+				}),
+				purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig),
+			}
+
+			permissions, err := perms.AuctionActivitiesAllowed(context.Background(), bidderWithoutGVLID, bidderWithoutGVLID)
+			assert.NoError(t, err)
+			assert.Equal(t, tt.allowBidRequest, permissions.AllowBidRequest)
+			assert.Equal(t, tt.passID, permissions.PassID)
+		})
+	}
+}
+
 func buildVendorList34() vendorList {
 	return vendorList{
 		VendorListVersion: 2,
@@ -1006,28 +1109,6 @@ func TestAllowActivitiesBidRequests(t *testing.T) {
 	}
 }
 
-func TestTCF1Consent(t *testing.T) {
-	bidderAllowedByConsent := openrtb_ext.BidderAppnexus
-	tcf1Consent := "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA"
-
-	perms := permissionsImpl{
-		cfg: &tcf2Config{},
-		vendorIDs: map[openrtb_ext.BidderName]uint16{
-			openrtb_ext.BidderAppnexus: 2,
-		},
-		aliasGVLIDs: map[string]uint16{},
-		consent:     tcf1Consent,
-		gdprSignal:  SignalYes,
-	}
-
-	permissions, err := perms.AuctionActivitiesAllowed(context.Background(), bidderAllowedByConsent, bidderAllowedByConsent)
-
-	assert.Nil(t, err, "TCF1 consent - no error returned")
-	assert.Equal(t, true, permissions.AllowBidRequest, "TCF1 consent - bid request not allowed")
-	assert.Equal(t, true, permissions.PassGeo, "TCF1 consent - passing geo not allowed")
-	assert.Equal(t, false, permissions.PassID, "TCF1 consent - passing id not allowed")
-}
-
 func TestAllowActivitiesVendorException(t *testing.T) {
 	noPurposeOrVendorConsentAndPubRestrictsP2 := "CPF_61ePF_61eFxAAAENAiCAAAAAAAAAAAAAACEAAAACEAAgAgAA"
 	noPurposeOrVendorConsentAndPubRestrictsNone := "CPF_61ePF_61eFxAAAENAiCAAAAAAAAAAAAAACEAAAAA"
@@ -1244,53 +1325,9 @@ func TestDefaultPermissions(t *testing.T) {
 	}
 }
 
-func TestGetSpecVersion(t *testing.T) {
-	tests := []struct {
-		name                string
-		policyVersion       uint8
-		expectedSpecVersion uint16
-		expectedError       bool
-	}{
-		{
-			name:                "policy_version_0_gives_spec_version_2",
-			policyVersion:       0,
-			expectedSpecVersion: 2,
-		},
-		{
-			name:                "policy_version_3_gives_spec_version_2",
-			policyVersion:       3,
-			expectedSpecVersion: 2,
-		},
-		{
-			name:                "policy_version_4_spec_version_3",
-			policyVersion:       4,
-			expectedSpecVersion: 3,
-		},
-		{
-			name:                "policy_version_5_error",
-			policyVersion:       5,
-			expectedSpecVersion: 0,
-			expectedError:       true,
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			specVersion, err := getSpecVersion(tt.policyVersion)
-			assert.Equal(t, tt.expectedSpecVersion, specVersion)
-			if tt.expectedError {
-				assert.Error(t, err)
-			} else {
-				assert.Nil(t, err)
-			}
-		})
-	}
-}
-
 func TestVendorListSelection(t *testing.T) {
 	policyVersion3WithVendor2AndPurpose1Consent := "CPGWbY_PGWbY_GYAAAENABDAAIAAAAAAAAAAACEAAAAA"
 	policyVersion4WithVendor2AndPurpose1Consent := "CPGWbY_PGWbY_GYAAAENABEAAIAAAAAAAAAAACEAAAAA"
-	policyVersion5WithVendor2AndPurpose1Consent := "CPGWbY_PGWbY_GYAAAENABFAAIAAAAAAAAAAACEAAAAA"
 
 	specVersion2vendorListData := MarshalVendorList(vendorList{
 		GVLSpecificationVersion: 2,
@@ -1359,11 +1396,6 @@ func TestVendorListSelection(t *testing.T) {
 			consent:           policyVersion4WithVendor2AndPurpose1Consent,
 			expectedAllowSync: true,
 		},
-		{
-			name:        "consent_tcf_policy_version_5_causes_error",
-			consent:     policyVersion5WithVendor2AndPurpose1Consent,
-			expectedErr: true,
-		},
 	}
 
 	for _, tt := range tests {
diff --git a/usersync/chooser_test.go b/usersync/chooser_test.go
index 3b820b99..0071a2e3 100644
--- a/usersync/chooser_test.go
+++ b/usersync/chooser_test.go
@@ -238,8 +238,8 @@ func TestChooserEvaluate(t *testing.T) {
 		Redirect: NewUniformBidderFilter(BidderFilterModeExclude)}
 
 	cookieNeedsSync := Cookie{}
-	cookieAlreadyHasSyncForA := Cookie{uids: map[string]uidWithExpiry{"keyA": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}}
-	cookieAlreadyHasSyncForB := Cookie{uids: map[string]uidWithExpiry{"keyB": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}}
+	cookieAlreadyHasSyncForA := Cookie{uids: map[string]UIDEntry{"keyA": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}}
+	cookieAlreadyHasSyncForB := Cookie{uids: map[string]UIDEntry{"keyB": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}}
 
 	testCases := []struct {
 		description        string
diff --git a/usersync/cookie_test.go b/usersync/cookie_test.go
index ee5062ea..5b86df54 100644
--- a/usersync/cookie_test.go
+++ b/usersync/cookie_test.go
@@ -1,177 +1,597 @@
 package usersync
 
 import (
-	"encoding/base64"
+	"errors"
 	"net/http"
 	"net/http/httptest"
-	"strings"
 	"testing"
 	"time"
 
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/stretchr/testify/assert"
 )
 
-func TestOptOutCookie(t *testing.T) {
-	cookie := &Cookie{
-		uids:   map[string]uidWithExpiry{"appnexus": {UID: "test"}},
-		optOut: true,
+func TestReadCookie(t *testing.T) {
+	testCases := []struct {
+		name            string
+		givenRequest    *http.Request
+		givenHttpCookie *http.Cookie
+		givenCookie     *Cookie
+		givenDecoder    Decoder
+		expectedCookie  *Cookie
+	}{
+		{
+			name:         "simple-cookie",
+			givenRequest: httptest.NewRequest("POST", "http://www.prebid.com", nil),
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+				},
+				optOut: false,
+			},
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID: "UID",
+					},
+				},
+				optOut: false,
+			},
+		},
+		{
+			name:         "empty-cookie",
+			givenRequest: httptest.NewRequest("POST", "http://www.prebid.com", nil),
+			givenCookie:  &Cookie{},
+			expectedCookie: &Cookie{
+				uids:   map[string]UIDEntry{},
+				optOut: false,
+			},
+		},
+		{
+			name:         "nil-cookie",
+			givenRequest: httptest.NewRequest("POST", "http://www.prebid.com", nil),
+			givenCookie:  nil,
+			expectedCookie: &Cookie{
+				uids:   map[string]UIDEntry{},
+				optOut: false,
+			},
+		},
+		{
+			name:         "corruptted-http-cookie",
+			givenRequest: httptest.NewRequest("POST", "http://www.prebid.com", nil),
+			givenHttpCookie: &http.Cookie{
+				Name:  "uids",
+				Value: "bad base64 encoding",
+			},
+			givenCookie: nil,
+			expectedCookie: &Cookie{
+				uids:   map[string]UIDEntry{},
+				optOut: false,
+			},
+		},
 	}
-	ensureConsistency(t, cookie)
-}
 
-func TestEmptyOptOutCookie(t *testing.T) {
-	cookie := &Cookie{
-		uids:   make(map[string]uidWithExpiry),
-		optOut: true,
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			if test.givenCookie != nil {
+				httpCookie, err := ToHTTPCookie(test.givenCookie)
+				assert.NoError(t, err)
+				test.givenRequest.AddCookie(httpCookie)
+			} else if test.givenCookie == nil && test.givenHttpCookie != nil {
+				test.givenRequest.AddCookie(test.givenHttpCookie)
+			}
+			actualCookie := ReadCookie(test.givenRequest, Base64Decoder{}, &config.HostCookie{})
+			assert.Equal(t, test.expectedCookie.uids, actualCookie.uids)
+			assert.Equal(t, test.expectedCookie.optOut, actualCookie.optOut)
+		})
 	}
-	ensureConsistency(t, cookie)
 }
 
-func TestEmptyCookie(t *testing.T) {
-	cookie := &Cookie{
-		uids:   make(map[string]uidWithExpiry),
-		optOut: false,
+func TestWriteCookie(t *testing.T) {
+	encoder := Base64Encoder{}
+	decoder := Base64Decoder{}
+
+	testCases := []struct {
+		name               string
+		givenCookie        *Cookie
+		givenSetSiteCookie bool
+		expectedCookie     *Cookie
+	}{
+		{
+			name: "simple-cookie",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+				},
+				optOut: false,
+			},
+			givenSetSiteCookie: false,
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+				},
+				optOut: false,
+			},
+		},
+		{
+			name: "simple-cookie-opt-out",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+				},
+				optOut: true,
+			},
+			givenSetSiteCookie: true,
+			expectedCookie: &Cookie{
+				uids:   map[string]UIDEntry{},
+				optOut: true,
+			},
+		},
+		{
+			name: "cookie-multiple-uids",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+					"rubicon": {
+						UID:     "UID2",
+						Expires: time.Time{},
+					},
+				},
+				optOut: false,
+			},
+			givenSetSiteCookie: true,
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+					"rubicon": {
+						UID:     "UID2",
+						Expires: time.Time{},
+					},
+				},
+				optOut: false,
+			},
+		},
 	}
-	ensureConsistency(t, cookie)
-}
 
-func TestCookieWithData(t *testing.T) {
-	cookie := newSampleCookie()
-	ensureConsistency(t, cookie)
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			// Write Cookie
+			w := httptest.NewRecorder()
+			encodedCookie, err := encoder.Encode(test.givenCookie)
+			assert.NoError(t, err)
+			WriteCookie(w, encodedCookie, &config.HostCookie{}, test.givenSetSiteCookie)
+			writtenCookie := w.Header().Get("Set-Cookie")
+
+			// Read Cookie
+			header := http.Header{}
+			header.Add("Cookie", writtenCookie)
+			r := &http.Request{Header: header}
+			actualCookie := ReadCookie(r, decoder, &config.HostCookie{})
+
+			assert.Equal(t, test.expectedCookie, actualCookie)
+		})
+	}
 }
 
-func TestBidderNameGets(t *testing.T) {
-	cookie := newSampleCookie()
-	id, exists, _ := cookie.GetUID("adnxs")
-	if !exists {
-		t.Errorf("Cookie missing expected Appnexus ID")
-	}
-	if id != "123" {
-		t.Errorf("Bad appnexus id. Expected %s, got %s", "123", id)
+func TestSync(t *testing.T) {
+	testCases := []struct {
+		name           string
+		givenCookie    *Cookie
+		givenSyncerKey string
+		givenUID       string
+		expectedCookie *Cookie
+		expectedError  error
+	}{
+		{
+			name: "simple-sync",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{},
+			},
+			givenSyncerKey: "adnxs",
+			givenUID:       "123",
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID: "123",
+					},
+				},
+			},
+		},
+		{
+			name: "dont-allow-syncs",
+			givenCookie: &Cookie{
+				uids:   map[string]UIDEntry{},
+				optOut: true,
+			},
+			givenSyncerKey: "adnxs",
+			givenUID:       "123",
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{},
+			},
+			expectedError: errors.New("the user has opted out of prebid server cookie syncs"),
+		},
+		{
+			name: "audienceNetwork",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{},
+			},
+			givenSyncerKey: string(openrtb_ext.BidderAudienceNetwork),
+			givenUID:       "0",
+			expectedError:  errors.New("audienceNetwork uses a UID of 0 as \"not yet recognized\""),
+		},
 	}
 
-	id, exists, _ = cookie.GetUID("rubicon")
-	if !exists {
-		t.Errorf("Cookie missing expected Rubicon ID")
-	}
-	if id != "456" {
-		t.Errorf("Bad rubicon id. Expected %s, got %s", "456", id)
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			err := test.givenCookie.Sync(test.givenSyncerKey, test.givenUID)
+			if test.expectedError != nil {
+				assert.Equal(t, test.expectedError, err)
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, test.expectedCookie.uids[test.givenSyncerKey].UID, test.givenCookie.uids[test.givenSyncerKey].UID)
+			}
+		})
 	}
 }
 
-func TestRejectAudienceNetworkCookie(t *testing.T) {
-	raw := &Cookie{
-		uids: map[string]uidWithExpiry{
-			"audienceNetwork": newTempId("0", 10),
+func TestGetUIDs(t *testing.T) {
+	testCases := []struct {
+		name           string
+		givenCookie    *Cookie
+		expectedCookie *Cookie
+		expectedLen    int
+	}{
+		{
+			name: "two-uids",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID: "123",
+					},
+					"rubicon": {
+						UID: "456",
+					},
+				},
+			},
+			expectedLen: 2,
+		},
+		{
+			name: "one-uid",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID: "123",
+					},
+				},
+			},
+			expectedLen: 1,
+		},
+		{
+			name:        "empty",
+			givenCookie: &Cookie{},
+			expectedLen: 0,
+		},
+		{
+			name:        "nil",
+			givenCookie: nil,
+			expectedLen: 0,
 		},
-		optOut: false,
-	}
-	parsed := ParseCookie(raw.ToHTTPCookie(90 * 24 * time.Hour))
-	if parsed.HasLiveSync("audienceNetwork") {
-		t.Errorf("Cookie serializing and deserializing should delete audienceNetwork values of 0")
 	}
 
-	err := parsed.TrySync("audienceNetwork", "0")
-	if err == nil {
-		t.Errorf("Cookie should reject audienceNetwork values of 0.")
-	}
-	if parsed.HasLiveSync("audienceNetwork") {
-		t.Errorf("Cookie The cookie should have rejected the audienceNetwork sync.")
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			uids := test.givenCookie.GetUIDs()
+			assert.Len(t, uids, test.expectedLen)
+			for key, value := range uids {
+				assert.Equal(t, test.givenCookie.uids[key].UID, value)
+			}
+
+		})
 	}
 }
 
-func TestOptOutReset(t *testing.T) {
-	cookie := newSampleCookie()
+func TestWriteCookieUserAgent(t *testing.T) {
+	encoder := Base64Encoder{}
 
-	cookie.SetOptOut(true)
-	if cookie.AllowSyncs() {
-		t.Error("After SetOptOut(true), a cookie should not allow more user syncs.")
+	testCases := []struct {
+		name                string
+		givenUserAgent      string
+		givenCookie         *Cookie
+		givenHostCookie     config.HostCookie
+		givenSetSiteCookie  bool
+		expectedContains    string
+		expectedNotContains string
+	}{
+		{
+			name:           "same-site-none",
+			givenUserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+				},
+				optOut: false,
+			},
+			givenHostCookie:    config.HostCookie{},
+			givenSetSiteCookie: true,
+			expectedContains:   "; Secure;",
+		},
+		{
+			name:           "older-chrome-version",
+			givenUserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3770.142 Safari/537.36",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+				},
+				optOut: false,
+			},
+			givenHostCookie:     config.HostCookie{},
+			givenSetSiteCookie:  true,
+			expectedNotContains: "SameSite=none",
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			// Set Up
+			req := httptest.NewRequest("GET", "http://www.prebid.com", nil)
+			req.Header.Set("User-Agent", test.givenUserAgent)
+
+			// Write Cookie
+			w := httptest.NewRecorder()
+			encodedCookie, err := encoder.Encode(test.givenCookie)
+			assert.NoError(t, err)
+			WriteCookie(w, encodedCookie, &test.givenHostCookie, test.givenSetSiteCookie)
+			writtenCookie := w.Header().Get("Set-Cookie")
+
+			if test.expectedContains == "" {
+				assert.NotContains(t, writtenCookie, test.expectedNotContains)
+			} else {
+				assert.Contains(t, writtenCookie, test.expectedContains)
+			}
+		})
 	}
-	ensureConsistency(t, cookie)
 }
 
-func TestOptIn(t *testing.T) {
-	cookie := &Cookie{
-		uids:   make(map[string]uidWithExpiry),
-		optOut: true,
+func TestPrepareCookieForWrite(t *testing.T) {
+	encoder := Base64Encoder{}
+	decoder := Base64Decoder{}
+	cookieToSend := &Cookie{
+		uids: map[string]UIDEntry{
+			"1": newTempId("1234567890123456789012345678901234567890123456", 7),
+			"7": newTempId("abcdefghijklmnopqrstuvwxy", 1),
+			"2": newTempId("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 6),
+			"3": newTempId("123456789012345678901234567896123456789012345678", 5),
+			"4": newTempId("aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ", 4),
+			"5": newTempId("12345678901234567890123456789012345678901234567890", 3),
+			"6": newTempId("abcdefghij", 2),
+		},
+		optOut: false,
 	}
 
-	cookie.SetOptOut(false)
-	if !cookie.AllowSyncs() {
-		t.Error("After SetOptOut(false), a cookie should allow more user syncs.")
+	testCases := []struct {
+		name                     string
+		givenMaxCookieSize       int
+		expectedRemainingUidKeys []string
+	}{
+		{
+			name:               "no-uids-ejected",
+			givenMaxCookieSize: 2000,
+			expectedRemainingUidKeys: []string{
+				"1", "2", "3", "4", "5", "6", "7",
+			},
+		},
+		{
+			name:               "no-uids-ejected-2",
+			givenMaxCookieSize: 0,
+			expectedRemainingUidKeys: []string{
+				"1", "2", "3", "4", "5", "6", "7",
+			},
+		},
+		{
+			name:               "one-uid-ejected",
+			givenMaxCookieSize: 900,
+			expectedRemainingUidKeys: []string{
+				"1", "2", "3", "4", "5", "6",
+			},
+		},
+		{
+			name:               "four-uids-ejected",
+			givenMaxCookieSize: 500,
+			expectedRemainingUidKeys: []string{
+				"1", "2", "3",
+			},
+		},
+		{
+			name:               "all-but-one-uids-ejected",
+			givenMaxCookieSize: 300,
+			expectedRemainingUidKeys: []string{
+				"1",
+			},
+		},
+		{
+			name:                     "all-uids-ejected",
+			givenMaxCookieSize:       100,
+			expectedRemainingUidKeys: []string{},
+		},
+		{
+			name:                     "invalid-max-size",
+			givenMaxCookieSize:       -100,
+			expectedRemainingUidKeys: []string{},
+		},
 	}
-	ensureConsistency(t, cookie)
-}
 
-func TestParseCorruptedCookie(t *testing.T) {
-	raw := http.Cookie{
-		Name:  "uids",
-		Value: "bad base64 encoding",
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			encodedCookie, err := cookieToSend.PrepareCookieForWrite(&config.HostCookie{MaxCookieSizeBytes: test.givenMaxCookieSize}, encoder)
+			assert.NoError(t, err)
+			decodedCookie := decoder.Decode(encodedCookie)
+
+			for _, key := range test.expectedRemainingUidKeys {
+				_, ok := decodedCookie.uids[key]
+				assert.Equal(t, true, ok)
+			}
+			assert.Equal(t, len(decodedCookie.uids), len(test.expectedRemainingUidKeys))
+		})
 	}
-	parsed := ParseCookie(&raw)
-	ensureEmptyMap(t, parsed)
 }
 
-func TestParseCorruptedCookieJSON(t *testing.T) {
-	cookieData := base64.URLEncoding.EncodeToString([]byte("bad json"))
-	raw := http.Cookie{
-		Name:  "uids",
-		Value: cookieData,
+func TestSyncHostCookie(t *testing.T) {
+	testCases := []struct {
+		name            string
+		givenCookie     *Cookie
+		givenUID        string
+		givenHostCookie *config.HostCookie
+		expectedCookie  *Cookie
+		expectedError   error
+	}{
+		{
+			name: "simple-sync",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{},
+			},
+			givenHostCookie: &config.HostCookie{
+				Family:     "syncer",
+				CookieName: "adnxs",
+			},
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"syncer": {
+						UID: "some-user-id",
+					},
+				},
+			},
+		},
+		{
+			name: "uids-already-present",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"some-syncer": {
+						UID: "some-other-user-id",
+					},
+				},
+			},
+			givenHostCookie: &config.HostCookie{
+				Family:     "syncer",
+				CookieName: "adnxs",
+			},
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"syncer": {
+						UID: "some-user-id",
+					},
+					"some-syncer": {
+						UID: "some-other-user-id",
+					},
+				},
+			},
+		},
+		{
+			name: "host-already-synced",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"syncer": {
+						UID: "some-user-id",
+					},
+				},
+			},
+			givenHostCookie: &config.HostCookie{
+				Family:     "syncer",
+				CookieName: "adnxs",
+			},
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"syncer": {
+						UID: "some-user-id",
+					},
+				},
+			},
+		},
 	}
-	parsed := ParseCookie(&raw)
-	ensureEmptyMap(t, parsed)
-}
 
-func TestParseNilSyncMap(t *testing.T) {
-	raw := http.Cookie{
-		Name:  uidCookieName,
-		Value: "",
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			r := httptest.NewRequest("POST", "http://www.prebid.com", nil)
+			r.AddCookie(&http.Cookie{
+				Name:  test.givenHostCookie.CookieName,
+				Value: "some-user-id",
+			})
+
+			SyncHostCookie(r, test.givenCookie, test.givenHostCookie)
+			for key, value := range test.expectedCookie.uids {
+				assert.Equal(t, value.UID, test.givenCookie.uids[key].UID)
+			}
+		})
 	}
-	parsed := ParseCookie(&raw)
-	ensureEmptyMap(t, parsed)
-	ensureConsistency(t, parsed)
 }
 
-func TestParseOtherCookie(t *testing.T) {
-	req := httptest.NewRequest("POST", "http://www.prebid.com", nil)
-	otherCookieName := "other"
-	id := "some-user-id"
-	req.AddCookie(&http.Cookie{
-		Name:  otherCookieName,
-		Value: id,
-	})
-	parsed := ParseCookieFromRequest(req, &config.HostCookie{
-		Family:     "adnxs",
-		CookieName: otherCookieName,
-	})
-	val, _, _ := parsed.GetUID("adnxs")
-	if val != id {
-		t.Errorf("Bad cookie value. Expected %s, got %s", id, val)
+func TestBidderNameGets(t *testing.T) {
+	cookie := newSampleCookie()
+	id, exists, _ := cookie.GetUID("adnxs")
+	if !exists {
+		t.Errorf("Cookie missing expected Appnexus ID")
+	}
+	if id != "123" {
+		t.Errorf("Bad appnexus id. Expected %s, got %s", "123", id)
+	}
+
+	id, exists, _ = cookie.GetUID("rubicon")
+	if !exists {
+		t.Errorf("Cookie missing expected Rubicon ID")
+	}
+	if id != "456" {
+		t.Errorf("Bad rubicon id. Expected %s, got %s", "456", id)
 	}
 }
 
-func TestParseCookieFromRequestOptOut(t *testing.T) {
+func TestReadCookieOptOut(t *testing.T) {
 	optOutCookieName := "optOutCookieName"
 	optOutCookieValue := "optOutCookieValue"
+	decoder := Base64Decoder{}
 
-	existingCookie := *(&Cookie{
-		uids: map[string]uidWithExpiry{
+	cookie := *(&Cookie{
+		uids: map[string]UIDEntry{
 			"foo": newTempId("fooID", 1),
 			"bar": newTempId("barID", 2),
 		},
 		optOut: false,
-	}).ToHTTPCookie(24 * time.Hour)
+	})
+
+	existingCookie, _ := ToHTTPCookie(&cookie)
 
 	testCases := []struct {
 		description          string
-		givenExistingCookies []http.Cookie
+		givenExistingCookies []*http.Cookie
 		expectedEmpty        bool
 		expectedSetOptOut    bool
 	}{
 		{
 			description: "Opt Out Cookie",
-			givenExistingCookies: []http.Cookie{
+			givenExistingCookies: []*http.Cookie{
 				existingCookie,
 				{Name: optOutCookieName, Value: optOutCookieValue}},
 			expectedEmpty:     true,
@@ -179,14 +599,14 @@ func TestParseCookieFromRequestOptOut(t *testing.T) {
 		},
 		{
 			description: "No Opt Out Cookie",
-			givenExistingCookies: []http.Cookie{
+			givenExistingCookies: []*http.Cookie{
 				existingCookie},
 			expectedEmpty:     false,
 			expectedSetOptOut: false,
 		},
 		{
 			description: "Opt Out Cookie - Wrong Value",
-			givenExistingCookies: []http.Cookie{
+			givenExistingCookies: []*http.Cookie{
 				existingCookie,
 				{Name: optOutCookieName, Value: "wrong"}},
 			expectedEmpty:     false,
@@ -194,7 +614,7 @@ func TestParseCookieFromRequestOptOut(t *testing.T) {
 		},
 		{
 			description: "Opt Out Cookie - Wrong Name",
-			givenExistingCookies: []http.Cookie{
+			givenExistingCookies: []*http.Cookie{
 				existingCookie,
 				{Name: "wrong", Value: optOutCookieValue}},
 			expectedEmpty:     false,
@@ -202,7 +622,7 @@ func TestParseCookieFromRequestOptOut(t *testing.T) {
 		},
 		{
 			description: "Opt Out Cookie - No Host Cookies",
-			givenExistingCookies: []http.Cookie{
+			givenExistingCookies: []*http.Cookie{
 				{Name: optOutCookieName, Value: optOutCookieValue}},
 			expectedEmpty:     true,
 			expectedSetOptOut: true,
@@ -213,10 +633,10 @@ func TestParseCookieFromRequestOptOut(t *testing.T) {
 		req := httptest.NewRequest("POST", "http://www.prebid.com", nil)
 
 		for _, c := range test.givenExistingCookies {
-			req.AddCookie(&c)
+			req.AddCookie(c)
 		}
 
-		parsed := ParseCookieFromRequest(req, &config.HostCookie{
+		parsed := ReadCookie(req, decoder, &config.HostCookie{
 			Family: "foo",
 			OptOutCookie: config.Cookie{
 				Name:  optOutCookieName,
@@ -233,125 +653,59 @@ func TestParseCookieFromRequestOptOut(t *testing.T) {
 	}
 }
 
-func TestCookieReadWrite(t *testing.T) {
-	cookie := newSampleCookie()
-
-	received := writeThenRead(cookie, 0)
-	uid, exists, isLive := received.GetUID("adnxs")
-	if !exists || !isLive || uid != "123" {
-		t.Errorf("Received cookie should have the adnxs ID=123. Got %s", uid)
-	}
-
-	uid, exists, isLive = received.GetUID("rubicon")
-	if !exists || !isLive || uid != "456" {
-		t.Errorf("Received cookie should have the rubicon ID=456. Got %s", uid)
-	}
-
-	assert.True(t, received.HasAnyLiveSyncs(), "Has Live Syncs")
-	assert.Len(t, received.uids, 2, "Sync Count")
-}
-
-func TestNilCookie(t *testing.T) {
-	var nilCookie *Cookie
-
-	if nilCookie.HasLiveSync("anything") {
-		t.Error("nil cookies should respond with false when asked if they have a sync")
-	}
-
-	if nilCookie.HasAnyLiveSyncs() {
-		t.Error("nil cookies shouldn't have any syncs.")
-	}
-
-	if nilCookie.AllowSyncs() {
-		t.Error("nil cookies shouldn't allow syncs to take place.")
+func TestOptIn(t *testing.T) {
+	cookie := &Cookie{
+		uids:   make(map[string]UIDEntry),
+		optOut: true,
 	}
 
-	uid, hadUID, isLive := nilCookie.GetUID("anything")
-
-	if uid != "" {
-		t.Error("nil cookies should return empty strings for the UID.")
-	}
-	if hadUID {
-		t.Error("nil cookies shouldn't claim to have a UID mapping.")
-	}
-	if isLive {
-		t.Error("nil cookies shouldn't report live UID mappings.")
+	cookie.SetOptOut(false)
+	if !cookie.AllowSyncs() {
+		t.Error("After SetOptOut(false), a cookie should allow more user syncs.")
 	}
+	ensureConsistency(t, cookie)
 }
 
-func TestGetUIDs(t *testing.T) {
+func TestOptOutReset(t *testing.T) {
 	cookie := newSampleCookie()
-	uids := cookie.GetUIDs()
 
-	assert.Len(t, uids, 2, "GetUIDs should return user IDs for all bidders")
-	assert.Equal(t, "123", uids["adnxs"], "GetUIDs should return the correct user ID for each bidder")
-	assert.Equal(t, "456", uids["rubicon"], "GetUIDs should return the correct user ID for each bidder")
+	cookie.SetOptOut(true)
+	if cookie.AllowSyncs() {
+		t.Error("After SetOptOut(true), a cookie should not allow more user syncs.")
+	}
+	ensureConsistency(t, cookie)
 }
 
-func TestGetUIDsWithEmptyCookie(t *testing.T) {
-	cookie := &Cookie{}
-	uids := cookie.GetUIDs()
-
-	assert.Len(t, uids, 0, "GetUIDs shouldn't return any user syncs for an empty cookie")
+func TestOptOutCookie(t *testing.T) {
+	cookie := &Cookie{
+		uids:   make(map[string]UIDEntry),
+		optOut: true,
+	}
+	ensureConsistency(t, cookie)
 }
 
-func TestGetUIDsWithNilCookie(t *testing.T) {
-	var cookie *Cookie
-	uids := cookie.GetUIDs()
-
-	assert.Len(t, uids, 0, "GetUIDs shouldn't return any user syncs for a nil cookie")
+func newTempId(uid string, offset int) UIDEntry {
+	return UIDEntry{
+		UID:     uid,
+		Expires: time.Now().Add(time.Duration(offset) * time.Minute).UTC(),
+	}
 }
 
-func TestTrimCookiesClosestExpirationDates(t *testing.T) {
-	cookieToSend := &Cookie{
-		uids: map[string]uidWithExpiry{
-			"k1": newTempId("12345678901234567890123456789012345678901234567890", 7),
-			"k2": newTempId("abcdefghijklmnopqrstuvwxyz", 1),
-			"k3": newTempId("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 6),
-			"k4": newTempId("12345678901234567890123456789612345678901234567890", 5),
-			"k5": newTempId("aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ", 4),
-			"k6": newTempId("12345678901234567890123456789012345678901234567890", 3),
-			"k7": newTempId("abcdefghijklmnopqrstuvwxyz", 2),
+func newSampleCookie() *Cookie {
+	return &Cookie{
+		uids: map[string]UIDEntry{
+			"adnxs":   newTempId("123", 10),
+			"rubicon": newTempId("456", 10),
 		},
 		optOut: false,
 	}
-
-	type aTest struct {
-		maxCookieSize int
-		expKeys       []string
-	}
-	testCases := []aTest{
-		{maxCookieSize: 2000, expKeys: []string{"k1", "k2", "k3", "k4", "k5", "k6", "k7"}}, //1 don't trim, set
-		{maxCookieSize: 0, expKeys: []string{"k1", "k2", "k3", "k4", "k5", "k6", "k7"}},    //2 unlimited size: don't trim, set
-		{maxCookieSize: 800, expKeys: []string{"k1", "k5", "k4", "k3", "k6"}},              //3 trim to size and set
-		{maxCookieSize: 500, expKeys: []string{"k1", "k4", "k3"}},                          //4 trim to size and set
-		{maxCookieSize: 200, expKeys: []string{}},                                          //5 insufficient size, trim to zero length and set
-		{maxCookieSize: -100, expKeys: []string{}},                                         //6 invalid size, trim to zero length and set
-	}
-	for i := range testCases {
-		processedCookie := writeThenRead(cookieToSend, testCases[i].maxCookieSize)
-
-		actualKeys := make([]string, 0, 7)
-		for key := range processedCookie.uids {
-			actualKeys = append(actualKeys, key)
-		}
-
-		assert.ElementsMatch(t, testCases[i].expKeys, actualKeys, "[Test %d]", i+1)
-	}
-}
-
-func ensureEmptyMap(t *testing.T, cookie *Cookie) {
-	if !cookie.AllowSyncs() {
-		t.Error("Empty cookies should allow user syncs.")
-	}
-	if cookie.HasAnyLiveSyncs() {
-		t.Error("Empty cookies shouldn't have any user syncs. Found at least 1.")
-	}
 }
 
 func ensureConsistency(t *testing.T, cookie *Cookie) {
+	decoder := Base64Decoder{}
+
 	if cookie.AllowSyncs() {
-		err := cookie.TrySync("pulsepoint", "1")
+		err := cookie.Sync("pulsepoint", "1")
 		if err != nil {
 			t.Errorf("Cookie sync should succeed if the user has opted in.")
 		}
@@ -380,13 +734,14 @@ func ensureConsistency(t *testing.T, cookie *Cookie) {
 			t.Error("If the user opted out, the PBSCookie should have no user syncs.")
 		}
 
-		err := cookie.TrySync("adnxs", "123")
+		err := cookie.Sync("adnxs", "123")
 		if err == nil {
 			t.Error("TrySync should fail if the user has opted out of PBSCookie syncs, but it succeeded.")
 		}
 	}
-
-	copiedCookie := ParseCookie(cookie.ToHTTPCookie(90 * 24 * time.Hour))
+	httpCookie, err := ToHTTPCookie(cookie)
+	assert.NoError(t, err)
+	copiedCookie := decoder.Decode(httpCookie.Value)
 	if copiedCookie.AllowSyncs() != cookie.AllowSyncs() {
 		t.Error("The PBSCookie interface shouldn't let modifications happen if the user has opted out")
 	}
@@ -414,61 +769,17 @@ func ensureConsistency(t *testing.T, cookie *Cookie) {
 	}
 }
 
-func newTempId(uid string, offset int) uidWithExpiry {
-	return uidWithExpiry{
-		UID:     uid,
-		Expires: time.Now().Add(time.Duration(offset) * time.Minute).UTC(),
+func ToHTTPCookie(cookie *Cookie) (*http.Cookie, error) {
+	encoder := Base64Encoder{}
+	encodedCookie, err := encoder.Encode(cookie)
+	if err != nil {
+		return nil, nil
 	}
-}
 
-func newSampleCookie() *Cookie {
-	return &Cookie{
-		uids: map[string]uidWithExpiry{
-			"adnxs":   newTempId("123", 10),
-			"rubicon": newTempId("456", 10),
-		},
-		optOut: false,
-	}
-}
-
-func writeThenRead(cookie *Cookie, maxCookieSize int) *Cookie {
-	w := httptest.NewRecorder()
-	hostCookie := &config.HostCookie{Domain: "mock-domain", MaxCookieSizeBytes: maxCookieSize}
-	cookie.SetCookieOnResponse(w, false, hostCookie, 90*24*time.Hour)
-	writtenCookie := w.HeaderMap.Get("Set-Cookie")
-
-	header := http.Header{}
-	header.Add("Cookie", writtenCookie)
-	request := http.Request{Header: header}
-	return ParseCookieFromRequest(&request, hostCookie)
-}
-
-func TestSetCookieOnResponseForSameSiteNone(t *testing.T) {
-	cookie := newSampleCookie()
-	w := httptest.NewRecorder()
-	req := httptest.NewRequest("GET", "http://www.prebid.com", nil)
-	ua := "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"
-	req.Header.Set("User-Agent", ua)
-	hostCookie := &config.HostCookie{Domain: "mock-domain", MaxCookieSizeBytes: 0}
-	cookie.SetCookieOnResponse(w, true, hostCookie, 90*24*time.Hour)
-	writtenCookie := w.HeaderMap.Get("Set-Cookie")
-	t.Log("Set-Cookie is: ", writtenCookie)
-	if !strings.Contains(writtenCookie, "; Secure;") {
-		t.Error("Set-Cookie should contain Secure")
-	}
-}
-
-func TestSetCookieOnResponseForOlderChromeVersion(t *testing.T) {
-	cookie := newSampleCookie()
-	w := httptest.NewRecorder()
-	req := httptest.NewRequest("GET", "http://www.prebid.com", nil)
-	ua := "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3770.142 Safari/537.36"
-	req.Header.Set("User-Agent", ua)
-	hostCookie := &config.HostCookie{Domain: "mock-domain", MaxCookieSizeBytes: 0}
-	cookie.SetCookieOnResponse(w, false, hostCookie, 90*24*time.Hour)
-	writtenCookie := w.HeaderMap.Get("Set-Cookie")
-	t.Log("Set-Cookie is: ", writtenCookie)
-	if strings.Contains(writtenCookie, "SameSite=none") {
-		t.Error("Set-Cookie should not contain SameSite=none")
-	}
+	return &http.Cookie{
+		Name:    uidCookieName,
+		Value:   encodedCookie,
+		Expires: time.Now().Add((90 * 24 * time.Hour)),
+		Path:    "/",
+	}, nil
 }
diff --git a/usersync/encoder_decoder_test.go b/usersync/encoder_decoder_test.go
new file mode 100644
index 00000000..5a87d4e7
--- /dev/null
+++ b/usersync/encoder_decoder_test.go
@@ -0,0 +1,149 @@
+package usersync
+
+import (
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestEncoderDecoder(t *testing.T) {
+	encoder := Base64Encoder{}
+	decoder := Base64Decoder{}
+
+	testCases := []struct {
+		name            string
+		givenRequest    *http.Request
+		givenHttpCookie *http.Cookie
+		givenCookie     *Cookie
+		givenDecoder    Decoder
+		expectedCookie  *Cookie
+	}{
+		{
+			name: "simple-cookie",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+				},
+				optOut: false,
+			},
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID: "UID",
+					},
+				},
+				optOut: false,
+			},
+		},
+		{
+			name:        "empty-cookie",
+			givenCookie: &Cookie{},
+			expectedCookie: &Cookie{
+				uids:   map[string]UIDEntry{},
+				optOut: false,
+			},
+		},
+		{
+			name:        "nil-cookie",
+			givenCookie: nil,
+			expectedCookie: &Cookie{
+				uids:   map[string]UIDEntry{},
+				optOut: false,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			encodedCookie, err := encoder.Encode(test.givenCookie)
+			assert.NoError(t, err)
+			decodedCookie := decoder.Decode(encodedCookie)
+
+			assert.Equal(t, test.expectedCookie.uids, decodedCookie.uids)
+			assert.Equal(t, test.expectedCookie.optOut, decodedCookie.optOut)
+		})
+	}
+}
+
+func TestEncoder(t *testing.T) {
+	encoder := Base64Encoder{}
+
+	testCases := []struct {
+		name                  string
+		givenCookie           *Cookie
+		expectedEncodedCookie string
+	}{
+		{
+			name: "simple-cookie",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+				},
+				optOut: false,
+			},
+			expectedEncodedCookie: "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiJVSUQiLCJleHBpcmVzIjoiMDAwMS0wMS0wMVQwMDowMDowMFoifX19",
+		},
+		{
+			name:                  "empty-cookie",
+			givenCookie:           &Cookie{},
+			expectedEncodedCookie: "e30=",
+		},
+		{
+			name:                  "nil-cookie",
+			givenCookie:           nil,
+			expectedEncodedCookie: "bnVsbA==",
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			encodedCookie, err := encoder.Encode(test.givenCookie)
+			assert.NoError(t, err)
+
+			assert.Equal(t, test.expectedEncodedCookie, encodedCookie)
+		})
+	}
+}
+
+func TestDecoder(t *testing.T) {
+	decoder := Base64Decoder{}
+
+	testCases := []struct {
+		name               string
+		givenEncodedCookie string
+		expectedCookie     *Cookie
+	}{
+		{
+			name:               "simple-encoded-cookie",
+			givenEncodedCookie: "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiJVSUQiLCJleHBpcmVzIjoiMDAwMS0wMS0wMVQwMDowMDowMFoifX19",
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID: "UID",
+					},
+				},
+				optOut: false,
+			},
+		},
+		{
+			name:               "nil-encoded-cookie",
+			givenEncodedCookie: "",
+			expectedCookie:     NewCookie(),
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			decodedCookie := decoder.Decode(test.givenEncodedCookie)
+			assert.Equal(t, test.expectedCookie, decodedCookie)
+		})
+	}
+}