platform/auth gains the LocalJWT (HS256 mint/verify, AUTH_TOKEN_SECRET, disabled when unset), the RFC 8628 DeviceFlow (start/approve/poll with requested-scope gating) and KeyStore.ReloadIfChanged (mtime-based, so a freshly minted key works across processes without a restart). One shared store at /commerce/apikeys.json (AUTH_APIKEYS_FILE) now backs the backoffice admin gate, the CMS API and every MCP endpoint; AUTH_TOKEN_SECRET lets the companion app's device flow mint bearer tokens the same surfaces accept. Submodule bumps: backoffice (AuthGate + device endpoints + mcp scope), go-cart-actor (mcpauth sources), go-graph-cms-v2 (shared store + token chain). Also lands the still-pending receipt-ocr sidecar (previous pass): the PP-OCR ONNX inference service + download-ocr-models.sh + k8s deployment/initContainer, wired into compose and the ledger RECEIPT_OCR_URL, with the extractor pairing fix riding the go-cart-actor bump. Deploy: AUTH_APIKEYS_FILE/AUTH_TOKEN_SECRET/AUTH_DEVICE_BASE_URL on backoffice + cart/profile/order/ledger in compose and k8s, /device ingress route, stack-secrets AUTH_TOKEN_SECRET optional ref, docs updated. Generated with Codebuff 🤖 Co-Authored-By: Codebuff <noreply@codebuff.com>
91 lines
2.4 KiB
Go
91 lines
2.4 KiB
Go
package auth
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestKeyStoreReloadIfChanged(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "apikeys.json")
|
|
// Writer process mints into the store.
|
|
writer, err := OpenKeyStore(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
key, rec, err := writer.Mint("integration", []string{"mcp"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Reader process opens the same file fresh.
|
|
reader, err := OpenKeyStore(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req := apiKeyReq(key)
|
|
if _, err := (APIKeyAuth{Keys: reader}).Authenticate(req); err != nil {
|
|
t.Fatalf("fresh key should validate: %v", err)
|
|
}
|
|
|
|
// Writer mints a second key AFTER the reader loaded → reader must reload.
|
|
time.Sleep(10 * time.Millisecond) // ensure mtime advances
|
|
key2, _, err := writer.Mint("companion", []string{"write"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := (APIKeyAuth{Keys: reader}).Authenticate(apiKeyReq(key2)); err == nil {
|
|
t.Fatal("key minted after load should NOT validate before reload")
|
|
}
|
|
if err := reader.ReloadIfChanged(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := (APIKeyAuth{Keys: reader}).Authenticate(apiKeyReq(key2)); err != nil {
|
|
t.Fatalf("key should validate after ReloadIfChanged: %v", err)
|
|
}
|
|
|
|
// Unchanged file → no-op (and keys survive).
|
|
if err := reader.ReloadIfChanged(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := (APIKeyAuth{Keys: reader}).Authenticate(apiKeyReq(key)); err != nil {
|
|
t.Fatalf("key should still validate: %v", err)
|
|
}
|
|
|
|
// Revoke on the writer side → reader sees it after reload.
|
|
if err := writer.Revoke(rec.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := reader.ReloadIfChanged(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := (APIKeyAuth{Keys: reader}).Authenticate(apiKeyReq(key)); err == nil {
|
|
t.Fatal("revoked key should not validate")
|
|
}
|
|
}
|
|
|
|
func apiKeyReq(key string) *http.Request {
|
|
r := httptest.NewRequest(http.MethodGet, "/api/nodes", nil)
|
|
r.Header.Set("X-API-Key", key)
|
|
return r
|
|
}
|
|
|
|
func TestKeyStoreReloadCorruptFile(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "apikeys.json")
|
|
ks, err := OpenKeyStore(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw := []byte(`{not json`)
|
|
if err := os.WriteFile(path, raw, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
if err := ks.ReloadIfChanged(); err == nil {
|
|
t.Fatal("corrupt file should error")
|
|
}
|
|
}
|