From 1b5b8e283f8f83d0176b27300dfce8ee7cbf3242 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 2 Mar 2021 13:59:07 +0100 Subject: [PATCH] Fix Tesla non-MFA login once more (#716) --- cmd/tesla.go | 21 ++++---- go.mod | 5 +- go.sum | 7 +-- vehicle/tesla.go | 56 ++++++---------------- vehicle/tesla/client.go | 104 ---------------------------------------- 5 files changed, 31 insertions(+), 162 deletions(-) delete mode 100644 vehicle/tesla/client.go diff --git a/cmd/tesla.go b/cmd/tesla.go index 4acc7700f..0f05daf1f 100644 --- a/cmd/tesla.go +++ b/cmd/tesla.go @@ -10,11 +10,12 @@ import ( "github.com/andig/evcc/server" "github.com/andig/evcc/util" - auth "github.com/andig/evcc/vehicle/tesla" + "github.com/andig/evcc/util/request" + "github.com/bogosj/tesla" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/thoas/go-funk" - "github.com/uhthomas/tesla" + "golang.org/x/oauth2" ) // teslaCmd represents the vehicle command @@ -43,18 +44,18 @@ func codePrompt(ctx context.Context, devices []tesla.Device) (tesla.Device, stri return devices[0], strings.TrimSpace(code), err } -func generateToken(user, pass string) { - client, err := auth.NewClient(log) +func generateToken(username, password string) { + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewHelper(log).Client) + client, err := tesla.NewClient( + ctx, + tesla.WithMFAHandler(codePrompt), + tesla.WithCredentials(username, password), + ) if err != nil { log.FATAL.Fatalln(err) } - client.DeviceHandler(codePrompt) - - token, err := client.Login(user, pass) - if err != nil { - log.FATAL.Fatalln(err) - } + token := client.Token() fmt.Println() fmt.Println("Add the following tokens to the tesla vehicle config:") diff --git a/go.mod b/go.mod index 8e5924b34..3d68ce029 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef github.com/avast/retry-go v3.0.0+incompatible github.com/benbjohnson/clock v1.0.3 - github.com/bogosj/tesla v0.0.0-20210226163712-3a995277d27d + github.com/bogosj/tesla v0.0.0-20210301204612-577c8a183e4c github.com/containrrr/shoutrrr v0.4.0 github.com/denisbrodbeck/machineid v1.0.1 github.com/dylanmei/iso8601 v0.1.0 @@ -53,7 +53,6 @@ require ( github.com/spf13/viper v1.7.1 github.com/thoas/go-funk v0.7.0 github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c - github.com/uhthomas/tesla v0.1.1 github.com/volkszaehler/mbmd v0.0.0-20210117183837-59dcc46d62d4 golang.org/x/net v0.0.0-20201216054612-986b41b23924 golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93 @@ -63,3 +62,5 @@ require ( ) replace github.com/spf13/viper => github.com/andig/viper v1.6.3-0.20201123175942-a5af09afab5b + +replace github.com/bogosj/tesla => github.com/andig/tesla v0.0.0-20210302081706-8eb39050ad30 diff --git a/go.sum b/go.sum index 0696c0939..97766304a 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,8 @@ github.com/andig/evcc-config v0.0.0-20210210171605-531c04a6bb59/go.mod h1:N0hIjI github.com/andig/gosunspec v0.0.0-20200429133549-3cf6a82fed9c/go.mod h1:YkshK8WMzYn1iXAZzHUO75gIqhMSan2ctgBVtBkRIyA= github.com/andig/gosunspec v0.0.0-20201103081418-ec9af6feefde h1:QgUO3swXFjTXjqT+EA1k7XZbTxtl8AenEckO0UF3Dfw= github.com/andig/gosunspec v0.0.0-20201103081418-ec9af6feefde/go.mod h1:YkshK8WMzYn1iXAZzHUO75gIqhMSan2ctgBVtBkRIyA= +github.com/andig/tesla v0.0.0-20210302081706-8eb39050ad30 h1:JoYBcYk3LnToIarXQ6PqGcdvKqDk6E22p/QuWzgsWwg= +github.com/andig/tesla v0.0.0-20210302081706-8eb39050ad30/go.mod h1:xmG/yUw+GhxVB2m5GQLV4Vo/byvOf0wwhYGQ6IWPl+k= github.com/andig/viper v1.6.3-0.20201123175942-a5af09afab5b h1:n3O7DTcqZzpl8/6zVCCkHYbc/zyTfXQCk4xaTAkQ5aE= github.com/andig/viper v1.6.3-0.20201123175942-a5af09afab5b/go.mod h1:6ISKOGKh+gHA6RIFKvIhSS7V8qY41Gi2LG6QyIJVuCs= github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= @@ -63,8 +65,6 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bogosj/tesla v0.0.0-20210226163712-3a995277d27d h1:Ug4K8qKaxzZRo72YBdr9xMkxiYwSACnaJgKsVsOpaow= -github.com/bogosj/tesla v0.0.0-20210226163712-3a995277d27d/go.mod h1:xmG/yUw+GhxVB2m5GQLV4Vo/byvOf0wwhYGQ6IWPl+k= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -494,8 +494,6 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/uhthomas/tesla v0.1.1 h1:5w0XOqZ8+yx1ROqAyEXw2koJM8BjnbDC9QgZieCTTU4= -github.com/uhthomas/tesla v0.1.1/go.mod h1:VEiKAVT/KQUesEKv8ofrbxdY3kXgSVEOeBKfqfrYHr8= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= @@ -601,7 +599,6 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210126194326-f9ce19ea3013/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93 h1:alLDrZkL34Y2bnGHfvC1CYBRBXCXgx8AC2vY4MRtYX4= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/vehicle/tesla.go b/vehicle/tesla.go index c1d0a7556..0695a9d30 100644 --- a/vehicle/tesla.go +++ b/vehicle/tesla.go @@ -3,7 +3,6 @@ package vehicle import ( "context" "errors" - "fmt" "strings" "time" @@ -11,7 +10,6 @@ import ( "github.com/andig/evcc/provider" "github.com/andig/evcc/util" "github.com/andig/evcc/util/request" - auth "github.com/andig/evcc/vehicle/tesla" "github.com/bogosj/tesla" "golang.org/x/oauth2" ) @@ -59,21 +57,26 @@ func NewTeslaFromConfig(other map[string]interface{}) (api.Vehicle, error) { embed: &embed{cc.Title, cc.Capacity}, } + // authenticated http client with logging injected to the Tesla client log := util.NewLogger("tesla") - authClient, err := auth.NewClient(log) + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewHelper(log).Client) + + var options []tesla.ClientOption + if cc.Tokens.Access != "" { + options = append(options, tesla.WithToken(&oauth2.Token{ + AccessToken: cc.Tokens.Access, + RefreshToken: cc.Tokens.Refresh, + Expiry: time.Now(), + })) + } else { + options = append(options, tesla.WithCredentials(cc.User, cc.Password)) + } + + client, err := tesla.NewClient(ctx, options...) if err != nil { return nil, err } - token, err := teslaToken(authClient, cc.User, cc.Password, cc.Tokens) - if err != nil { - return nil, fmt.Errorf("login failed: %w", err) - } - - // authenticated http client with logging injected to the Tesla client - ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewHelper(log).Client) - client, _ := tesla.NewClient(ctx, token) - vehicles, err := client.Vehicles() if err != nil { return nil, err @@ -99,35 +102,6 @@ func NewTeslaFromConfig(other map[string]interface{}) (api.Vehicle, error) { return v, nil } -// teslaToken creates the Tesla OAuth token from given credentials -func teslaToken(auth *auth.Client, user, password string, tokens teslaTokens) (*oauth2.Token, error) { - // without tokens try to login - will fail if MFA enabled - if tokens.Access == "" { - token, err := auth.Login(user, password) - if err != nil { - err = fmt.Errorf("%w: if using multi-factor authentication, create tokens using `evcc tesla-token`", err) - } - - return token, err - } - - // create tokensource with given tokens - ctx := context.Background() - ts := auth.Config.TokenSource(ctx, &oauth2.Token{ - AccessToken: tokens.Access, - RefreshToken: tokens.Refresh, - Expiry: time.Now(), - }) - - // test the token source - token, err := ts.Token() - if err != nil { - err = fmt.Errorf("%w: token refresh failed, check access and refresh tokens are valid", err) - } - - return token, err -} - // chargeState implements the api.Vehicle interface func (v *Tesla) chargeState() (float64, error) { state, err := v.vehicle.ChargeState() diff --git a/vehicle/tesla/client.go b/vehicle/tesla/client.go deleted file mode 100644 index 8fe0f71e0..000000000 --- a/vehicle/tesla/client.go +++ /dev/null @@ -1,104 +0,0 @@ -package tesla - -import ( - "context" - "crypto/rand" - "crypto/sha256" - "encoding/base64" - "errors" - "fmt" - "io" - - "github.com/andig/evcc/util" - "github.com/andig/evcc/util/request" - "github.com/uhthomas/tesla" - "golang.org/x/oauth2" -) - -// Client is the tesla authentication client -type Client struct { - Config *oauth2.Config - auth *tesla.Auth - verifier string -} - -// github.com/uhthomas/tesla -func state() string { - var b [9]byte - if _, err := io.ReadFull(rand.Reader, b[:]); err != nil { - panic(err) - } - return base64.RawURLEncoding.EncodeToString(b[:]) -} - -// https://www.oauth.com/oauth2-servers/pkce/ -func pkce() (verifier, challenge string, err error) { - var p [87]byte - if _, err := io.ReadFull(rand.Reader, p[:]); err != nil { - return "", "", fmt.Errorf("rand read full: %w", err) - } - verifier = base64.RawURLEncoding.EncodeToString(p[:]) - b := sha256.Sum256([]byte(challenge)) - challenge = base64.RawURLEncoding.EncodeToString(b[:]) - return verifier, challenge, nil -} - -// NewClient creates a tesla authentication client -func NewClient(log *util.Logger) (*Client, error) { - config := &oauth2.Config{ - ClientID: "ownerapi", - ClientSecret: "", - RedirectURL: "https://auth.tesla.com/void/callback", - Scopes: []string{"openid email offline_access"}, - Endpoint: oauth2.Endpoint{ - AuthURL: "https://auth.tesla.com/oauth2/v3/authorize", - TokenURL: "https://auth.tesla.com/oauth2/v3/token", - }, - } - - verifier, challenge, err := pkce() - if err != nil { - return nil, fmt.Errorf("pkce: %w", err) - } - - auth := &tesla.Auth{ - Client: request.NewHelper(log).Client, - AuthURL: config.AuthCodeURL(state(), oauth2.AccessTypeOffline, - oauth2.SetAuthURLParam("code_challenge", challenge), - oauth2.SetAuthURLParam("code_challenge_method", "S256"), - ), - } - - client := &Client{ - Config: config, - auth: auth, - verifier: verifier, - } - client.DeviceHandler(client.mfaUnsupported) - - return client, nil -} - -// Login executes the MFA or non-MFA login -func (c *Client) Login(username, password string) (*oauth2.Token, error) { - ctx := context.Background() - code, err := c.auth.Do(ctx, username, password) - if err != nil { - return nil, err - } - - token, err := c.Config.Exchange(ctx, code, - oauth2.SetAuthURLParam("code_verifier", c.verifier), - ) - - return token, err -} - -// DeviceHandler sets an alternative authentication device handler -func (c *Client) DeviceHandler(handler func(context.Context, []tesla.Device) (tesla.Device, string, error)) { - c.auth.SelectDevice = handler -} - -func (c *Client) mfaUnsupported(_ context.Context, _ []tesla.Device) (tesla.Device, string, error) { - return tesla.Device{}, "", errors.New("multi factor authentication is not supported") -}