Mercedes: generalize provider login (#2384)

This commit is contained in:
andig 2022-01-26 08:21:30 +01:00 • committed by GitHub
parent b031610594
commit b4a004b7bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 246 additions and 236 deletions

View file

@ -196,25 +196,10 @@ type WebController interface {
WebControl(*mux.Router)
}
type Callback struct {
Path string
Handler RedirectHandlerFunc
}
// RedirectHandlerFunc should return an http.HandlerFunc responding with an http.Redirect(..., redirectURi, ...)
type RedirectHandlerFunc func(redirectURI string) http.HandlerFunc
// ProviderLogin is the ability to provide OAuth authentication through the ui
type ProviderLogin interface {
SetBasePath(basePath string)
// Provides ....
Callback() Callback
SetOAuthCallbackURI(uri string)
LoggedIn() bool
LoginPath() string
SetCallbackParams(uri string, authenticated chan<- bool)
LoginHandler() http.HandlerFunc
LogoutPath() string
LogoutHandler() http.HandlerFunc
CallbackHandler(baseURI string) http.HandlerFunc
}

View file

@ -4,9 +4,6 @@ const { protocol, hostname, port, pathname } = window.location;
const baseAPI = axios.create({
baseURL: protocol + "//" + hostname + (port ? ":" + port : "") + pathname + "/",
headers: {
Accept: "application/json",
},
});
// global error handling

View file

@ -104,7 +104,7 @@ import Notifications from "../components/Notifications";
import store from "../store";
import authAPI from "../authapi";
import baseAPI from "../baseapi";
export default {
name: "App",
@ -155,14 +155,14 @@ export default {
return this.providerLogins.filter((login) => !login.loggedIn).length;
},
providerLogins() {
return this.store.state.loadpoints
.filter((lp) => lp.vehicleProviderLoginPath && lp.vehicleProviderLogoutPath)
.map((lp) => ({
title: lp.vehicleTitle,
loggedIn: lp.vehicleProviderLoggedIn,
loginPath: lp.vehicleProviderLoginPath,
logoutPath: lp.vehicleProviderLogoutPath,
}));
return this.store.state.auth
? Object.entries(this.store.state.auth.vehicles).map(([k, v]) => ({
title: k,
loggedIn: v.authenticated,
loginPath: v.uri + "/login",
logoutPath: v.uri + "/logout",
}))
: [];
},
},
created: function () {
@ -212,11 +212,11 @@ export default {
},
handleProviderAuthorization: async function (provider) {
if (!provider.loggedIn) {
authAPI.post(provider.loginPath).then(function (response) {
baseAPI.post(provider.loginPath).then(function (response) {
window.location.href = response.data.loginUri;
});
} else {
authAPI.post(provider.logoutPath);
baseAPI.post(provider.logoutPath);
}
},
},

View file

@ -14,8 +14,10 @@ import (
"github.com/evcc-io/evcc/provider/mqtt"
"github.com/evcc-io/evcc/push"
"github.com/evcc-io/evcc/server"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/vehicle"
"github.com/evcc-io/evcc/vehicle/wrapper"
"github.com/gorilla/handlers"
)
type config struct {
@ -79,6 +81,7 @@ type ConfigProvider struct {
chargers map[string]api.Charger
vehicles map[string]api.Vehicle
visited map[string]bool
auth *util.AuthCollection
}
func (cp *ConfigProvider) TrackVisitors() {
@ -200,25 +203,40 @@ func (cp *ConfigProvider) configureVehicles(conf config) error {
return nil
}
// webControl handles implemented routes by devices.
// for now only api.ProviderLogin related routes
func (cp *ConfigProvider) webControl(httpd *server.HTTPd) {
func canonicalName(s string) string {
return strings.ToLower(strings.ReplaceAll(s, " ", "_"))
}
// webControl handles routing for devices. For now only api.ProviderLogin related routes
func (cp *ConfigProvider) webControl(httpd *server.HTTPd, paramC chan<- util.Param) {
router := httpd.Router()
auth := router.PathPrefix("/auth").Subrouter()
auth.Use(handlers.CompressHandler)
auth.Use(handlers.CORS(
handlers.AllowedHeaders([]string{"Content-Type"}),
))
// initialize
cp.auth = util.NewAuthCollection(paramC)
for _, v := range cp.vehicles {
if provider, ok := v.(api.ProviderLogin); ok {
title := url.QueryEscape(strings.ToLower(strings.ReplaceAll(v.Title(), " ", "_")))
title := url.QueryEscape(canonicalName(v.Title()))
basePath := fmt.Sprintf("vehicles/%s", title)
basePath := fmt.Sprintf("/auth/vehicles/%s", title)
provider.SetBasePath(basePath)
// TODO make evccURI configurable, add warnings for any network/ localhost
evccURI := fmt.Sprintf("http://%s", httpd.Addr)
baseURI := fmt.Sprintf("%s/auth/%s", evccURI, basePath)
callback := provider.Callback()
callbackURI := fmt.Sprintf("http://%s%s", httpd.Addr, callback.Path)
{
provider.SetOAuthCallbackURI(callbackURI)
log.INFO.Printf("ensure the oauth client redirect/callback is configured for %s: %s", v.Title(), callbackURI)
}
// register vehicle
ap := cp.auth.Register(v.Title(), baseURI)
// TODO: how to handle multiple vehicles of the same type
redirectURI := fmt.Sprintf("%s/callback", baseURI)
provider.SetCallbackParams(redirectURI, ap.Handler())
log.INFO.Printf("ensure the oauth client redirect/callback is configured for %s: %s", v.Title(), redirectURI)
// TODO how to handle multiple vehicles of the same type
//
// problems, thoughts and ideas:
// conflicting callbacks!
@ -226,20 +244,20 @@ func (cp *ConfigProvider) webControl(httpd *server.HTTPd) {
// - or a general callback handler and the specific vehicle is transported in the state?
// - callback handler needs an option to set the token at the right vehicle and use the right code exchange
// TODO: what about https?
router.
auth.
Methods(http.MethodGet).
Path(callback.Path).
HandlerFunc(callback.Handler(fmt.Sprintf("http://%s", httpd.Addr)))
router.
Path(fmt.Sprintf("/%s/callback", basePath)).
HandlerFunc(provider.CallbackHandler(evccURI))
auth.
Methods(http.MethodPost).
Path(provider.LoginPath()).
Path(fmt.Sprintf("/%s/login", basePath)).
HandlerFunc(provider.LoginHandler())
router.
auth.
Methods(http.MethodPost).
Path(provider.LogoutPath()).
Path(fmt.Sprintf("/%s/logout", basePath)).
HandlerFunc(provider.LogoutHandler())
}
}
cp.auth.Publish()
}

View file

@ -188,9 +188,6 @@ func run(cmd *cobra.Command, args []string) {
socketHub := server.NewSocketHub()
httpd := server.NewHTTPd(uri, site, socketHub, cache)
// allow web access for vehicles
cp.webControl(httpd)
// metrics
if viper.GetBool("metrics") {
httpd.Router().Handle("/metrics", promhttp.Handler())
@ -219,6 +216,9 @@ func run(cmd *cobra.Command, args []string) {
valueChan <- util.Param{Key: "sponsor", Val: sponsor.Subject}
}
// allow web access for vehicles
cp.webControl(httpd, valueChan)
// version check
go updater.Run(log, httpd, tee, valueChan)

View file

@ -542,9 +542,6 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
lp.startVehicleDetection()
}
// publish providerLogins
lp.publishProviderLogins()
// read initial charger state to prevent immediately disabling charger
if enabled, err := lp.charger.Enabled(); err == nil {
if lp.enabled = enabled; enabled {
@ -1404,9 +1401,6 @@ func (lp *LoadPoint) Update(sitePower float64, cheap bool, batteryBuffered bool)
// update progress and soc before status is updated
lp.publishChargeProgress()
// publish providerLogins
lp.publishProviderLogins()
// read and publish status
if err := lp.updateChargerStatus(); err != nil {
lp.log.ERROR.Printf("charger: %v", err)
@ -1536,13 +1530,3 @@ func (lp *LoadPoint) Update(sitePower float64, cheap bool, batteryBuffered bool)
lp.log.ERROR.Println(err)
}
}
func (lp *LoadPoint) publishProviderLogins() {
for _, vehicle := range lp.vehicles {
if provider, ok := vehicle.(api.ProviderLogin); ok {
lp.publish("vehicleProviderLoggedIn", provider.LoggedIn())
lp.publish("vehicleProviderLoginPath", provider.LoginPath())
lp.publish("vehicleProviderLogoutPath", provider.LogoutPath())
}
}
}

File diff suppressed because one or more lines are too long

4
dist/index.html vendored
View file

@ -1,5 +1,5 @@
<!DOCTYPE html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="EV Charge Controller"><meta name="author" content="andig"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><link rel="apple-touch-icon" sizes="180x180" href="ico/apple-touch-icon.png?[[.Version]]"><link rel="icon" type="image/png" sizes="32x32" href="ico/favicon-32x32.png?[[.Version]]"><link rel="icon" type="image/png" sizes="16x16" href="ico/favicon-16x16.png?[[.Version]]"><link rel="manifest" href="ico/site.webmanifest"><link rel="mask-icon" href="ico/safari-pinned-tab.svg?[[.Version]]" color="#18191a"><link rel="shortcut icon" href="ico/favicon.ico?[[.Version]]"><meta name="apple-mobile-web-app-title" content="evcc"><meta name="application-name" content="evcc"><meta name="msapplication-TileColor" content="#18191a"><meta name="msapplication-config" content="ico/browserconfig.xml"><meta name="theme-color" content="#18191a"><title>evcc</title><link href="css/chunk-vendors.4692b1e2.css" rel="preload" as="style"><link href="css/index.55163eb0.css" rel="preload" as="style"><link href="js/chunk-vendors.323cc329.js" rel="preload" as="script"><link href="js/index.b87bdfa4.js" rel="preload" as="script"><link href="css/chunk-vendors.4692b1e2.css" rel="stylesheet"><link href="css/index.55163eb0.css" rel="stylesheet"></head><body><script>window.evcc = {
<!DOCTYPE html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="EV Charge Controller"><meta name="author" content="andig"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><link rel="apple-touch-icon" sizes="180x180" href="ico/apple-touch-icon.png?[[.Version]]"><link rel="icon" type="image/png" sizes="32x32" href="ico/favicon-32x32.png?[[.Version]]"><link rel="icon" type="image/png" sizes="16x16" href="ico/favicon-16x16.png?[[.Version]]"><link rel="manifest" href="ico/site.webmanifest"><link rel="mask-icon" href="ico/safari-pinned-tab.svg?[[.Version]]" color="#18191a"><link rel="shortcut icon" href="ico/favicon.ico?[[.Version]]"><meta name="apple-mobile-web-app-title" content="evcc"><meta name="application-name" content="evcc"><meta name="msapplication-TileColor" content="#18191a"><meta name="msapplication-config" content="ico/browserconfig.xml"><meta name="theme-color" content="#18191a"><title>evcc</title><link href="css/chunk-vendors.4692b1e2.css" rel="preload" as="style"><link href="css/index.6aeecebf.css" rel="preload" as="style"><link href="js/chunk-vendors.323cc329.js" rel="preload" as="script"><link href="js/index.e5e2e523.js" rel="preload" as="script"><link href="css/chunk-vendors.4692b1e2.css" rel="stylesheet"><link href="css/index.6aeecebf.css" rel="stylesheet"></head><body><script>window.evcc = {
version: "[[.Version]]",
configured: "[[.Configured]]",
commit: "[[.Commit]]",
};</script><div id="app"></div><script src="js/chunk-vendors.323cc329.js"></script><script src="js/index.b87bdfa4.js"></script></body></html>
};</script><div id="app"></div><script src="js/chunk-vendors.323cc329.js"></script><script src="js/index.e5e2e523.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dist/js/index.e5e2e523.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/js/index.e5e2e523.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -66,9 +66,7 @@ func NewHTTPd(url string, site site.API, hub *SocketHub, cache *util.Cache) *HTT
api.Use(jsonHandler)
api.Use(handlers.CompressHandler)
api.Use(handlers.CORS(
handlers.AllowedHeaders([]string{
"Content-Type",
}),
handlers.AllowedHeaders([]string{"Content-Type"}),
))
// site api

64
util/providerauth.go Normal file
View file

@ -0,0 +1,64 @@
package util
import "sync"
type AuthCollection struct {
mu sync.Mutex
paramC chan<- Param
vehicles map[string]*AuthProvider
}
func NewAuthCollection(paramC chan<- Param) *AuthCollection {
return &AuthCollection{
paramC: paramC,
vehicles: make(map[string]*AuthProvider),
}
}
func (ac *AuthCollection) Register(title, baseURI string) *AuthProvider {
ap := &AuthProvider{
ac: ac,
Uri: baseURI,
}
ac.mu.Lock()
ac.vehicles[title] = ap
ac.mu.Unlock()
return ap
}
// publish routes and status
func (ac *AuthCollection) Publish() {
ac.mu.Lock()
defer ac.mu.Unlock()
val := struct {
Vehicles map[string]*AuthProvider `json:"vehicles"`
}{
Vehicles: ac.vehicles,
}
ac.paramC <- Param{Key: "auth", Val: val}
}
type AuthProvider struct {
ac *AuthCollection
Uri string `json:"uri"`
Authenticated bool `json:"authenticated"`
}
func (ap *AuthProvider) Handler() chan<- bool {
c := make(chan bool)
go func() {
for auth := range c {
ap.ac.mu.Lock()
ap.Authenticated = auth
ap.ac.mu.Unlock()
ap.ac.Publish()
}
}()
return c
}

View file

@ -42,7 +42,7 @@ func NewMercedesFromConfig(other map[string]interface{}) (api.Vehicle, error) {
var options []mercedes.IdentityOptions
// TODO: Load tokens from a persistence storage and use those during startup
// TODO Load tokens from a persistence storage and use those during startup
// e.g. persistence.Load("key")
// if tokens != nil {
// options = append(options, mercedes.WithToken(&oauth2.Token{
@ -54,15 +54,13 @@ func NewMercedesFromConfig(other map[string]interface{}) (api.Vehicle, error) {
log := util.NewLogger("mercedes")
updateC := make(chan struct{})
// TODO: session secret from config/persistence
identity, err := mercedes.NewIdentity(log, cc.ClientID, cc.ClientSecret, updateC, options...)
// TODO session secret from config/persistence
identity, err := mercedes.NewIdentity(log, cc.ClientID, cc.ClientSecret, options...)
if err != nil {
return nil, err
}
api := mercedes.NewAPI(log, identity, updateC)
api := mercedes.NewAPI(log, identity)
v := &Mercedes{
embed: &cc.embed,

View file

@ -1,12 +1,8 @@
package mercedes
import (
"context"
"fmt"
"sync"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/provider"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"golang.org/x/oauth2"
@ -20,57 +16,28 @@ const BaseURI = "https://api.mercedes-benz.com/vehicledata/v2"
// API is the Mercedes api client
type API struct {
*request.Helper
api.ProviderLogin
updatedC chan struct{}
mu sync.Mutex
}
// NewAPI creates a new api client
func NewAPI(log *util.Logger, identity *Identity, updatedC chan struct{}) *API {
func NewAPI(log *util.Logger, identity *Identity) *API {
v := &API{
Helper: request.NewHelper(log),
ProviderLogin: identity,
updatedC: updatedC,
Helper: request.NewHelper(log),
}
// authenticated http client with logging injected to the Mercedes client
go func() {
for range v.updatedC {
log.TRACE.Println("update api client")
v.mu.Lock()
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, v.Client)
v.Client = identity.AuthConfig.Client(ctx, identity.Token())
v.mu.Unlock()
// TODO: hacky resetting all caches.
provider.ResetCached()
}
}()
// replace client transport with authenticated transport
v.Client.Transport = &oauth2.Transport{
Source: identity,
Base: v.Client.Transport,
}
return v
}
func (v *API) Update() chan struct{} {
return v.updatedC
}
// SoC implements the /soc response
func (v *API) SoC(vin string) (EVResponse, error) {
if !v.LoggedIn() {
return EVResponse{}, fmt.Errorf("invalid provider login")
}
var res EVResponse
uri := fmt.Sprintf("%s/vehicles/%s/resources/soc", BaseURI, vin)
v.mu.Lock()
defer v.mu.Unlock()
err := v.GetJSON(uri, &res)
return res, err
@ -78,16 +45,9 @@ func (v *API) SoC(vin string) (EVResponse, error) {
// Range implements the /rangeelectric response
func (v *API) Range(vin string) (EVResponse, error) {
if !v.LoggedIn() {
return EVResponse{}, fmt.Errorf("invalid provider login")
}
var res EVResponse
uri := fmt.Sprintf("%s/vehicles/%s/resources/rangeelectric", BaseURI, vin)
v.mu.Lock()
defer v.mu.Unlock()
err := v.GetJSON(uri, &res)
return res, err

View file

@ -19,37 +19,36 @@ type IdentityOptions func(c *Identity) error
// WithToken provides an oauth2.Token to the client for auth.
func WithToken(t *oauth2.Token) IdentityOptions {
return func(c *Identity) error {
c.token = t
return func(v *Identity) error {
v.ReuseTokenSource.Apply(t)
return nil
}
}
type Identity struct {
log *util.Logger
*ReuseTokenSource
sessionSecret []byte
AuthConfig *oauth2.Config
token *oauth2.Token
loginUpdateC chan struct{}
basePath string
authC chan<- bool
oc *oauth2.Config
}
// TODO: SessionSecret from config/persistence
func NewIdentity(log *util.Logger, id, secret string, loginUpdateC chan struct{}, options ...IdentityOptions) (*Identity, error) {
var err error
func generateSecret() ([]byte, error) {
var b [16]byte
_, err := io.ReadFull(rand.Reader, b[:])
return b[:], err
}
// TODO SessionSecret from config/persistence
func NewIdentity(log *util.Logger, id, secret string, options ...IdentityOptions) (*Identity, error) {
provider, err := oidc.NewProvider(context.Background(), "https://id.mercedes-benz.com")
if err != nil {
return nil, fmt.Errorf("failed to initialize OIDC provider: %s", err)
}
v := &Identity{
log: log,
loginUpdateC: loginUpdateC,
sessionSecret: genSessionSecret(),
AuthConfig: &oauth2.Config{
log: log,
oc: &oauth2.Config{
ClientID: id,
ClientSecret: secret,
Endpoint: provider.Endpoint(),
@ -57,6 +56,9 @@ func NewIdentity(log *util.Logger, id, secret string, loginUpdateC chan struct{}
},
}
v.ReuseTokenSource = &ReuseTokenSource{cb: v.invalidToken}
v.sessionSecret, err = generateSecret()
for _, o := range options {
if err == nil {
err = o(v)
@ -66,33 +68,18 @@ func NewIdentity(log *util.Logger, id, secret string, loginUpdateC chan struct{}
return v, err
}
func genSessionSecret() []byte {
var b [16]byte
if _, err := io.ReadFull(rand.Reader, b[:]); err != nil {
panic(err)
// invalidToken is the callback for the token source when token expires
func (v *Identity) invalidToken() {
if v.authC != nil {
v.authC <- false
}
return b[:]
}
func (v *Identity) Token() *oauth2.Token {
return v.token
}
var _ api.ProviderLogin = (*Identity)(nil)
func (v *Identity) SetBasePath(basepath string) {
v.basePath = basepath
}
func (v *Identity) Callback() api.Callback {
return api.Callback{
Path: fmt.Sprintf("%s/callback", v.basePath),
Handler: v.redirectHandler(),
}
}
func (v *Identity) SetOAuthCallbackURI(uri string) {
v.AuthConfig.RedirectURL = uri
func (v *Identity) SetCallbackParams(uri string, authC chan<- bool) {
v.oc.RedirectURL = uri
v.authC = authC
}
func (v *Identity) LoginHandler() http.HandlerFunc {
@ -102,7 +89,7 @@ func (v *Identity) LoginHandler() http.HandlerFunc {
b, _ := json.Marshal(struct {
LoginUri string `json:"loginUri"`
}{
LoginUri: v.AuthConfig.AuthCodeURL(state.Encrypt(), oauth2.AccessTypeOffline,
LoginUri: v.oc.AuthCodeURL(state.Encrypt(), oauth2.AccessTypeOffline,
oauth2.SetAuthURLParam("prompt", "login consent"),
),
})
@ -114,72 +101,56 @@ func (v *Identity) LoginHandler() http.HandlerFunc {
func (v *Identity) LogoutHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
v.token = nil
v.ReuseTokenSource.Apply(nil)
v.authC <- false
w.WriteHeader(http.StatusOK)
_, _ = w.Write(nil)
}
}
// LoggedIn implements the api.ProviderLogin interface
func (v *Identity) LoggedIn() bool {
return v.token.Valid()
}
func (v *Identity) CallbackHandler(baseURI string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
v.log.TRACE.Println("callback request retrieved")
// LoginPath implements the api.ProviderLogin interface
func (v *Identity) LoginPath() string {
return fmt.Sprintf("%s/login", v.basePath)
}
// LogoutPath implements the api.ProviderLogin interface
func (v *Identity) LogoutPath() string {
return fmt.Sprintf("%s/logout", v.basePath)
}
func (v *Identity) redirectHandler() api.RedirectHandlerFunc {
return func(redirectURI string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
v.log.TRACE.Println("callback request retrieved")
data, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
fmt.Fprintln(w, "invalid response:", data)
return
}
if error, ok := data["error"]; ok {
fmt.Fprintf(w, "error: %s: %s\n", error, data["error_description"])
return
}
states, ok := data["state"]
if !ok || len(states) != 1 {
fmt.Fprintln(w, "invalid state response:", data)
return
} else if err := Validate(states[0], v.sessionSecret); err != nil {
fmt.Fprintf(w, "failed state validation: %s", err)
return
}
codes, ok := data["code"]
if !ok || len(codes) != 1 {
fmt.Fprintln(w, "invalid response:", data)
return
}
token, err := v.AuthConfig.Exchange(context.Background(), codes[0])
if err != nil {
fmt.Fprintln(w, "token error:", err)
return
}
if token.Valid() {
v.token = token
v.log.TRACE.Println("sending login update...")
v.loginUpdateC <- struct{}{}
}
http.Redirect(w, r, redirectURI, http.StatusFound)
data, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
fmt.Fprintln(w, "invalid response:", data)
return
}
if error, ok := data["error"]; ok {
fmt.Fprintf(w, "error: %s: %s\n", error, data["error_description"])
return
}
states, ok := data["state"]
if !ok || len(states) != 1 {
fmt.Fprintln(w, "invalid state response:", data)
return
} else if err := Validate(states[0], v.sessionSecret); err != nil {
fmt.Fprintf(w, "failed state validation: %s", err)
return
}
codes, ok := data["code"]
if !ok || len(codes) != 1 {
fmt.Fprintln(w, "invalid response:", data)
return
}
token, err := v.oc.Exchange(context.Background(), codes[0])
if err != nil {
fmt.Fprintln(w, "token error:", err)
return
}
if token.Valid() {
v.log.TRACE.Println("sending login update...")
v.ReuseTokenSource.Apply(token)
v.authC <- true
}
http.Redirect(w, r, baseURI, http.StatusFound)
}
}

View file

@ -20,7 +20,7 @@ type State struct {
Time time.Time
}
// TODO: Move to another more general place in the repo
// TODO Move to another more general place in the repo
func NewState(key []byte) State {
return State{
key: key,

View file

@ -0,0 +1,35 @@
package mercedes
import (
"fmt"
"sync"
"golang.org/x/oauth2"
)
var ErrNotLoggedIn = fmt.Errorf("not logged in")
type ReuseTokenSource struct {
mu sync.Mutex
t *oauth2.Token
cb func()
}
func (ts *ReuseTokenSource) Token() (*oauth2.Token, error) {
ts.mu.Lock()
defer ts.mu.Unlock()
if ts.t.Valid() {
return ts.t, nil
}
ts.cb() // invalid token callback
return nil, ErrNotLoggedIn
}
func (ts *ReuseTokenSource) Apply(t *oauth2.Token) {
ts.mu.Lock()
ts.t = t
ts.mu.Unlock()
}