VW: migrate WeConnect auth to OIDC token exchange (#30277)
This commit is contained in:
parent
ec830ef20d
commit
4eada9ba5c
6 changed files with 79 additions and 163 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
71
vehicle/vw/weconnect/oauth.go
Normal file
71
vehicle/vw/weconnect/oauth.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue