* Add ConfigKeyStore for in-memory key management * Introduce DatabaseKeyStore for PostgreSQL-backed key storage * Create KeyStoreAuthenticator for API key validation * Define SQL procedures for key management in PostgreSQL * Document keystore functionality and usage in KEYSTORE.md
5.0 KiB
Keystore
Per-user named auth keys with pluggable storage. Each user can hold multiple keys of different types — JWT secrets, header API keys, OAuth2 client credentials, or generic API keys. Keys are identified by a human-readable name ("CI deploy", "mobile app") and can carry scopes and arbitrary metadata.
Key types
| Constant | Value | Use case |
|---|---|---|
KeyTypeJWTSecret |
jwt_secret |
Per-user JWT signing secret |
KeyTypeHeaderAPI |
header_api |
Static API key sent in a request header |
KeyTypeOAuth2 |
oauth2 |
OAuth2 client credentials |
KeyTypeGenericAPI |
api |
General-purpose application key |
Storage backends
ConfigKeyStore
In-memory store seeded from a static list. Suitable for a small, fixed set of service-account keys loaded from a config file. Keys created at runtime via CreateKey are held in memory and lost on restart.
// Pre-load keys from config (KeyHash = SHA-256 hex of the raw key)
store := security.NewConfigKeyStore([]security.UserKey{
{
UserID: 1,
KeyType: security.KeyTypeGenericAPI,
KeyHash: "e3b0c44298fc1c149afb...", // sha256(rawKey)
Name: "CI deploy",
Scopes: []string{"deploy"},
IsActive: true,
},
})
DatabaseKeyStore
Backed by PostgreSQL stored procedures. Supports optional caching (default 2-minute TTL). Apply keystore_schema.sql before use.
db, _ := sql.Open("postgres", dsn)
store := security.NewDatabaseKeyStore(db)
// With options
store = security.NewDatabaseKeyStore(db, security.DatabaseKeyStoreOptions{
CacheTTL: 5 * time.Minute,
SQLNames: &security.KeyStoreSQLNames{
ValidateKey: "myapp_keystore_validate", // override one procedure name
},
})
Managing keys
ctx := context.Background()
// Create — raw key returned once; store it securely
resp, err := store.CreateKey(ctx, security.CreateKeyRequest{
UserID: 42,
KeyType: security.KeyTypeGenericAPI,
Name: "mobile app",
Scopes: []string{"read", "write"},
})
fmt.Println(resp.RawKey) // only shown here; hashed internally
// List
keys, err := store.GetUserKeys(ctx, 42, "") // "" = all types
keys, err = store.GetUserKeys(ctx, 42, security.KeyTypeGenericAPI)
// Revoke
err = store.DeleteKey(ctx, 42, resp.Key.ID)
// Validate (used by authenticators internally)
key, err := store.ValidateKey(ctx, rawKey, "")
HTTP authentication
KeyStoreAuthenticator wraps any KeyStore and implements the Authenticator interface. It is drop-in compatible with DatabaseAuthenticator and works in CompositeSecurityProvider.
Keys are extracted from the request in this order:
Authorization: Bearer <key>Authorization: ApiKey <key>X-API-Key: <key>
auth := security.NewKeyStoreAuthenticator(store, "") // "" = accept any key type
// Restrict to a specific type:
auth = security.NewKeyStoreAuthenticator(store, security.KeyTypeGenericAPI)
Plug it into a handler:
handler := resolvespec.NewHandler(db, registry,
resolvespec.WithAuthenticator(auth),
)
Login and Logout return an error — key lifecycle is managed through KeyStore directly.
On successful validation the request context receives a UserContext where:
UserID— from the keyRoles— the key'sScopesClaims["key_type"]— key type stringClaims["key_name"]— key name
Database setup
Apply keystore_schema.sql to your PostgreSQL database. It requires the users table from the main database_schema.sql.
\i pkg/security/keystore_schema.sql
This creates:
user_keystable with indexes onuser_id,key_hash, andkey_typeresolvespec_keystore_get_user_keys(p_user_id, p_key_type)resolvespec_keystore_create_key(p_request jsonb)resolvespec_keystore_delete_key(p_user_id, p_key_id)resolvespec_keystore_validate_key(p_key_hash, p_key_type)
Custom procedure names
store := security.NewDatabaseKeyStore(db, security.DatabaseKeyStoreOptions{
SQLNames: &security.KeyStoreSQLNames{
GetUserKeys: "myschema_get_keys",
CreateKey: "myschema_create_key",
DeleteKey: "myschema_delete_key",
ValidateKey: "myschema_validate_key",
},
})
// Validate names at startup
names := &security.KeyStoreSQLNames{
GetUserKeys: "myschema_get_keys",
// ...
}
if err := security.ValidateKeyStoreSQLNames(names); err != nil {
log.Fatal(err)
}
Security notes
- Raw keys are never stored. Only the SHA-256 hex digest is persisted.
- The raw key is generated with
crypto/rand(32 bytes, base64url-encoded) and returned exactly once inCreateKeyResponse.RawKey. - Hash comparisons in
ConfigKeyStoreusecrypto/subtle.ConstantTimeCompareto prevent timing side-channels. DeleteKeyperforms a soft delete (is_active = false). TheDatabaseKeyStoreinvalidates the cache entry immediately, but due to the cache TTL a revoked key may authenticate for up toCacheTTL(default 2 minutes) in a distributed environment. SetCacheTTL: 0to disable caching if immediate revocation is required.