VW: migrate WeConnect auth to OIDC token exchange (#30277)

This commit is contained in:
andig 2026-05-28 08:28:04 +02:00 • committed by GitHub
parent ec830ef20d
commit 4eada9ba5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 79 additions and 163 deletions

View file

@ -1,96 +0,0 @@
package loginapps
import (
"net/http"
"net/url"
"strings"
"time"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/oauth"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/urlvalues"
"github.com/evcc-io/evcc/vehicle/vag/cariad"
"golang.org/x/oauth2"
)
var Endpoint = &oauth2.Endpoint{
AuthURL: cariad.BaseURL + "/user-login/login/v1",
TokenURL: cariad.BaseURL + "/user-login/refresh/v1",
}
type Service struct {
*request.Helper
}
func New(log *util.Logger) *Service {
return &Service{
Helper: request.NewHelper(log),
}
}
func (v *Service) Exchange(q url.Values) (*Token, error) {
if err := urlvalues.Require(q, "state", "id_token", "access_token", "code"); err != nil {
return nil, err
}
data := map[string]string{
"region": "emea",
"redirect_uri": "weconnect://authenticated",
"state": q.Get("state"),
"id_token": q.Get("id_token"),
"access_token": q.Get("access_token"),
"authorizationCode": q.Get("code"),
}
var res Token
req, err := request.New(http.MethodPost, Endpoint.AuthURL, request.MarshalJSON(data), request.JSONEncoding)
if err == nil {
err = v.DoJSON(req, &res)
}
return &res, err
}
func (v *Service) Refresh(token *Token) (*Token, error) {
body := url.Values{
"grant_type": []string{"refresh_token"},
"refresh_token": []string{token.RefreshToken},
"client_id": []string{cariad.ClientID},
}
req, err := request.New(
http.MethodPost,
cariad.BaseURL+"/auth/v1/idk/oidc/token",
strings.NewReader(body.Encode()),
request.URLEncoding,
map[string]string{
"Connection": "keep-alive",
"User-Agent": cariad.UserAgent,
"Accept": "application/json",
"x-android-package-name": cariad.AndroidPackageName,
},
)
var res oauth2.Token
if err == nil {
err = v.DoJSON(req, &res)
}
res.Expiry = time.Now().Add(time.Duration(res.ExpiresIn) * time.Second)
t := Token(res)
return &t, err
}
// refreshToken renews the LoginApps token
func (v *Service) refreshToken(token *oauth2.Token) (*oauth2.Token, error) {
res, err := v.Refresh((*Token)(token))
return (*oauth2.Token)(res), err
}
// TokenSource creates a refreshing oauth2 token source
func (v *Service) TokenSource(token *Token) oauth2.TokenSource {
return oauth.RefreshTokenSource((*oauth2.Token)(token), v.refreshToken)
}

View file

@ -1,28 +0,0 @@
package loginapps
import (
"encoding/json"
"time"
"golang.org/x/oauth2"
)
// Token is the loginapps token
type Token oauth2.Token
func (t *Token) UnmarshalJSON(data []byte) error {
var s struct {
AccessToken string
RefreshToken string
}
err := json.Unmarshal(data, &s)
if err == nil {
t.TokenType = "bearer"
t.AccessToken = s.AccessToken
t.RefreshToken = s.RefreshToken
t.Expiry = time.Now().Add(time.Hour)
}
return err
}

View file

@ -1,31 +0,0 @@
package loginapps
import (
"encoding/json"
"testing"
)
func TestUnmarshalJSON(t *testing.T) {
var tok Token
str := `{"accesstoken":"access","refreshtoken":"refresh"}`
if err := json.Unmarshal([]byte(str), &tok); err != nil {
t.Error(err)
}
if tok.AccessToken != "access" {
t.Error("AccessToken")
}
if tok.RefreshToken != "refresh" {
t.Error("RefreshToken")
}
if tok.TokenType != "bearer" {
t.Error("TokenType")
}
if tok.Expiry.IsZero() {
t.Error("Expiry")
}
}

View file

@ -6,7 +6,6 @@ import (
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/vehicle/vag/loginapps"
"github.com/evcc-io/evcc/vehicle/vag/vwidentity"
"github.com/evcc-io/evcc/vehicle/vw/weconnect"
)
@ -49,18 +48,17 @@ func NewVWFromConfig(other map[string]any) (api.Vehicle, error) {
log := util.NewLogger("vw").Redact(cc.User, cc.Password, cc.VIN)
q, err := vwidentity.LoginWithAuthURL(log, weconnect.LoginURL, weconnect.AuthParams, cc.User, cc.Password)
q, err := vwidentity.Login(log, weconnect.AuthParams, cc.User, cc.Password)
if err != nil {
return nil, err
}
apps := loginapps.New(log)
token, err := apps.Exchange(q)
token, err := weconnect.ExchangeCode(log, q)
if err != nil {
return nil, err
}
api := weconnect.NewAPI(log, apps.TokenSource(token))
api := weconnect.NewAPI(log, weconnect.TokenSource(log, token))
api.Client.Timeout = cc.Timeout
vehicle, err := ensureVehicleEx(

View file

@ -0,0 +1,71 @@
package weconnect
import (
"net/http"
"net/url"
"strings"
"time"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/oauth"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/urlvalues"
"github.com/evcc-io/evcc/vehicle/vag/cariad"
"golang.org/x/oauth2"
)
const tokenURL = cariad.BaseURL + "/auth/v1/idk/oidc/token"
// ExchangeCode swaps the authorization code from the OIDC callback for an
// access/refresh token pair at the cariad BFF OIDC token endpoint. Replaces
// the legacy WeConnect SSO exchange (/user-login/login/v1), which VW removed
// in May 2026.
func ExchangeCode(log *util.Logger, q url.Values) (*oauth2.Token, error) {
if err := urlvalues.Require(q, "code", "code_verifier"); err != nil {
return nil, err
}
data := url.Values{
"grant_type": {"authorization_code"},
"code": {q.Get("code")},
"code_verifier": {q.Get("code_verifier")},
"redirect_uri": {"weconnect://authenticated"},
"client_id": {cariad.ClientID},
}
return postToken(log, data)
}
// TokenSource returns a refreshing oauth2.TokenSource that swaps refresh
// tokens for fresh access tokens at the same OIDC token endpoint.
func TokenSource(log *util.Logger, token *oauth2.Token) oauth2.TokenSource {
return oauth.RefreshTokenSource(token, func(t *oauth2.Token) (*oauth2.Token, error) {
data := url.Values{
"grant_type": {"refresh_token"},
"refresh_token": {t.RefreshToken},
"client_id": {cariad.ClientID},
}
return postToken(log, data)
})
}
func postToken(log *util.Logger, data url.Values) (*oauth2.Token, error) {
req, err := request.New(http.MethodPost, tokenURL, strings.NewReader(data.Encode()),
map[string]string{
"Content-Type": request.FormContent,
"Accept": request.JSONContent,
"User-Agent": cariad.UserAgent,
"x-android-package-name": cariad.AndroidPackageName,
})
if err != nil {
return nil, err
}
var token oauth2.Token
if err := request.NewHelper(log).DoJSON(req, &token); err != nil {
return nil, err
}
token.Expiry = time.Now().Add(time.Duration(token.ExpiresIn) * time.Second)
return &token, nil
}

View file

@ -6,11 +6,13 @@ import (
"github.com/evcc-io/evcc/vehicle/vag/cariad"
)
const LoginURL = cariad.BaseURL + "/user-login/v1/authorize"
// AuthParams are the OIDC authorize parameters for the WeConnect ID client.
// The authorize endpoint is the legacy identity.vwgroup.io OIDC entry point
// (vwidentity.Config.AuthURL); the token endpoint is the cariad BFF (see
// oauth.go).
var AuthParams = url.Values{
"response_type": {"code id_token token"},
"client_id": {"a24fba63-34b3-4d43-b181-942111e6bda8@apps_vw-dilab_com"},
"client_id": {cariad.ClientID},
"redirect_uri": {"weconnect://authenticated"},
"scope": {"openid profile badge cars vin"}, // dealers
}