Fix Tesla non-MFA login once more (#716)

This commit is contained in:
andig 2021-03-02 13:59:07 +01:00 • committed by GitHub
parent 8ea528ec04
commit 1b5b8e283f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 31 additions and 162 deletions

View file

@ -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:")

5
go.mod
View file

@ -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

7
go.sum
View file

@ -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=

View file

@ -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()

View file

@ -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")
}