Fix BMW login (#2766)
This commit is contained in:
parent
ed4fcdffcc
commit
795634c329
2 changed files with 136 additions and 59 deletions
|
|
@ -41,19 +41,30 @@ func NewAPI(log *util.Logger, brand string, identity oauth2.TokenSource) *API {
|
|||
return v
|
||||
}
|
||||
|
||||
func (v *API) eadrax() (VehiclesStatusResponse, error) {
|
||||
var res VehiclesStatusResponse
|
||||
uri := fmt.Sprintf("%s/eadrax-vcs/v1/vehicles?apptimezone=60&appDateTime=%d", CocoApiURI, time.Now().Unix())
|
||||
|
||||
req, err := request.New(http.MethodGet, uri, nil, map[string]string{
|
||||
"Content-Type": request.JSONContent,
|
||||
"X-User-Agent": v.xUserAgent,
|
||||
})
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Vehicles implements returns the /user/vehicles api
|
||||
func (v *API) Vehicles() ([]string, error) {
|
||||
var resp VehiclesResponse
|
||||
uri := fmt.Sprintf("%s/user/vehicles", ApiURI)
|
||||
// uri := fmt.Sprintf("%s/eadrax-vcs/v1/vehicles", CocoApiURI, vin)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, uri, nil)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &resp)
|
||||
resp, err := v.eadrax()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var vehicles []string
|
||||
for _, v := range resp.Vehicles {
|
||||
for _, v := range resp {
|
||||
vehicles = append(vehicles, v.VIN)
|
||||
}
|
||||
|
||||
|
|
@ -62,16 +73,7 @@ func (v *API) Vehicles() ([]string, error) {
|
|||
|
||||
// Status implements the /user/vehicles/<vin>/status api
|
||||
func (v *API) Status(vin string) (VehicleStatus, error) {
|
||||
var resp VehiclesStatusResponse
|
||||
uri := fmt.Sprintf("%s/eadrax-vcs/v1/vehicles?apptimezone=60&appDateTime=%d", CocoApiURI, time.Now().Unix())
|
||||
|
||||
req, err := request.New(http.MethodGet, uri, nil, map[string]string{
|
||||
"X-User-Agent": v.xUserAgent,
|
||||
})
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &resp)
|
||||
}
|
||||
|
||||
resp, err := v.eadrax()
|
||||
if err == nil {
|
||||
for _, res := range resp {
|
||||
if res.VIN == vin {
|
||||
|
|
|
|||
|
|
@ -1,22 +1,26 @@
|
|||
package bmw
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/oauth"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
cv "github.com/nirasan/go-oauth-pkce-code-verifier"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const AuthURI = "https://customer.bmwgroup.com/gcdm/oauth/authenticate"
|
||||
const (
|
||||
AuthURI = "https://customer.bmwgroup.com/gcdm/oauth"
|
||||
RedirectURI = "com.bmw.connected://oauth"
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
*request.Helper
|
||||
|
|
@ -46,62 +50,133 @@ func (v *Identity) Login(user, password string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (v *Identity) RefreshToken(_ *oauth2.Token) (*oauth2.Token, error) {
|
||||
data := url.Values{
|
||||
"username": []string{v.user},
|
||||
"password": []string{v.password},
|
||||
"client_id": []string{"31c357a0-7a1d-4590-aa99-33b97244d048"},
|
||||
"redirect_uri": []string{"com.bmw.connected://oauth"},
|
||||
"response_type": []string{"token"},
|
||||
"scope": []string{"authenticate_user vehicle_data remote_services"},
|
||||
"nonce": []string{"login_nonce"},
|
||||
func (v *Identity) retrieveToken(data url.Values) (*oauth2.Token, error) {
|
||||
var tok struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
|
||||
req, err := request.New(http.MethodPost, AuthURI, strings.NewReader(data.Encode()), request.URLEncoding)
|
||||
uri := fmt.Sprintf("%s/token", AuthURI)
|
||||
req, err := request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), map[string]string{
|
||||
"Content-Type": request.FormContent,
|
||||
"Authorization": "Basic MzFjMzU3YTAtN2ExZC00NTkwLWFhOTktMzNiOTcyNDRkMDQ4OmMwZTMzOTNkLTcwYTItNGY2Zi05ZDNjLTg1MzBhZjY0ZDU1Mg==",
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &tok)
|
||||
}
|
||||
|
||||
token := &oauth2.Token{
|
||||
AccessToken: tok.AccessToken,
|
||||
RefreshToken: tok.RefreshToken,
|
||||
Expiry: time.Now().Add(time.Duration(tok.ExpiresIn) * time.Second),
|
||||
}
|
||||
|
||||
return token, err
|
||||
}
|
||||
|
||||
func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
|
||||
if token == nil || token.RefreshToken == "" {
|
||||
return v.login()
|
||||
}
|
||||
|
||||
data := url.Values{
|
||||
"redirect_uri": []string{RedirectURI},
|
||||
"refresh_token": []string{token.RefreshToken},
|
||||
"grant_type": []string{"refresh_token"},
|
||||
}
|
||||
|
||||
return v.retrieveToken(data)
|
||||
}
|
||||
|
||||
func (v *Identity) login() (*oauth2.Token, error) {
|
||||
// don't follow redirects
|
||||
v.Client.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }
|
||||
defer func() { v.Client.CheckRedirect = nil }()
|
||||
|
||||
cv, err := cv.CreateCodeVerifier()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// don't follow redirects
|
||||
v.Client.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }
|
||||
defer func() { v.Client.CheckRedirect = nil }()
|
||||
v.Jar, err = cookiejar.New(&cookiejar.Options{
|
||||
PublicSuffixList: publicsuffix.List,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := url.Values{
|
||||
"client_id": []string{"31c357a0-7a1d-4590-aa99-33b97244d048"},
|
||||
"response_type": []string{"code"},
|
||||
"redirect_uri": []string{RedirectURI},
|
||||
"state": []string{"cwU-gIE27j67poy2UcL3KQ"},
|
||||
"scope": []string{"authenticate_user vehicle_data remote_services"},
|
||||
"nonce": []string{"login_nonce"},
|
||||
"code_challenge_method": []string{"S256"},
|
||||
"code_challenge": []string{cv.CodeChallengeS256()},
|
||||
"username": []string{v.user},
|
||||
"password": []string{v.password},
|
||||
"grant_type": []string{"authorization_code"},
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s/authenticate", AuthURI)
|
||||
req, err := request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), request.URLEncoding)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res struct {
|
||||
RedirectTo string `json:"redirect_to"`
|
||||
}
|
||||
|
||||
if err := v.DoJSON(req, &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query, err := url.ParseQuery(strings.TrimPrefix(res.RedirectTo, "redirect_uri=com.bmw.connected://oauth?"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
auth := query.Get("authorization")
|
||||
if auth == "" {
|
||||
return nil, errors.New("authorization code not found")
|
||||
}
|
||||
|
||||
data.Set("authorization", auth)
|
||||
delete(data, "username")
|
||||
delete(data, "password")
|
||||
delete(data, "grant_type")
|
||||
|
||||
req, err = request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), request.URLEncoding)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := v.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
var desc struct {
|
||||
Error string
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&desc); err != nil {
|
||||
return nil, fmt.Errorf("could not obtain token")
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%s:%s", desc.Error, desc.ErrorDescription)
|
||||
uri = resp.Header.Get("Location")
|
||||
if uri == "" {
|
||||
return nil, errors.New("authorization code not found")
|
||||
}
|
||||
|
||||
uri := resp.Header.Get("Location")
|
||||
|
||||
query, err := url.ParseQuery(strings.TrimPrefix(uri, "com.bmw.connected://oauth#"))
|
||||
query, err = url.ParseQuery(strings.TrimPrefix(uri, "com.bmw.connected://oauth?"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
at := query.Get("access_token")
|
||||
expires, err := strconv.Atoi(query.Get("expires_in"))
|
||||
if err != nil || at == "" || expires == 0 {
|
||||
return nil, errors.New("could not obtain token")
|
||||
data = url.Values{
|
||||
"code": []string{query.Get("code")},
|
||||
"redirect_uri": []string{RedirectURI},
|
||||
"grant_type": []string{"authorization_code"},
|
||||
"code_verifier": []string{cv.CodeChallengePlain()},
|
||||
}
|
||||
|
||||
token := &oauth2.Token{
|
||||
AccessToken: at,
|
||||
Expiry: time.Now().Add(time.Duration(expires) * time.Second),
|
||||
}
|
||||
|
||||
return token, nil
|
||||
return v.retrieveToken(data)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue