VW: fix login (#25540)
This commit is contained in:
parent
d42122f7e8
commit
e6fe76f293
4 changed files with 179 additions and 52 deletions
20
api/error.go
20
api/error.go
|
|
@ -2,6 +2,7 @@ package api
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
)
|
||||
|
|
@ -43,6 +44,25 @@ func (err *ErrLoginRequired) Error() string {
|
|||
return "login required"
|
||||
}
|
||||
|
||||
// ErrUrl indicates that the error contains an extractable URL
|
||||
type ErrUrl struct {
|
||||
err string
|
||||
url *url.URL
|
||||
}
|
||||
|
||||
// UrlError creates an error message containing an url
|
||||
func UrlError(err string, url *url.URL) *ErrUrl {
|
||||
return &ErrUrl{err, url}
|
||||
}
|
||||
|
||||
func (err *ErrUrl) Error() string {
|
||||
return err.err
|
||||
}
|
||||
|
||||
func (err *ErrUrl) URL() *url.URL {
|
||||
return err.url
|
||||
}
|
||||
|
||||
// ErrAsleep indicates that vehicle is asleep. Caller may chose to wake up the vehicle and retry.
|
||||
var ErrAsleep error = errAsleep{}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ func ErrorAsJson(err error) any {
|
|||
Error string `json:"error"`
|
||||
Line int `json:"line,omitempty"`
|
||||
LoginRequired string `json:"loginRequired,omitempty"`
|
||||
URI string `json:"uri,omitempty"`
|
||||
}{
|
||||
Error: err.Error(),
|
||||
}
|
||||
|
|
@ -21,6 +22,10 @@ func ErrorAsJson(err error) any {
|
|||
res.LoginRequired = ae.ProviderAuth
|
||||
}
|
||||
|
||||
if ue := new(api.ErrUrl); errors.As(err, &ue) {
|
||||
res.URI = ue.URL().String()
|
||||
}
|
||||
|
||||
var (
|
||||
ype *yaml.ParserError
|
||||
yue *yaml.UnmarshalError
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
package cariad
|
||||
|
||||
const BaseURL = "https://emea.bff.cariad.digital"
|
||||
const (
|
||||
BaseURL = "https://emea.bff.cariad.digital"
|
||||
AndroidPackageName = "com.volkswagen.weconnect"
|
||||
UserAgent = "Volkswagen/3.51.1-android/14"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
package vwidentity
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/urlvalues"
|
||||
|
|
@ -91,79 +95,173 @@ func (v *Service) Login(uri, user, password string) (url.Values, error) {
|
|||
"state": {uuid.NewString()},
|
||||
}
|
||||
|
||||
var vars FormVars
|
||||
uri = uri + "&" + query.Encode()
|
||||
|
||||
// GET identity.vwgroup.io/signin-service/v1/signin/b7a5bb47-f875-47cf-ab83-2ba3bf6bb738@apps_vw-dilab_com?relayState=15404cb51c8b4cc5efeee1d2c2a73e5b41562faa
|
||||
resp, err := v.Get(uri)
|
||||
if err == nil {
|
||||
vars, err = FormValues(resp.Body, "form#emailPasswordForm")
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Try to extract legacy form, but don't fail if it's not found
|
||||
if vars, err := FormValues(bytes.NewReader(body), "form#emailPasswordForm"); err == nil {
|
||||
return v.loginLegacy(vars, user, password)
|
||||
}
|
||||
|
||||
return v.loginNew(body, user, password)
|
||||
}
|
||||
|
||||
// loginLegacy performs the legacy VW identity login flow
|
||||
func (v *Service) loginLegacy(vars FormVars, user, password string) (url.Values, error) {
|
||||
var params CredentialParams
|
||||
|
||||
// POST identity.vwgroup.io/signin-service/v1/b7a5bb47-f875-47cf-ab83-2ba3bf6bb738@apps_vw-dilab_com/login/identifier
|
||||
if err == nil {
|
||||
data := url.Values{
|
||||
"_csrf": {vars.Inputs["_csrf"]},
|
||||
"relayState": {vars.Inputs["relayState"]},
|
||||
"hmac": {vars.Inputs["hmac"]},
|
||||
"email": {user},
|
||||
}
|
||||
data := url.Values{
|
||||
"_csrf": {vars.Inputs["_csrf"]},
|
||||
"relayState": {vars.Inputs["relayState"]},
|
||||
"hmac": {vars.Inputs["hmac"]},
|
||||
"email": {user},
|
||||
}
|
||||
|
||||
uri = BaseURL + vars.Action
|
||||
if resp, err = v.PostForm(uri, data); err == nil {
|
||||
if params, err = ParseCredentialsPage(resp.Body); err == nil && params.TemplateModel.Error != "" {
|
||||
err = errors.New(params.TemplateModel.Error)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
uri := BaseURL + vars.Action
|
||||
resp, err := v.PostForm(uri, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if params, err = ParseCredentialsPage(resp.Body); err == nil && params.TemplateModel.Error != "" {
|
||||
err = errors.New(params.TemplateModel.Error)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// POST identity.vwgroup.io/signin-service/v1/b7a5bb47-f875-47cf-ab83-2ba3bf6bb738@apps_vw-dilab_com/login/authenticate
|
||||
if err == nil {
|
||||
data := url.Values{
|
||||
"_csrf": {params.CsrfToken},
|
||||
"relayState": {params.TemplateModel.RelayState},
|
||||
"hmac": {params.TemplateModel.Hmac},
|
||||
"email": {user},
|
||||
"password": {password},
|
||||
}
|
||||
data = url.Values{
|
||||
"_csrf": {params.CsrfToken},
|
||||
"relayState": {params.TemplateModel.RelayState},
|
||||
"hmac": {params.TemplateModel.Hmac},
|
||||
"email": {user},
|
||||
"password": {password},
|
||||
}
|
||||
|
||||
// reuse url from identifier step before
|
||||
uri = strings.ReplaceAll(uri, params.TemplateModel.IdentifierUrl, params.TemplateModel.PostAction)
|
||||
// reuse url from identifier step before
|
||||
uri = strings.ReplaceAll(uri, params.TemplateModel.IdentifierUrl, params.TemplateModel.PostAction)
|
||||
|
||||
if resp, err = v.PostForm(uri, data); err == nil {
|
||||
resp.Body.Close()
|
||||
resp, err = v.PostForm(uri, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
err = errors.New(resp.Status)
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if e := resp.Request.URL.Query().Get("error"); e != "" {
|
||||
err = errors.New(e)
|
||||
}
|
||||
|
||||
if consent := resp.Request.URL.Query().Get("updated") != "" || strings.Contains(resp.Request.URL.Path, "/consent/"); err == nil && consent {
|
||||
err = errors.New("terms of service updated- please open app or website and confirm: " + resp.Request.URL.String())
|
||||
}
|
||||
}
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
return nil, errors.New(resp.Status)
|
||||
}
|
||||
|
||||
// GET identity.vwgroup.io/oidc/v1/oauth/sso?clientId=b7a5bb47-f875-47cf-ab83-2ba3bf6bb738@apps_vw-dilab_com&relayState=15404cb51c8b4cc5efeee1d2c2a73e5b41562faa&userId=bca09cc0-8eba-4110-af71-7242868e1bf1&HMAC=2b01ce6a351fad4dd97dc8110d0967b46c95889ab5010c660a616462e66a83ca
|
||||
// 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
|
||||
|
||||
var location *url.URL
|
||||
if err == nil {
|
||||
loc := strings.ReplaceAll(resp.Header.Get("Location"), "#", "?") // convert to parseable url
|
||||
if location, err = url.Parse(loc); err == nil {
|
||||
return location.Query(), nil
|
||||
}
|
||||
parsed, err := url.Parse(resp.Header.Get("Location"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseAuthLocation(parsed)
|
||||
}
|
||||
|
||||
// loginNew performs the new VW identity login flow
|
||||
func (v *Service) loginNew(body []byte, user, password string) (url.Values, error) {
|
||||
state, err := extractState(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, err
|
||||
// POST to new login endpoint
|
||||
loginData := url.Values{
|
||||
"username": {user},
|
||||
"password": {password},
|
||||
"state": {state},
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s/u/login?state=%s", BaseURL, state)
|
||||
resp, err := v.PostForm(uri, loginData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
location := resp.Header.Get("Location")
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
return nil, errors.New(resp.Status)
|
||||
}
|
||||
|
||||
if location == "" {
|
||||
return nil, errors.New("no redirect location in new login flow")
|
||||
}
|
||||
|
||||
redirectURL, err := resolveLocation(resp.Request.URL, location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if redirectURL.Scheme == "https" || redirectURL.Scheme == "http" {
|
||||
return nil, fmt.Errorf("unexpected redirect URL scheme: %s, expected app specific url (e.g. 'weconnect://...')", redirectURL.Scheme)
|
||||
}
|
||||
|
||||
return parseAuthLocation(redirectURL)
|
||||
}
|
||||
|
||||
func resolveLocation(base *url.URL, location string) (*url.URL, error) {
|
||||
locURL, err := url.Parse(location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if locURL.IsAbs() {
|
||||
return locURL, nil
|
||||
}
|
||||
return base.ResolveReference(locURL), nil
|
||||
}
|
||||
|
||||
func parseAuthLocation(u *url.URL) (url.Values, error) {
|
||||
u.RawQuery = u.Fragment
|
||||
|
||||
if errStr := u.Query().Get("error"); errStr != "" {
|
||||
return nil, errors.New(errStr)
|
||||
}
|
||||
|
||||
if consent := u.Query().Get("updated") != "" || strings.Contains(u.Path, "/consent/"); consent {
|
||||
return nil, api.UrlError(
|
||||
fmt.Sprintf("terms of service updated- please open app or website and confirm: %s", u.String()),
|
||||
u,
|
||||
)
|
||||
}
|
||||
|
||||
return u.Query(), nil
|
||||
}
|
||||
|
||||
func extractState(body []byte) (string, error) {
|
||||
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
stateInput := doc.Find("input[name=state]").First()
|
||||
if stateInput.Length() == 0 {
|
||||
return "", errors.New("state parameter not found")
|
||||
}
|
||||
|
||||
state, ok := stateInput.Attr("value")
|
||||
if !ok || state == "" {
|
||||
return "", errors.New("state value not found")
|
||||
}
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue