From f9a2efaffbe0fb5700a150fcafb6408f6a17b6b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferdinand=20M=C3=BCtsch?= Date: Tue, 21 May 2019 22:40:59 +0200 Subject: [PATCH] Support for username-password authentication. --- middlewares/authenticate.go | 75 +++++++++++++++++++++++++++++++------ models/user.go | 5 ++- static/index.html | 14 ++++--- 3 files changed, 76 insertions(+), 18 deletions(-) diff --git a/middlewares/authenticate.go b/middlewares/authenticate.go index a6e8a63..5297cfb 100644 --- a/middlewares/authenticate.go +++ b/middlewares/authenticate.go @@ -2,8 +2,12 @@ package middlewares import ( "context" + "crypto/md5" "encoding/base64" + "encoding/hex" + "errors" "net/http" + "regexp" "strings" "time" @@ -31,16 +35,34 @@ func (m *AuthenticateMiddleware) Handle(w http.ResponseWriter, r *http.Request, m.Init() } - authHeader := strings.Split(r.Header.Get("Authorization"), " ") - if len(authHeader) != 2 { + var user *models.User + var userKey string + user, userKey, err := m.tryGetUserByApiKey(r) + + if err != nil { + user, userKey, err = m.tryGetUserByPassword(r) + } + + if err != nil { w.WriteHeader(http.StatusUnauthorized) return } + m.Cache.Set(userKey, user, cache.DefaultExpiration) + + ctx := context.WithValue(r.Context(), models.UserKey, user) + next(w, r.WithContext(ctx)) +} + +func (m *AuthenticateMiddleware) tryGetUserByApiKey(r *http.Request) (*models.User, string, error) { + authHeader := strings.Split(r.Header.Get("Authorization"), " ") + if len(authHeader) != 2 || authHeader[0] != "Basic" { + return nil, "", errors.New("Failed to extract API key") + } + key, err := base64.StdEncoding.DecodeString(authHeader[1]) if err != nil { - w.WriteHeader(http.StatusUnauthorized) - return + return nil, "", err } var user *models.User @@ -49,15 +71,46 @@ func (m *AuthenticateMiddleware) Handle(w http.ResponseWriter, r *http.Request, if !ok { user, err = m.UserSrvc.GetUserByKey(userKey) if err != nil { - w.WriteHeader(http.StatusUnauthorized) - return + return nil, "", err } } else { user = cachedUser.(*models.User) } - - m.Cache.Set(userKey, user, cache.DefaultExpiration) - - ctx := context.WithValue(r.Context(), models.UserKey, user) - next(w, r.WithContext(ctx)) + return user, userKey, nil +} + +func (m *AuthenticateMiddleware) tryGetUserByPassword(r *http.Request) (*models.User, string, error) { + authHeader := strings.Split(r.Header.Get("Authorization"), " ") + if len(authHeader) != 2 || authHeader[0] != "Basic" { + return nil, "", errors.New("Failed to extract API key") + } + + hash, err := base64.StdEncoding.DecodeString(authHeader[1]) + userKey := strings.TrimSpace(string(hash)) + if err != nil { + return nil, "", err + } + + var user *models.User + cachedUser, ok := m.Cache.Get(userKey) + if !ok { + re := regexp.MustCompile(`^(.+):(.+)$`) + groups := re.FindAllStringSubmatch(userKey, -1) + if len(groups) == 0 || len(groups[0]) != 3 { + return nil, "", errors.New("Failed to parse user agent string") + } + userId, password := groups[0][1], groups[0][2] + user, err = m.UserSrvc.GetUserById(userId) + if err != nil { + return nil, "", err + } + passwordHash := md5.Sum([]byte(password)) + passwordHashString := hex.EncodeToString(passwordHash[:]) + if passwordHashString != user.Password { + return nil, "", errors.New("Invalid password") + } + } else { + user = cachedUser.(*models.User) + } + return user, userKey, nil } diff --git a/models/user.go b/models/user.go index c336b5f..7cb5cdc 100644 --- a/models/user.go +++ b/models/user.go @@ -1,6 +1,7 @@ package models type User struct { - ID string `json:"id" gorm:"primary_key"` - ApiKey string `json:"api_key" gorm:"unique"` + ID string `json:"id" gorm:"primary_key"` + ApiKey string `json:"api_key" gorm:"unique"` + Password string `json:"-"` } diff --git a/static/index.html b/static/index.html index 8a37749..14059d7 100644 --- a/static/index.html +++ b/static/index.html @@ -43,7 +43,7 @@ grid-area: header } - .apikey-input { + .input { width: 300px; } @@ -52,8 +52,10 @@

Statistics

- - + + + + @@ -96,12 +98,14 @@ } function load(interval, live) { - let apiKey = document.getElementById('apikey-input').value + let user = document.getElementById('user-input').value + let password = document.getElementById('password-input').value + let hashed = btoa(`${user}:${password}`) fetch(`${window.location.href}/api/summary?interval=${interval}&live=${live}`, { method: 'GET', headers: { 'Accept': 'application/json', - 'Authorization': `Basic ${btoa(apiKey)}` + 'Authorization': `Basic ${hashed}` } }) .then((res) => {