evcc-io/server/providerauth/handler.go
2025-10-31 15:25:53 +00:00

186 lines
4.4 KiB
Go

package providerauth
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"sync"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/core/keys"
"github.com/evcc-io/evcc/util"
)
type errorResponse struct {
Error string `json:"error"`
}
type loginResponse struct {
LoginUri string `json:"loginUri"`
}
// jsonWrite writes a JSON response
func jsonWrite(w http.ResponseWriter, data any) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
}
// jsonError writes an error response
func jsonError(w http.ResponseWriter, status int, message string) {
w.WriteHeader(status)
jsonWrite(w, errorResponse{Error: message})
}
// Handler manages a dynamic map of routes for handling the redirect during
// OAuth authentication. When a route is registered a token OAuth state is returned.
// On GET request the generic handler identifies route and target handler
// by request state obtained from the request and delegates to the registered handler.
type Handler struct {
mu sync.Mutex
log *util.Logger
secret []byte
providers map[string]api.AuthProvider
states map[string]string
updateC chan string
}
// TODO get status from update channel
func (a *Handler) run(paramC chan<- util.Param) {
for range a.updateC {
a.mu.Lock()
res := make(map[string]*AuthProvider)
for id, provider := range a.providers {
res[provider.DisplayName()] = &AuthProvider{
ID: url.QueryEscape(id),
Authenticated: provider.Authenticated(),
}
}
a.mu.Unlock()
// publish the updated auth providers
paramC <- util.Param{Key: keys.AuthProviders, Val: res}
}
}
func (a *Handler) register(name string, handler api.AuthProvider) (chan<- string, error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.providers[name] != nil {
return nil, fmt.Errorf("provider already registered: %s", name)
}
a.providers[name] = handler
return a.updateC, nil
}
func (a *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
a.log.DEBUG.Printf("login request for: %s", id)
a.mu.Lock()
defer a.mu.Unlock()
provider, ok := a.providers[id]
if !ok {
jsonError(w, http.StatusBadRequest, "invalid id")
return
}
// Generate a new state and store the provider
state := NewState()
encryptedState := state.Encrypt(a.secret)
a.states[encryptedState] = id
// Schedule cleanup for stale state entries after state becomes invalid
time.AfterFunc(stateValidity, func() {
a.mu.Lock()
defer a.mu.Unlock()
delete(a.states, encryptedState)
})
uri, err := provider.Login(encryptedState)
if err != nil {
jsonError(w, http.StatusBadRequest, err.Error())
return
}
jsonWrite(w, loginResponse{LoginUri: uri})
}
func (a *Handler) handleLogout(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
a.log.DEBUG.Printf("logout request for: %s", id)
a.mu.Lock()
defer a.mu.Unlock()
provider, ok := a.providers[id]
if !ok {
jsonError(w, http.StatusBadRequest, "invalid id")
return
}
// Handle logout
if err := provider.Logout(); err != nil {
a.log.ERROR.Printf("logout for provider %s failed: %v", id, err)
jsonError(w, http.StatusInternalServerError, "logout failed")
return
}
jsonWrite(w, "OK")
}
func (a *Handler) handleCallback(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
if q.Has("error") {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "error: %s: %s\n", q.Get("error"), q.Get("error_description"))
return
}
encryptedState := q.Get("state")
state, err := DecryptState(encryptedState, a.secret)
if err != nil || !state.Valid() {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "invalid state")
return
}
a.mu.Lock()
defer a.mu.Unlock()
// Find the corresponding provider
id, ok := a.states[encryptedState]
if !ok {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "no provider found for state")
return
}
provider, ok := a.providers[id]
if !ok {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "internal provider state unexpected")
return
}
// Remove the state from the map
delete(a.states, encryptedState)
// Handle the callback
if err := provider.HandleCallback(r.URL.Query()); err != nil {
a.log.ERROR.Printf("callback for provider %s failed: %v", id, err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintln(w, "callback failed")
return
}
http.Redirect(w, r, "/", http.StatusFound)
}