Mercedes: generalize provider login (#2384)
This commit is contained in:
parent
b031610594
commit
b4a004b7bb
19 changed files with 246 additions and 236 deletions
21
api/api.go
21
api/api.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
4
dist/index.html
vendored
|
|
@ -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>
|
||||
2
dist/js/index.b87bdfa4.js
vendored
2
dist/js/index.b87bdfa4.js
vendored
File diff suppressed because one or more lines are too long
1
dist/js/index.b87bdfa4.js.map
vendored
1
dist/js/index.b87bdfa4.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/js/index.e5e2e523.js
vendored
Normal file
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
1
dist/js/index.e5e2e523.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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
64
util/providerauth.go
Normal 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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
35
vehicle/mercedes/tokensource.go
Normal file
35
vehicle/mercedes/tokensource.go
Normal 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()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue