126 lines
3.7 KiB
Go
126 lines
3.7 KiB
Go
package server
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"log/slog"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// credCache memoises successful Basic-auth validations for a short TTL. Browsers
|
|
// resend the Authorization header on every request, so without this each one
|
|
// would trigger a full PAM round-trip (slow, and a brute-force/lockout risk).
|
|
// Only positive results are cached, keyed by a salted SHA-256 of user+password
|
|
// so the cache never holds a recoverable secret. A fresh random salt per process
|
|
// keeps keys from being precomputable.
|
|
type credCache struct {
|
|
mu sync.Mutex
|
|
ttl time.Duration
|
|
salt []byte
|
|
entries map[string]time.Time
|
|
}
|
|
|
|
func newCredCache(ttl time.Duration) *credCache {
|
|
salt := make([]byte, 16)
|
|
_, _ = rand.Read(salt)
|
|
return &credCache{ttl: ttl, salt: salt, entries: map[string]time.Time{}}
|
|
}
|
|
|
|
func (c *credCache) key(user, pass string) string {
|
|
h := sha256.New()
|
|
h.Write(c.salt)
|
|
h.Write([]byte(user))
|
|
h.Write([]byte{0})
|
|
h.Write([]byte(pass))
|
|
return hex.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
// valid reports whether user/pass was validated within the TTL.
|
|
func (c *credCache) valid(user, pass string) bool {
|
|
if c.ttl <= 0 {
|
|
return false
|
|
}
|
|
k := c.key(user, pass)
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
exp, ok := c.entries[k]
|
|
if !ok {
|
|
return false
|
|
}
|
|
if time.Now().After(exp) {
|
|
delete(c.entries, k)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// store records a successful validation, opportunistically pruning expired keys.
|
|
func (c *credCache) store(user, pass string) {
|
|
if c.ttl <= 0 {
|
|
return
|
|
}
|
|
k := c.key(user, pass)
|
|
now := time.Now()
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.entries[k] = now.Add(c.ttl)
|
|
if len(c.entries) > 1024 {
|
|
for kk, exp := range c.entries {
|
|
if now.After(exp) {
|
|
delete(c.entries, kk)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// basicAuth wraps next with HTTP Basic authentication validated by authFn (PAM,
|
|
// see internal/pamauth). It mirrors kerberosAuth: a successful login writes the
|
|
// username into userHeader for the existing access pipeline (accessMiddleware /
|
|
// wsHandler), and any inbound value of userHeader is always discarded first so a
|
|
// client cannot spoof an identity.
|
|
//
|
|
// challenge controls the no/invalid-credentials case:
|
|
// - challenge=true (REST/page API): reply 401 + WWW-Authenticate: Basic so the
|
|
// browser prompts for credentials.
|
|
// - challenge=false (WebSocket upgrade): fall through unauthenticated; the
|
|
// session resolves to default_user. Browsers cannot show a login dialog for a
|
|
// WebSocket, but once they have cached credentials from the page's API calls
|
|
// they resend them on the upgrade, so the validated path is still taken.
|
|
func basicAuth(authFn func(user, pass string) error, userHeader, realm string, challenge bool, cache *credCache, log *slog.Logger, next http.Handler) http.Handler {
|
|
if realm == "" {
|
|
realm = "uopi"
|
|
}
|
|
challengeValue := `Basic realm="` + realm + `", charset="UTF-8"`
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Never trust a client-supplied identity header; only a validated login sets it.
|
|
r.Header.Del(userHeader)
|
|
|
|
if user, pass, ok := r.BasicAuth(); ok && user != "" {
|
|
if cache.valid(user, pass) {
|
|
r.Header.Set(userHeader, user)
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
if err := authFn(user, pass); err == nil {
|
|
cache.store(user, pass)
|
|
r.Header.Set(userHeader, user)
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
} else {
|
|
log.Warn("basic auth failed", "user", user, "err", err)
|
|
}
|
|
}
|
|
|
|
// No or invalid credentials.
|
|
if challenge {
|
|
w.Header().Set("WWW-Authenticate", challengeValue)
|
|
http.Error(w, "authentication required", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r) // best-effort (WebSocket): downstream → default_user
|
|
})
|
|
}
|