Documentation
¶
Overview ¶
Package keys owns signing-key storage, lookup, encryption-at-rest, kid-based rotation with grace periods, and JWKS publication/consumption for OneAuth.
<!-- design:start --> The keys package centralizes everything about JWT signing-key material: where keys live, how they are looked up (by clientID or by kid), how HMAC secrets are encrypted at rest, how rotated keys are kept alive during a grace period, and how public keys are published and consumed over JWKS. It deliberately keeps two contracts separate — KeyLookup (read-only) and the write extensions KeyStorage and KidStorage — so read-only sources such as a remote JWKS endpoint can plug in without implementing write methods, and so decorators operate on one KeyRecord value instead of forwarding many per-field accessors. Concrete persistent backends (FS, GORM, GAE) live in stores/ and implement these interfaces; this package ships the in-memory implementations and the cross-cutting decorators.
ENTITIES ¶
KeyRecord — single value type carrying ClientID, Key, Algorithm, and Kid for every key operation, replacing scattered per-field accessors.
KeyLookup — read-only interface: GetKey(clientID) and GetKeyByKid(kid).
KeyStorage — KeyLookup plus PutKey/DeleteKey/ListKeyIDs; the clientID-keyed write contract for persistent backends and the encryption decorator.
KidStorage — KeyLookup plus Add/Remove/CleanExpired; keyed by kid, so GetKey(clientID) always returns ErrKeyNotFound. Lets retired keys persist across restarts in backends rather than only in process memory.
InMemoryKeyStore — thread-safe in-memory KeyStorage maintaining a kid->clientID secondary index so GetKeyByKid stays O(1) and consistent on overwrite and delete.
EncryptedKeyStorage — KeyStorage decorator that AES-256-GCM encrypts HMAC (HS256/384/512) secrets at rest while asymmetric keys pass through. It computes the kid from plaintext before encryption so kid lookups still work, and falls back to returning ciphertext-as-plaintext when decryption fails to tolerate pre-encryption data during migration.
NewEncryptedKeyStorage — builds the decorator from a 64-hex-char (32-byte) master key, deriving the AES key via HKDF-SHA256 with a fixed info string.
JWKSHandler — HTTP handler serving /.well-known/jwks.json from a KeyStore plus an optional KidStore for grace-period keys. Publishes only asymmetric keys (HS256 secrets are never exposed) and supports ETag / If-None-Match / Cache-Control conditional caching.
JWKSKeyStore — read-only KeyLookup that fetches and caches public keys from a remote JWKS URL with background refresh. Cache misses trigger an on-demand refresh (rate-limited by MinRefreshGap) before returning ErrKeyNotFound; GetSigningKey always errors since only public keys are available.
NewJWKSKeyStore — constructs a JWKSKeyStore with functional options (WithHTTPClient, WithRefreshInterval, WithMinRefreshGap); Start() performs the initial fetch and launches background refresh. Defaults: RefreshInterval 1h, MinRefreshGap 5s.
KidStore — in-memory KidStorage holding kid->key mappings including time-expiring grace-period entries, so a rotated-out key keeps validating tokens until its grace period lapses.
CompositeKeyLookup — tries multiple KeyLookups in order and returns the first hit, combining a KeyStorage's current key with a KidStore's grace entries behind one lookup.
FLOWS ¶
See [diagrams.md](diagrams.md) for sequence diagrams of: key rotation with a grace-period KidStore, and JWKS publication merging current keys with grace-period keys. <!-- design:end -->
Index ¶
- Variables
- type CompositeKeyLookup
- type EncryptedKeyStorage
- func (e *EncryptedKeyStorage) DeleteKey(clientID string) error
- func (e *EncryptedKeyStorage) GetCurrentKid(clientID string) (string, error)
- func (e *EncryptedKeyStorage) GetExpectedAlg(clientID string) (string, error)
- func (e *EncryptedKeyStorage) GetKey(clientID string) (*KeyRecord, error)
- func (e *EncryptedKeyStorage) GetKeyByKid(kid string) (*KeyRecord, error)
- func (e *EncryptedKeyStorage) GetSigningKey(clientID string) (any, error)
- func (e *EncryptedKeyStorage) GetVerifyKey(clientID string) (any, error)
- func (e *EncryptedKeyStorage) ListKeyIDs() ([]string, error)
- func (e *EncryptedKeyStorage) ListKeys() ([]string, error)
- func (e *EncryptedKeyStorage) PutKey(rec *KeyRecord) error
- func (e *EncryptedKeyStorage) RegisterKey(clientID string, key any, algorithm string) error
- type InMemoryKeyStore
- func (s *InMemoryKeyStore) DeleteKey(clientID string) error
- func (s *InMemoryKeyStore) GetCurrentKid(clientID string) (string, error)
- func (s *InMemoryKeyStore) GetExpectedAlg(clientID string) (string, error)
- func (s *InMemoryKeyStore) GetKey(clientID string) (*KeyRecord, error)
- func (s *InMemoryKeyStore) GetKeyByKid(kid string) (*KeyRecord, error)
- func (s *InMemoryKeyStore) GetSigningKey(clientID string) (any, error)
- func (s *InMemoryKeyStore) GetVerifyKey(clientID string) (any, error)
- func (s *InMemoryKeyStore) ListKeyIDs() ([]string, error)
- func (s *InMemoryKeyStore) ListKeys() ([]string, error)
- func (s *InMemoryKeyStore) PutKey(rec *KeyRecord) error
- func (s *InMemoryKeyStore) RegisterKey(clientID string, key any, algorithm string) error
- type JWKSHandler
- type JWKSKeyStore
- func (s *JWKSKeyStore) GetExpectedAlg(clientID string) (string, error)
- func (s *JWKSKeyStore) GetKey(clientID string) (*KeyRecord, error)
- func (s *JWKSKeyStore) GetKeyByKid(kid string) (*KeyRecord, error)
- func (s *JWKSKeyStore) GetSigningKey(clientID string) (any, error)
- func (s *JWKSKeyStore) GetVerifyKey(clientID string) (any, error)
- func (s *JWKSKeyStore) Start() error
- func (s *JWKSKeyStore) Stop()
- type JWKSOption
- type KeyLookup
- type KeyRecord
- type KeyStorage
- type KidStorage
- type KidStore
- func (s *KidStore) Add(kid string, key any, algorithm string, clientID string, expiresAt time.Time) error
- func (s *KidStore) CleanExpired() error
- func (s *KidStore) GetKey(clientID string) (*KeyRecord, error)
- func (s *KidStore) GetKeyByKid(kid string) (*KeyRecord, error)
- func (s *KidStore) Len() int
- func (s *KidStore) Remove(kid string) error
Constants ¶
This section is empty.
Variables ¶
var ( ErrKeyNotFound = fmt.Errorf("signing key not found") ErrAlgorithmMismatch = fmt.Errorf("algorithm mismatch") ErrKidNotFound = fmt.Errorf("key not found for kid") )
Common errors for key operations
Functions ¶
This section is empty.
Types ¶
type CompositeKeyLookup ¶
type CompositeKeyLookup struct {
Lookups []KeyLookup
}
CompositeKeyLookup tries multiple KeyLookups in order, returning the first successful result. Used to combine a KeyStorage's current-key lookup with a KidStore's grace-period entries.
func (*CompositeKeyLookup) GetKey ¶
func (c *CompositeKeyLookup) GetKey(clientID string) (*KeyRecord, error)
GetKey tries each lookup in order for a client_id match.
func (*CompositeKeyLookup) GetKeyByKid ¶
func (c *CompositeKeyLookup) GetKeyByKid(kid string) (*KeyRecord, error)
GetKeyByKid tries each lookup in order for a kid match.
type EncryptedKeyStorage ¶
type EncryptedKeyStorage struct {
// contains filtered or unexported fields
}
EncryptedKeyStorage is a KeyStorage decorator that transparently encrypts HMAC (HS256/HS384/HS512) client secrets at rest using AES-256-GCM. Asymmetric keys (RS256, ES256 public keys) pass through unencrypted.
Because it wraps KeyStorage and operates on KeyRecord, it only needs to implement 5 methods — no manual forwarding of individual field accessors.
The kid is computed from plaintext key material in PutKey before encryption, so kid-based lookups work correctly even though the stored bytes are encrypted.
func NewEncryptedKeyStorage ¶
func NewEncryptedKeyStorage(inner KeyStorage, masterKeyHex string) (*EncryptedKeyStorage, error)
NewEncryptedKeyStorage creates an EncryptedKeyStorage wrapping inner. masterKeyHex must be exactly 64 hex characters (32 bytes).
func (*EncryptedKeyStorage) DeleteKey ¶
func (e *EncryptedKeyStorage) DeleteKey(clientID string) error
DeleteKey delegates to the inner store.
func (*EncryptedKeyStorage) GetCurrentKid ¶
func (e *EncryptedKeyStorage) GetCurrentKid(clientID string) (string, error)
GetCurrentKid returns the kid for the given clientID.
func (*EncryptedKeyStorage) GetExpectedAlg ¶
func (e *EncryptedKeyStorage) GetExpectedAlg(clientID string) (string, error)
GetExpectedAlg is a convenience method for callers using the old interface.
func (*EncryptedKeyStorage) GetKey ¶
func (e *EncryptedKeyStorage) GetKey(clientID string) (*KeyRecord, error)
GetKey retrieves and decrypts the key for the given clientID.
func (*EncryptedKeyStorage) GetKeyByKid ¶
func (e *EncryptedKeyStorage) GetKeyByKid(kid string) (*KeyRecord, error)
GetKeyByKid retrieves and decrypts the key matching the given kid.
func (*EncryptedKeyStorage) GetSigningKey ¶
func (e *EncryptedKeyStorage) GetSigningKey(clientID string) (any, error)
GetSigningKey is a convenience method for callers using the old interface.
func (*EncryptedKeyStorage) GetVerifyKey ¶
func (e *EncryptedKeyStorage) GetVerifyKey(clientID string) (any, error)
GetVerifyKey is a convenience method for callers using the old interface.
func (*EncryptedKeyStorage) ListKeyIDs ¶
func (e *EncryptedKeyStorage) ListKeyIDs() ([]string, error)
ListKeyIDs delegates to the inner store.
func (*EncryptedKeyStorage) ListKeys ¶
func (e *EncryptedKeyStorage) ListKeys() ([]string, error)
ListKeys is a convenience alias for ListKeyIDs.
func (*EncryptedKeyStorage) PutKey ¶
func (e *EncryptedKeyStorage) PutKey(rec *KeyRecord) error
PutKey computes kid from plaintext, then encrypts HMAC keys before storing.
func (*EncryptedKeyStorage) RegisterKey ¶
func (e *EncryptedKeyStorage) RegisterKey(clientID string, key any, algorithm string) error
RegisterKey is a convenience method for callers using the old interface.
type InMemoryKeyStore ¶
type InMemoryKeyStore struct {
// contains filtered or unexported fields
}
InMemoryKeyStore is a thread-safe in-memory KeyStorage implementation. Suitable for testing and simple single-process deployments.
func NewInMemoryKeyStore ¶
func NewInMemoryKeyStore() *InMemoryKeyStore
NewInMemoryKeyStore creates a new empty InMemoryKeyStore.
func (*InMemoryKeyStore) DeleteKey ¶
func (s *InMemoryKeyStore) DeleteKey(clientID string) error
DeleteKey removes the key for the given clientID.
func (*InMemoryKeyStore) GetCurrentKid ¶
func (s *InMemoryKeyStore) GetCurrentKid(clientID string) (string, error)
GetCurrentKid returns the kid for the given clientID.
func (*InMemoryKeyStore) GetExpectedAlg ¶
func (s *InMemoryKeyStore) GetExpectedAlg(clientID string) (string, error)
GetExpectedAlg is a convenience method matching the old KeyStore interface.
func (*InMemoryKeyStore) GetKey ¶
func (s *InMemoryKeyStore) GetKey(clientID string) (*KeyRecord, error)
GetKey returns the key record for the given clientID.
func (*InMemoryKeyStore) GetKeyByKid ¶
func (s *InMemoryKeyStore) GetKeyByKid(kid string) (*KeyRecord, error)
GetKeyByKid returns the key record matching the given kid.
func (*InMemoryKeyStore) GetSigningKey ¶
func (s *InMemoryKeyStore) GetSigningKey(clientID string) (any, error)
GetSigningKey is a convenience method matching the old KeyStore interface.
func (*InMemoryKeyStore) GetVerifyKey ¶
func (s *InMemoryKeyStore) GetVerifyKey(clientID string) (any, error)
GetVerifyKey is a convenience method matching the old KeyStore interface.
func (*InMemoryKeyStore) ListKeyIDs ¶
func (s *InMemoryKeyStore) ListKeyIDs() ([]string, error)
ListKeyIDs returns all registered client IDs.
func (*InMemoryKeyStore) ListKeys ¶
func (s *InMemoryKeyStore) ListKeys() ([]string, error)
ListKeys is a convenience alias for ListKeyIDs.
func (*InMemoryKeyStore) PutKey ¶
func (s *InMemoryKeyStore) PutKey(rec *KeyRecord) error
PutKey stores a key record. Computes Kid from key material if not set.
func (*InMemoryKeyStore) RegisterKey ¶
func (s *InMemoryKeyStore) RegisterKey(clientID string, key any, algorithm string) error
RegisterKey is a convenience method matching the old WritableKeyStore interface.
type JWKSHandler ¶
type JWKSHandler struct {
KeyStore KeyStorage // needs ListKeyIDs() and GetKey()
KidStore *KidStore // optional: serves previous keys during grace period
CacheMaxAge int // Cache-Control max-age in seconds (default: 3600)
}
JWKSHandler serves a JWKS (JSON Web Key Set) endpoint at /.well-known/jwks.json. Only asymmetric keys (RS256/ES256) are included — HS256 secrets are never exposed.
func (*JWKSHandler) ServeHTTP ¶
func (h *JWKSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
type JWKSKeyStore ¶
type JWKSKeyStore struct {
JWKSURL string
HTTPClient *http.Client
RefreshInterval time.Duration // default: 1 hour
MinRefreshGap time.Duration // default: 5 seconds
// contains filtered or unexported fields
}
JWKSKeyStore implements KeyStore (read-only) by fetching public keys from a remote JWKS endpoint. It caches keys locally and refreshes them periodically.
func NewJWKSKeyStore ¶
func NewJWKSKeyStore(jwksURL string, opts ...JWKSOption) *JWKSKeyStore
NewJWKSKeyStore creates a new JWKSKeyStore. Call Start() to begin fetching keys.
func (*JWKSKeyStore) GetExpectedAlg ¶
func (s *JWKSKeyStore) GetExpectedAlg(clientID string) (string, error)
GetExpectedAlg returns the algorithm for the given client ID.
func (*JWKSKeyStore) GetKey ¶
func (s *JWKSKeyStore) GetKey(clientID string) (*KeyRecord, error)
GetKey returns ErrKeyNotFound — JWKSKeyStore only supports kid-based lookup. JWKS doesn't carry clientID→key mappings; use GetKeyByKid instead.
func (*JWKSKeyStore) GetKeyByKid ¶
func (s *JWKSKeyStore) GetKeyByKid(kid string) (*KeyRecord, error)
GetKeyByKid returns the key record for the given kid. ClientID is empty since JWKS doesn't carry client_id metadata — the middleware skips the cross-app check in this case.
func (*JWKSKeyStore) GetSigningKey ¶
func (s *JWKSKeyStore) GetSigningKey(clientID string) (any, error)
GetSigningKey always returns an error — JWKS only exposes public keys.
func (*JWKSKeyStore) GetVerifyKey ¶
func (s *JWKSKeyStore) GetVerifyKey(clientID string) (any, error)
GetVerifyKey returns the public key for the given client ID. If the key is not cached, triggers a refresh before returning ErrKeyNotFound.
func (*JWKSKeyStore) Start ¶
func (s *JWKSKeyStore) Start() error
Start performs the initial JWKS fetch and starts background refresh.
func (*JWKSKeyStore) Stop ¶
func (s *JWKSKeyStore) Stop()
Stop stops the background refresh goroutine.
type JWKSOption ¶
type JWKSOption func(*JWKSKeyStore)
JWKSOption configures a JWKSKeyStore.
func WithHTTPClient ¶
func WithHTTPClient(c *http.Client) JWKSOption
WithHTTPClient sets the HTTP client for JWKS fetching.
func WithMinRefreshGap ¶
func WithMinRefreshGap(d time.Duration) JWKSOption
WithMinRefreshGap sets the minimum time between refreshes (prevents stampede).
func WithRefreshInterval ¶
func WithRefreshInterval(d time.Duration) JWKSOption
WithRefreshInterval sets how often keys are refreshed in the background.
type KeyLookup ¶
type KeyLookup interface {
// GetKey returns the key record for the given clientID.
// Returns ErrKeyNotFound if the client has no registered key.
GetKey(clientID string) (*KeyRecord, error)
// GetKeyByKid returns the key record matching the given kid.
// Returns ErrKidNotFound if no key matches or the key has expired.
GetKeyByKid(kid string) (*KeyRecord, error)
}
KeyLookup provides read-only key lookup by clientID or kid. Implemented by all keystores, including read-only ones like JWKSKeyStore.
type KeyRecord ¶
type KeyRecord struct {
ClientID string // owning client/app
Key any // []byte for HMAC, PEM bytes for asymmetric
Algorithm string // "HS256", "RS256", "ES256", etc.
Kid string // key identifier, computed from key material
}
KeyRecord holds all fields for a stored signing key. All key operations work with this type rather than separate accessor methods.
type KeyStorage ¶
type KeyStorage interface {
KeyLookup
// PutKey stores a key record. If Kid is empty, it is auto-computed
// from the key material and algorithm. Overwrites any existing key
// for the same ClientID.
PutKey(record *KeyRecord) error
// DeleteKey removes the key for the given clientID.
DeleteKey(clientID string) error
// ListKeyIDs returns all registered client IDs.
ListKeyIDs() ([]string, error)
}
KeyStorage extends KeyLookup with write operations. Implemented by persistent backends (InMemory, GORM, FS, GAE).
type KidStorage ¶ added in v0.0.83
type KidStorage interface {
KeyLookup
// Add registers a kid→key mapping. If expiresAt is zero, the key has
// no expiry. Re-adding an existing kid overwrites it.
Add(kid string, key any, algorithm string, clientID string, expiresAt time.Time) error
// Remove deletes a kid entry. Removing an absent kid is not an error.
Remove(kid string) error
// CleanExpired removes all entries whose expiry has passed.
CleanExpired() error
}
KidStorage is the write side of a kid-indexed key store. It extends the read-only KeyLookup with the grace-period operations KidStore provides, so retired keys can be persisted across process restarts by backends in stores/ (FS, GORM, GAE) rather than living only in process memory.
Unlike KeyStorage, KidStorage is keyed by kid (not clientID): GetKey by clientID always returns ErrKeyNotFound — only GetKeyByKid is meaningful.
type KidStore ¶
type KidStore struct {
// contains filtered or unexported fields
}
KidStore is an in-memory KidStorage that tracks kid→key mappings, including grace-period entries retained during key rotation.
Usage during rotation:
- kidStore.Add(oldKid, oldKey, alg, clientID, time.Now().Add(gracePeriod))
- keyStorage.PutKey(newRecord) // overwrites current
- kidStore holds the old key until grace period expires
func (*KidStore) Add ¶
func (s *KidStore) Add(kid string, key any, algorithm string, clientID string, expiresAt time.Time) error
Add registers a kid→key mapping. If expiresAt is zero, the key has no expiry.
func (*KidStore) CleanExpired ¶
CleanExpired removes all expired entries.
func (*KidStore) GetKey ¶
GetKey always returns ErrKeyNotFound — KidStore only supports kid-based lookup.
func (*KidStore) GetKeyByKid ¶
GetKeyByKid returns the key record for the given kid. Returns ErrKidNotFound if the kid is unknown or expired.