From 5506053dd3686d8f8687c6241e73c0db308fa8a5 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 17 May 2026 16:18:34 +0200 Subject: [PATCH] VW/Audi/Seat/Skoda: recover from marketing consent prompt (#29980) --- vehicle/vag/vwidentity/endpoint.go | 67 ++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/vehicle/vag/vwidentity/endpoint.go b/vehicle/vag/vwidentity/endpoint.go index 7c27c650c..c4f206ab0 100644 --- a/vehicle/vag/vwidentity/endpoint.go +++ b/vehicle/vag/vwidentity/endpoint.go @@ -170,6 +170,15 @@ func (v *Service) loginLegacy(vars FormVars, user, password string) (url.Values, // GET identity.vwgroup.io/signin-service/v1/consent/users/bca09cc0-8eba-4110-af71-7242868e1bf1/b7a5bb47-f875-47cf-ab83-2ba3bf6bb738@apps_vw-dilab_com?scopes=openid%20profile%20birthdate%20nickname%20address%20phone%20cars%20mbb&relayState=15404cb51c8b4cc5efeee1d2c2a73e5b41562faa&callback=https://identity.vwgroup.io/oidc/v1/oauth/client/callback&hmac=a590931ca3cd9dc3a27f1d1c0c162bf1e5c5c32c9f5b40fcb36d4c6edc631e03 // GET identity.vwgroup.io/oidc/v1/oauth/client/callback/success?user_id=bca09cc0-8eba-4110-af71-7242868e1bf1&client_id=b7a5bb47-f875-47cf-ab83-2ba3bf6bb738@apps_vw-dilab_com&scopes=openid%20profile%20birthdate%20nickname%20address%20phone%20cars%20mbb&consentedScopes=openid%20profile%20birthdate%20nickname%20address%20phone%20cars%20mbb&relayState=f89a0b750c93e278a7ace170ce374e9cb9eb0a74&hmac=2b728f463c3cfe80f3271fbb35680e5e5218ca70025a46e7fadf7c7982decc2b + // VW periodically interjects an optional marketing consent page after an + // otherwise successful login. Skip it without consenting so evcc recovers + // automatically instead of requiring a restart (#29760). + if location, ok, err := v.skipMarketingConsent(resp); err != nil { + return nil, err + } else if ok { + return parseAuthLocation(location) + } + parsed, err := url.Parse(resp.Header.Get("Location")) if err != nil { return nil, err @@ -177,6 +186,57 @@ func (v *Service) loginLegacy(vars FormVars, user, password string) (url.Values, return parseAuthLocation(parsed) } +// marketingConsentCallback returns the OIDC callback url embedded in an +// optional VW/Audi marketing consent page. VW periodically interjects this +// page (path .../consent/marketing/...) after an otherwise successful login. +// It returns a nil url if u is not a marketing consent page. +func marketingConsentCallback(u *url.URL) (*url.URL, error) { + if u == nil || !strings.Contains(u.Path, "/consent/marketing/") { + return nil, nil + } + + callback := u.Query().Get("callback") + if callback == "" { + return nil, errors.New("marketing consent page is missing callback url") + } + + cb, err := url.Parse(callback) + if err != nil { + return nil, err + } + // normalize spaces in query values (e.g. scopes) so the request is valid + cb.RawQuery = cb.Query().Encode() + + return cb, nil +} + +// skipMarketingConsent detects the optional VW/Audi marketing consent page that +// the identity server periodically interjects and continues the login without +// consenting by following the OIDC callback embedded in the consent request. +// The bool result reports whether resp was such a consent page. +func (v *Service) skipMarketingConsent(resp *http.Response) (*url.URL, bool, error) { + if resp.Request == nil { + return nil, false, nil + } + + cb, err := marketingConsentCallback(resp.Request.URL) + if err != nil { + return nil, true, err + } + if cb == nil { + return nil, false, nil + } + + cbResp, err := v.Get(cb.String()) + if err != nil { + return nil, true, err + } + defer cbResp.Body.Close() + + location, err := url.Parse(cbResp.Header.Get("Location")) + return location, true, err +} + // loginNew performs the new VW identity login flow func (v *Service) loginNew(body []byte, user, password string) (url.Values, error) { state, err := extractState(body) @@ -203,6 +263,13 @@ func (v *Service) loginNew(body []byte, user, password string) (url.Values, erro return nil, errors.New(resp.Status) } + // skip the optional marketing consent page without consenting (#29760) + if loc, ok, err := v.skipMarketingConsent(resp); err != nil { + return nil, err + } else if ok { + return parseAuthLocation(loc) + } + if location == "" { return nil, errors.New("no redirect location in new login flow") }