Fix Tesla non-MFA login once more (#716)
This commit is contained in:
parent
8ea528ec04
commit
1b5b8e283f
5 changed files with 31 additions and 162 deletions
21
cmd/tesla.go
21
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:")
|
||||
|
|
|
|||
5
go.mod
5
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
|
||||
|
|
|
|||
7
go.sum
7
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=
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue