Fully support auth in Web UI; persist users in localStorage (for now); add ugly ?auth=... param

This commit is contained in:
Philipp Heckel 2022-02-25 23:25:04 -05:00
parent 6d343c0f1a
commit 530f55c234
16 changed files with 237 additions and 72 deletions

View file

@ -862,7 +862,7 @@ func parseSince(r *http.Request, poll bool) (sinceTime, error) {
func (s *Server) handleOptions(w http.ResponseWriter, _ *http.Request) error {
w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST")
w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests
w.Header().Set("Access-Control-Allow-Headers", "Authorization") // CORS, allow auth
w.Header().Set("Access-Control-Allow-Headers", "Authorization") // CORS, allow auth via JS
return nil
}
@ -1091,7 +1091,7 @@ func (s *Server) withAuth(next handleFunc, perm auth.Permission) handleFunc {
return err
}
var user *auth.User // may stay nil if no auth header!
username, password, ok := r.BasicAuth()
username, password, ok := extractUserPass(r)
if ok {
if user, err = s.auth.Authenticate(username, password); err != nil {
log.Printf("authentication failed: %s", err.Error())
@ -1108,6 +1108,27 @@ func (s *Server) withAuth(next handleFunc, perm auth.Permission) handleFunc {
}
}
// extractUserPass reads the username/password from the basic auth header (Authorization: Basic ...),
// or from the ?auth=... query param. The latter is required only to support the WebSocket JavaScript
// class, which does not support passing headers during the initial request. The auth query param
// is effectively double base64 encoded. Its format is base64(Basic base64(user:pass)).
func extractUserPass(r *http.Request) (username string, password string, ok bool) {
username, password, ok = r.BasicAuth()
if ok {
return
}
authParam := readQueryParam(r, "authorization", "auth")
if authParam != "" {
a, err := base64.RawURLEncoding.DecodeString(authParam)
if err != nil {
return
}
r.Header.Set("Authorization", string(a))
return r.BasicAuth()
}
return
}
// visitor creates or retrieves a rate.Limiter for the given visitor.
// This function was taken from https://www.alexedwards.net/blog/how-to-rate-limit-http-requests (MIT).
func (s *Server) visitor(r *http.Request) *visitor {

View file

@ -657,6 +657,25 @@ func TestServer_Auth_Fail_CannotPublish(t *testing.T) {
require.Equal(t, 403, response.Code) // Anonymous read not allowed
}
func TestServer_Auth_ViaQuery(t *testing.T) {
c := newTestConfig(t)
c.AuthFile = filepath.Join(t.TempDir(), "user.db")
c.AuthDefaultRead = false
c.AuthDefaultWrite = false
s := newTestServer(t, c)
manager := s.auth.(auth.Manager)
require.Nil(t, manager.AddUser("ben", "some pass", auth.RoleAdmin))
u := fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(basicAuth("ben:some pass"))))
response := request(t, s, "GET", u, "", nil)
require.Equal(t, 200, response.Code)
u = fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(basicAuth("ben:WRONNNGGGG"))))
response = request(t, s, "GET", u, "", nil)
require.Equal(t, 401, response.Code)
}
/*
func TestServer_Curl_Publish_Poll(t *testing.T) {
s, port := test.StartServer(t)

View file

@ -14,12 +14,24 @@ func readBoolParam(r *http.Request, defaultValue bool, names ...string) bool {
}
func readParam(r *http.Request, names ...string) string {
value := readHeaderParam(r, names...)
if value != "" {
return value
}
return readQueryParam(r, names...)
}
func readHeaderParam(r *http.Request, names ...string) string {
for _, name := range names {
value := r.Header.Get(name)
if value != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func readQueryParam(r *http.Request, names ...string) string {
for _, name := range names {
value := r.URL.Query().Get(strings.ToLower(name))
if value != "" {