package httpapi import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "io" "net/http" "strconv" "strings" "time" ) const internalSignatureWindow = 5 * time.Minute func (a *API) withInternalAdminAuth(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(io.LimitReader(r.Body, 10<<20)) if err != nil { writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", nil) return } _ = r.Body.Close() r.Body = io.NopCloser(bytes.NewReader(body)) if err := a.verifyInternalAdminSignature(r, body); err != nil { writeError(w, r, http.StatusForbidden, 30041, reasonForbidden, "forbidden", nil) return } next(w, r) } } func (a *API) verifyInternalAdminSignature(r *http.Request, body []byte) error { secret := strings.TrimSpace(a.authCfg.InternalSecret) if secret == "" { return ErrForbidden } serviceName := strings.TrimSpace(r.Header.Get("X-Service-Name")) if expected := strings.TrimSpace(a.authCfg.InternalServiceName); expected != "" && serviceName != expected { return ErrForbidden } timestampRaw := strings.TrimSpace(r.Header.Get("X-Timestamp")) nonce := strings.TrimSpace(r.Header.Get("X-Nonce")) signature := strings.TrimSpace(r.Header.Get("X-Signature")) if timestampRaw == "" || nonce == "" || signature == "" { return ErrForbidden } ts, err := strconv.ParseInt(timestampRaw, 10, 64) if err != nil { return ErrForbidden } when := time.Unix(ts, 0) if time.Since(when) > internalSignatureWindow || time.Until(when) > internalSignatureWindow { return ErrForbidden } bodyHash := sha256.Sum256(body) signingText := strings.Join([]string{ r.Method, r.URL.RequestURI(), hex.EncodeToString(bodyHash[:]), timestampRaw, nonce, }, "\n") mac := hmac.New(sha256.New, []byte(secret)) _, _ = mac.Write([]byte(signingText)) expected := hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(expected), []byte(signature)) { return ErrForbidden } return nil }