evcc-io/vehicle/fiat/identity.go
2025-08-11 10:23:13 +02:00

237 lines
5.8 KiB
Go

package fiat
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"time"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/cognitoidentity"
"github.com/aws/aws-sdk-go-v2/service/cognitoidentity/types"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/samber/lo"
)
const (
LoginURI = "https://loginmyuconnect.fiat.com"
TokenURI = "https://authz.sdpr-01.fcagcv.com/v2/cognito/identity/token"
Region = "eu-west-1"
)
type Identity struct {
*request.Helper
ctx context.Context
user, password string
uid string
creds *types.Credentials
}
// NewIdentity creates Fiat identity
func NewIdentity(log *util.Logger, ctx context.Context, user, password string) *Identity {
return &Identity{
Helper: request.NewHelper(log),
ctx: ctx,
user: user,
password: password,
}
}
// Login authenticates with username/password to get new aws credentials
func (v *Identity) Login() error {
v.Client.Jar, _ = cookiejar.New(nil)
data := url.Values{
"APIKey": {ApiKey},
"pageURL": {"https://myuconnect.fiat.com/de/de/vehicle-services"},
"sdk": {"js_latest"},
"sdkBuild": {"12234"},
"format": {"json"},
}
uri := fmt.Sprintf("%s/accounts.webSdkBootstrap?%s", LoginURI, data.Encode())
req, _ := request.New(http.MethodGet, uri, nil, map[string]string{
"Accept": "*/*",
})
if _, err := v.Do(req); err != nil {
return err
}
var res struct {
ErrorInfo
UID string
StatusReason string
SessionInfo struct {
LoginToken string `json:"login_token"`
ExpiresIn string `json:"expires_in"`
}
}
uri = fmt.Sprintf("%s/accounts.login", LoginURI)
data = url.Values{
"loginID": {v.user},
"password": {v.password},
"sessionExpiration": {"7776000"},
"APIKey": {ApiKey},
"pageURL": {"https://myuconnect.fiat.com/de/de/login"},
"sdk": {"js_latest"},
"sdkBuild": {"12234"},
"format": {"json"},
"targetEnv": {"jssdk"},
"include": {"profile,data,emails"}, // subscriptions,preferences
"includeUserInfo": {"true"},
"loginMode": {"standard"},
"lang": {"de0de"},
"source": {"showScreenSet"},
"authMode": {"cookie"},
}
req, _ = request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), map[string]string{
"Accept": "*/*",
"Content-Type": "application/x-www-form-urlencoded",
})
if err := v.DoJSON(req, &res); err != nil {
return err
}
if err := res.ErrorInfo.Error(); err != nil {
return err
}
v.uid = res.UID
var token struct {
ErrorInfo
StatusReason string
IDToken string `json:"id_token"`
}
data = url.Values{
"fields": {"profile.firstName,profile.lastName,profile.email,country,locale,data.disclaimerCodeGSDP"}, // data.GSDPisVerified
"APIKey": {ApiKey},
"pageURL": {"https://myuconnect.fiat.com/de/de/dashboard"},
"sdk": {"js_latest"},
"sdkBuild": {"12234"},
"format": {"json"},
"login_token": {res.SessionInfo.LoginToken},
"authMode": {"cookie"},
}
uri = fmt.Sprintf("%s/accounts.getJWT?%s", LoginURI, data.Encode())
req, _ = request.New(http.MethodGet, uri, nil, map[string]string{
"Accept": "*/*",
})
if err := v.DoJSON(req, &token); err != nil {
return err
}
if err := token.ErrorInfo.Error(); err != nil {
return err
}
var identity struct {
Token, IdentityID string
}
gigya := struct {
GigyaToken string `json:"gigya_token"`
}{
GigyaToken: token.IDToken,
}
headers := map[string]string{
"Content-Type": "application/json",
"X-Clientapp-Version": "1.0",
"ClientRequestId": lo.RandomString(16, lo.LettersCharset),
"X-Api-Key": XApiKey,
"X-Originator-Type": "web",
}
req, _ = request.New(http.MethodPost, TokenURI, request.MarshalJSON(gigya), headers)
if err := v.DoJSON(req, &identity); err != nil {
return err
}
cfg, err := config.LoadDefaultConfig(v.ctx, config.WithRegion(Region))
if err != nil {
return err
}
svc := cognitoidentity.NewFromConfig(cfg)
credRes, err := svc.GetCredentialsForIdentity(v.ctx, &cognitoidentity.GetCredentialsForIdentityInput{
IdentityId: &identity.IdentityID,
Logins: map[string]string{
"cognito-identity.amazonaws.com": identity.Token,
},
})
if err != nil {
return err
}
v.creds = credRes.Credentials
return nil
}
// UID returns the logged in users uid
func (v *Identity) UID() string {
return v.uid
}
// Sign signs an AWS request using identity's credentials
func (v *Identity) Sign(req *http.Request, body io.ReadSeeker) error {
// refresh credentials
if v.creds.Expiration.Before(time.Now().Add(-time.Minute)) {
if err := v.Login(); err != nil {
return fmt.Errorf("login failed: %w", err)
}
}
// sign request
credProvider := credentials.NewStaticCredentialsProvider(
*v.creds.AccessKeyId, *v.creds.SecretKey, *v.creds.SessionToken,
)
credentials, err := credProvider.Retrieve(v.ctx)
if err != nil {
return err
}
payloadHash, err := hashBody(body)
if err != nil {
return err
}
signer := v4.NewSigner()
return signer.SignHTTP(v.ctx, credentials, req, payloadHash, "execute-api", Region, time.Now())
}
func hashBody(body io.ReadSeeker) (string, error) {
if body == nil {
// For empty payloads, use the SHA-256 hash of empty string
return "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", nil
}
// Read the body content
bodyBytes, err := io.ReadAll(body)
if err != nil {
return "", err
}
// Reset the body seeker to the beginning
if _, err := body.Seek(0, io.SeekStart); err != nil {
return "", err
}
// Calculate SHA-256 hash
hash := sha256.Sum256(bodyBytes)
return hex.EncodeToString(hash[:]), nil
}