feat: enhance API key management and ownership recording in verifyHandler

- Implemented logic to ensure an API key is created or retrieved for each wallet during the verification process.
- Added best-effort recording of ownership for both API keys and wallets in the namespace ownership database.
- Improved error handling and logging for better traceability of ownership checks and API key operations.
- Cleaned up unnecessary comments and whitespace in the auth_handlers.go file for better code readability.
This commit is contained in:
anonpenguin 2025-10-29 06:53:51 +02:00
parent c208ff3288
commit ad088bd476
2 changed files with 68 additions and 9 deletions

View File

@ -192,7 +192,6 @@ func (g *Gateway) verifyHandler(w http.ResponseWriter, r *http.Request) {
switch chainType {
case "ETH":
// EVM personal_sign verification of the nonce
// Hash: keccak256("\x19Ethereum Signed Message:\n" + len(nonce) + nonce)
msg := []byte(req.Nonce)
prefix := []byte("\x19Ethereum Signed Message:\n" + strconv.Itoa(len(msg)))
hash := ethcrypto.Keccak256(prefix, msg)
@ -228,7 +227,7 @@ func (g *Gateway) verifyHandler(w http.ResponseWriter, r *http.Request) {
case "SOL":
// Solana uses Ed25519 signatures
// Signature is base64-encoded, public key is the wallet address (base58)
// Decode base64 signature (Solana signatures are 64 bytes)
sig, err := base64.StdEncoding.DecodeString(req.Signature)
if err != nil {
@ -241,7 +240,6 @@ func (g *Gateway) verifyHandler(w http.ResponseWriter, r *http.Request) {
}
// Decode base58 public key (Solana wallet address)
// Using a simple base58 decoder
pubKeyBytes, err := base58Decode(req.Wallet)
if err != nil {
writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid wallet address: %v", err))
@ -296,6 +294,45 @@ func (g *Gateway) verifyHandler(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
// Ensure API key exists for this (namespace, wallet) and record ownerships
// This is done automatically after successful verification; no second nonce needed
var apiKey string
// Try existing linkage
r1, err := db.Query(internalCtx,
"SELECT api_keys.key FROM wallet_api_keys JOIN api_keys ON wallet_api_keys.api_key_id = api_keys.id WHERE wallet_api_keys.namespace_id = ? AND LOWER(wallet_api_keys.wallet) = LOWER(?) LIMIT 1",
nsID, req.Wallet,
)
if err == nil && r1 != nil && r1.Count > 0 && len(r1.Rows) > 0 && len(r1.Rows[0]) > 0 {
if s, ok := r1.Rows[0][0].(string); ok {
apiKey = s
} else {
b, _ := json.Marshal(r1.Rows[0][0])
_ = json.Unmarshal(b, &apiKey)
}
}
if strings.TrimSpace(apiKey) == "" {
// Create new API key with format ak_<random>:<namespace>
buf := make([]byte, 18)
if _, err := rand.Read(buf); err == nil {
apiKey = "ak_" + base64.RawURLEncoding.EncodeToString(buf) + ":" + ns
if _, err := db.Query(internalCtx, "INSERT INTO api_keys(key, name, namespace_id) VALUES (?, ?, ?)", apiKey, "", nsID); err == nil {
// Link wallet -> api_key
rid, err := db.Query(internalCtx, "SELECT id FROM api_keys WHERE key = ? LIMIT 1", apiKey)
if err == nil && rid != nil && rid.Count > 0 && len(rid.Rows) > 0 && len(rid.Rows[0]) > 0 {
apiKeyID := rid.Rows[0][0]
_, _ = db.Query(internalCtx, "INSERT OR IGNORE INTO wallet_api_keys(namespace_id, wallet, api_key_id) VALUES (?, ?, ?)", nsID, strings.ToLower(req.Wallet), apiKeyID)
}
}
}
}
// Record ownerships (best-effort)
_, _ = db.Query(internalCtx, "INSERT OR IGNORE INTO namespace_ownership(namespace_id, owner_type, owner_id) VALUES (?, 'api_key', ?)", nsID, apiKey)
_, _ = db.Query(internalCtx, "INSERT OR IGNORE INTO namespace_ownership(namespace_id, owner_type, owner_id) VALUES (?, 'wallet', ?)", nsID, req.Wallet)
writeJSON(w, http.StatusOK, map[string]any{
"access_token": token,
"token_type": "Bearer",
@ -303,6 +340,7 @@ func (g *Gateway) verifyHandler(w http.ResponseWriter, r *http.Request) {
"refresh_token": refresh,
"subject": req.Wallet,
"namespace": ns,
"api_key": apiKey,
"nonce": req.Nonce,
"signature_verified": true,
})
@ -1091,17 +1129,17 @@ func (g *Gateway) logoutHandler(w http.ResponseWriter, r *http.Request) {
// Used for decoding Solana public keys (base58-encoded 32-byte ed25519 public keys)
func base58Decode(encoded string) ([]byte, error) {
const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
// Build reverse lookup map
lookup := make(map[rune]int)
for i, c := range alphabet {
lookup[c] = i
}
// Convert to big integer
num := big.NewInt(0)
base := big.NewInt(58)
for _, c := range encoded {
val, ok := lookup[c]
if !ok {
@ -1110,10 +1148,10 @@ func base58Decode(encoded string) ([]byte, error) {
num.Mul(num, base)
num.Add(num, big.NewInt(int64(val)))
}
// Convert to bytes
decoded := num.Bytes()
// Add leading zeros for each leading '1' in the input
for _, c := range encoded {
if c != '1' {
@ -1121,6 +1159,6 @@ func base58Decode(encoded string) ([]byte, error) {
}
decoded = append([]byte{0}, decoded...)
}
return decoded, nil
}

View File

@ -214,6 +214,8 @@ func (g *Gateway) authorizationMiddleware(next http.Handler) http.Handler {
// Identify actor from context
ownerType := ""
ownerID := ""
apiKeyFallback := ""
if v := ctx.Value(ctxKeyJWT); v != nil {
if claims, ok := v.(*jwtClaims); ok && claims != nil && strings.TrimSpace(claims.Sub) != "" {
// Determine subject type.
@ -237,6 +239,13 @@ func (g *Gateway) authorizationMiddleware(next http.Handler) http.Handler {
ownerID = strings.TrimSpace(s)
}
}
} else if ownerType == "wallet" {
// If we have a JWT wallet, also capture the API key as fallback
if v := ctx.Value(ctxKeyAPIKey); v != nil {
if s, ok := v.(string); ok && strings.TrimSpace(s) != "" {
apiKeyFallback = strings.TrimSpace(s)
}
}
}
if ownerType == "" || ownerID == "" {
@ -244,6 +253,12 @@ func (g *Gateway) authorizationMiddleware(next http.Handler) http.Handler {
return
}
g.logger.ComponentInfo("gateway", "namespace auth check",
zap.String("namespace", ns),
zap.String("owner_type", ownerType),
zap.String("owner_id", ownerID),
)
// Check ownership in DB using internal auth context
db := g.client.Database()
internalCtx := client.WithInternalAuth(ctx)
@ -261,6 +276,12 @@ func (g *Gateway) authorizationMiddleware(next http.Handler) http.Handler {
q := "SELECT 1 FROM namespace_ownership WHERE namespace_id = ? AND owner_type = ? AND owner_id = ? LIMIT 1"
res, err := db.Query(internalCtx, q, nsID, ownerType, ownerID)
// If primary owner check fails and we have a JWT wallet with API key fallback, try the API key
if (err != nil || res == nil || res.Count == 0) && ownerType == "wallet" && apiKeyFallback != "" {
res, err = db.Query(internalCtx, q, nsID, "api_key", apiKeyFallback)
}
if err != nil || res == nil || res.Count == 0 {
writeError(w, http.StatusForbidden, "forbidden: not an owner of namespace")
return