VW/Audi/Seat/Skoda: recover from marketing consent prompt (#29980)

This commit is contained in:
andig 2026-05-17 16:18:34 +02:00 • committed by GitHub
parent 1200fbc2f9
commit 5506053dd3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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")
}