Provider auth: require authenticated session for OAuth login/logout (#33115)
Co-authored-by: Michael Geers <michael@geers.tv>
This commit is contained in:
parent
226425e22e
commit
4adbf141d6
7 changed files with 65 additions and 17 deletions
|
|
@ -481,10 +481,11 @@ func runRoot(cmd *cobra.Command, args []string) {
|
||||||
once.Do(func() { close(stopC) }) // signal loop to end
|
once.Do(func() { close(stopC) }) // signal loop to end
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// allow web access for vehicles
|
|
||||||
configureAuth(httpd.Router(), valueChan)
|
|
||||||
|
|
||||||
authObject := auth.New()
|
authObject := auth.New()
|
||||||
|
|
||||||
|
// allow web access for vehicles
|
||||||
|
configureAuth(httpd.Router(), server.EnsureAuthHandler(authObject), valueChan)
|
||||||
|
|
||||||
if ok, _ := cmd.Flags().GetBool(flagDisableAuth); ok {
|
if ok, _ := cmd.Flags().GetBool(flagDisableAuth); ok {
|
||||||
log.WARN.Println("❗❗❗ Authentication is disabled. This is dangerous. Your data and credentials are not protected.")
|
log.WARN.Println("❗❗❗ Authentication is disabled. This is dangerous. Your data and credentials are not protected.")
|
||||||
authObject.SetAuthMode(auth.Disabled)
|
authObject.SetAuthMode(auth.Disabled)
|
||||||
|
|
|
||||||
|
|
@ -1544,7 +1544,7 @@ func configureLoadpoints(conf globalconfig.All) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// configureAuth handles routing for devices. For now only api.AuthProvider related routes
|
// configureAuth handles routing for devices. For now only api.AuthProvider related routes
|
||||||
func configureAuth(router *mux.Router, paramC chan<- util.Param) {
|
func configureAuth(router *mux.Router, authMiddleware mux.MiddlewareFunc, paramC chan<- util.Param) {
|
||||||
auth := router.PathPrefix("/providerauth").Subrouter()
|
auth := router.PathPrefix("/providerauth").Subrouter()
|
||||||
auth.Use(handlers.CompressHandler)
|
auth.Use(handlers.CompressHandler)
|
||||||
auth.Use(handlers.CORS(
|
auth.Use(handlers.CORS(
|
||||||
|
|
@ -1554,8 +1554,8 @@ func configureAuth(router *mux.Router, paramC chan<- util.Param) {
|
||||||
// backwards-compatible revert of https://github.com/evcc-io/evcc/pull/21266
|
// backwards-compatible revert of https://github.com/evcc-io/evcc/pull/21266
|
||||||
router.PathPrefix("/oauth").Handler(auth)
|
router.PathPrefix("/oauth").Handler(auth)
|
||||||
|
|
||||||
// wire the handler
|
// wire the handler; login/logout require an authenticated session
|
||||||
providerauth.Setup(auth, paramC)
|
providerauth.Setup(auth, paramC, authMiddleware)
|
||||||
}
|
}
|
||||||
|
|
||||||
// isExperimental returns if experimental features are enabled
|
// isExperimental returns if experimental features are enabled
|
||||||
|
|
|
||||||
|
|
@ -320,14 +320,14 @@ func (s *HTTPd) RegisterSystemHandler(site *core.Site, pub publisher, cache *uti
|
||||||
}
|
}
|
||||||
|
|
||||||
// API key endpoints require an authenticated session.
|
// API key endpoints require an authenticated session.
|
||||||
ensureAuth := ensureAuthHandler(auth)
|
ensureAuth := EnsureAuthHandler(auth)
|
||||||
api.Methods("GET").Path("/apikey").Handler(ensureAuth(apiKeyStatusHandler(auth)))
|
api.Methods("GET").Path("/apikey").Handler(ensureAuth(apiKeyStatusHandler(auth)))
|
||||||
api.Methods("POST").Path("/apikey").Handler(ensureAuth(regenerateApiKeyHandler(auth)))
|
api.Methods("POST").Path("/apikey").Handler(ensureAuth(regenerateApiKeyHandler(auth)))
|
||||||
}
|
}
|
||||||
|
|
||||||
{ // api/config
|
{ // api/config
|
||||||
api := api.PathPrefix("/config").Subrouter()
|
api := api.PathPrefix("/config").Subrouter()
|
||||||
api.Use(ensureAuthHandler(auth))
|
api.Use(EnsureAuthHandler(auth))
|
||||||
|
|
||||||
routes := map[string]route{
|
routes := map[string]route{
|
||||||
"auth": {"POST", "/auth", authHandler},
|
"auth": {"POST", "/auth", authHandler},
|
||||||
|
|
@ -423,7 +423,7 @@ func (s *HTTPd) RegisterSystemHandler(site *core.Site, pub publisher, cache *uti
|
||||||
|
|
||||||
{ // api/system
|
{ // api/system
|
||||||
api := api.PathPrefix("/system").Subrouter()
|
api := api.PathPrefix("/system").Subrouter()
|
||||||
api.Use(ensureAuthHandler(auth))
|
api.Use(EnsureAuthHandler(auth))
|
||||||
|
|
||||||
routes := map[string]route{
|
routes := map[string]route{
|
||||||
"log": {"GET", "/log", logHandler},
|
"log": {"GET", "/log", logHandler},
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,8 @@ func logoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureAuthHandler(authObject auth.Auth) mux.MiddlewareFunc {
|
// EnsureAuthHandler returns middleware that rejects unauthenticated requests
|
||||||
|
func EnsureAuthHandler(authObject auth.Auth) mux.MiddlewareFunc {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if next == nil {
|
if next == nil {
|
||||||
|
|
|
||||||
|
|
@ -75,3 +75,15 @@ func TestRequireCriticalConfig(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnsureAuthHandler(t *testing.T) {
|
||||||
|
get := func(a auth.Auth) int {
|
||||||
|
next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
EnsureAuthHandler(a)(next).ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/login", nil))
|
||||||
|
return rec.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, get(fakeAuth{mode: auth.Enabled}), "enabled must reject without credentials")
|
||||||
|
assert.Equal(t, http.StatusOK, get(fakeAuth{mode: auth.Disabled}), "disabled must pass through")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,14 +32,14 @@ func init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup connects the redirect handler to the router and registers the callback channel
|
// Setup connects the redirect handler to the router and registers the callback channel.
|
||||||
func Setup(router *mux.Router, paramC chan<- util.Param) {
|
// Callback stays open: the cross-site IdP redirect carries no session cookie, the state token gates it.
|
||||||
// callback?code=...&state=...
|
func Setup(router *mux.Router, paramC chan<- util.Param, authMiddleware mux.MiddlewareFunc) {
|
||||||
|
gate := func(h http.HandlerFunc) http.Handler { return authMiddleware(h) }
|
||||||
|
|
||||||
router.Methods(http.MethodGet).Path("/callback").HandlerFunc(instance.handleCallback)
|
router.Methods(http.MethodGet).Path("/callback").HandlerFunc(instance.handleCallback)
|
||||||
// login?id=...
|
router.Methods(http.MethodGet).Path("/login").Handler(gate(instance.handleLogin))
|
||||||
router.Methods(http.MethodGet).Path("/login").HandlerFunc(instance.handleLogin)
|
router.Methods(http.MethodGet).Path("/logout").Handler(gate(instance.handleLogout))
|
||||||
// logout?id=...
|
|
||||||
router.Methods(http.MethodGet).Path("/logout").HandlerFunc(instance.handleLogout)
|
|
||||||
|
|
||||||
go instance.run(paramC)
|
go instance.run(paramC)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
34
server/providerauth/providerauth_test.go
Normal file
34
server/providerauth/providerauth_test.go
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
package providerauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/util"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// stand-in auth middleware rejecting every request
|
||||||
|
func blockAll(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// login/logout are gated, callback stays open (302 to error page, not middleware 401)
|
||||||
|
func TestSetupGating(t *testing.T) {
|
||||||
|
router := mux.NewRouter()
|
||||||
|
Setup(router, make(chan util.Param, 1), blockAll)
|
||||||
|
|
||||||
|
get := func(path string) int {
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
|
||||||
|
return rec.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, get("/login?id=x"), "login must be gated")
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, get("/logout?id=x"), "logout must be gated")
|
||||||
|
assert.Equal(t, http.StatusFound, get("/callback"), "callback must stay open (gated by state token, not session)")
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue