evcc-io/charger/ghostone/identity.go

94 lines
2.1 KiB
Go

package ghostone
import (
"context"
"errors"
"net/http"
"net/url"
"strings"
"time"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/oauth"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/transport"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/oauth2"
)
type tokenSource struct {
*request.Helper
oauth2.TokenSource
uri string
user string
password string
}
// TokenSource creates a JWT token source for the ghost REST API
func TokenSource(ctx context.Context, log *util.Logger, uri, user, password string) (oauth2.TokenSource, error) {
c := &tokenSource{
Helper: request.NewHelper(log),
uri: uri + "/jwt/login",
user: user,
password: password,
}
c.Client.Transport = transport.Insecure()
token, err := c.login(ctx)
if err != nil {
return nil, err
}
c.TokenSource = oauth.RefreshTokenSource(token, c.refresh)
return c, nil
}
func (c *tokenSource) login(ctx context.Context) (*oauth2.Token, error) {
data := url.Values{
"user": {c.user},
"pass": {c.password},
}
req, err := request.New(http.MethodPost, c.uri, strings.NewReader(data.Encode()), request.URLEncoding)
if err != nil {
return nil, err
}
resp, err := c.Do(req.WithContext(ctx))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := request.ResponseError(resp); err != nil {
return nil, err
}
token := resp.Header.Get("Authorization")
if token == "" {
return nil, errors.New("missing authorization header")
}
// strip "Bearer " prefix if present
token = strings.TrimPrefix(token, "Bearer ")
// extract expiry from JWT claims
expiry := time.Now().Add(10 * time.Minute) // fallback
var claims jwt.RegisteredClaims
if _, _, err := jwt.NewParser(jwt.WithoutClaimsValidation()).ParseUnverified(token, &claims); err == nil && claims.ExpiresAt != nil {
expiry = claims.ExpiresAt.Time
}
return &oauth2.Token{
AccessToken: token,
TokenType: "Bearer",
Expiry: expiry,
}, nil
}
func (c *tokenSource) refresh(_ *oauth2.Token) (*oauth2.Token, error) {
// re-login to get new token (no context needed- refresh runs in steady-state, not during init)
return c.login(context.Background())
}