Documentation
¶
Overview ¶
Package apikeys is the credential a program uses to call an API: issued once, shown once, revocable, scoped, and recognisable in a log.
A framework that only has passwords and browser sessions pushes every machine-to-machine caller into one of two bad places — a shared password in a config file, or a JWT with no way to revoke it. This package is the third option, and it is the one every product ends up needing.
The shape of a key ¶
nk_a1b2c3d4_Zm9vYmFyYmF6cXV4... │ │ └ the secret: 256 bits, never stored │ └ the key id: what a listing shows and a log line names └ a fixed prefix, so a leaked key is recognisable in a scan
The prefix is not decoration. Secret scanners match on prefixes, and a key that looks like any other base64 string is one nobody can find in a repository they just leaked.
Index ¶
- Constants
- Variables
- func Middleware(store Store) func(http.Handler) http.Handler
- func Require(scopes ...string) func(http.Handler) http.Handler
- type Flavor
- type Key
- func Authenticate(ctx context.Context, store Store, presented string) (Key, error)
- func FromContext(ctx context.Context) (Key, bool)
- func Issue(ctx context.Context, store Store, spec Key) (Key, string, error)
- func Rotate(ctx context.Context, store Store, id string, grace time.Duration) (Key, string, error)
- type SQLStore
- func (s *SQLStore) ByID(ctx context.Context, id string) (Key, error)
- func (s *SQLStore) Create(ctx context.Context, key Key) error
- func (s *SQLStore) List(ctx context.Context, ownerID string) ([]Key, error)
- func (s *SQLStore) Revoke(ctx context.Context, id string, at time.Time) error
- func (s *SQLStore) TouchLastUsed(ctx context.Context, id string, at time.Time) error
- type SQLStoreConfig
- type Store
Constants ¶
const HeaderName = "X-API-Key"
HeaderName is the dedicated header. The Authorization bearer scheme is accepted too, because half the clients in the world only know that one.
const Prefix = "nk"
Prefix is the fixed marker every key carries.
Variables ¶
var ( // ErrInvalidKey covers a malformed key, an unknown one, a revoked one // and an expired one — one error, because telling them apart tells an // attacker which guess was closer. ErrInvalidKey = errors.New("apikeys: invalid key") // ErrNotFound is for management operations on a key id that does not // exist. It is NOT what a failed authentication returns. ErrNotFound = errors.New("apikeys: no such key") )
Errors callers distinguish.
Functions ¶
func Middleware ¶
Middleware authenticates requests carrying an API key.
It puts the key's OWNER in the observability context, which is not bookkeeping: the rate limiter keys on the authenticated user id with the tenant as a prefix, so a key that lands there is throttled as an identity instead of sharing a bucket with every other caller behind the same address. Measuring that the limiter already worked this way is what made this three lines instead of a second limiter.
A request with no key passes through untouched. Authentication is what this middleware does; authorisation is the policy layer's job, and a route that must have a key uses Require.
Types ¶
type Flavor ¶
type Flavor string
Flavor is the SQL dialect, handled explicitly for the same reason the accounts store does it: placeholders and timestamp types differ, and guessing is how a store "works" on one engine.
type Key ¶
type Key struct {
ID string
Name string
OwnerID string
SecretHash string
Scopes []string
CreatedAt time.Time
ExpiresAt time.Time
LastUsedAt time.Time
RevokedAt time.Time
RotatedFrom string
}
Key is an issued credential. SecretHash is what is stored; the secret itself exists only in the string returned by Issue.
func Authenticate ¶
Authenticate resolves a presented key string.
The comparison is constant-time, and the lookup is by ID — so a wrong secret costs one hash, not a scan of every key, and a valid id with a wrong secret is indistinguishable from an unknown id.
func FromContext ¶
FromContext returns the key that authenticated this request.
func Issue ¶
Issue creates a key and returns BOTH the record and the single string the caller must copy now. The secret is not recoverable afterwards: only its hash is stored, which is what makes a leaked database not a leak of every key.
func Rotate ¶
Rotate issues a replacement and revokes the original after a grace period, so a deployment can roll the new key out before the old one stops working. A rotation that breaks the caller at the moment it happens is a rotation nobody performs.
type SQLStore ¶
type SQLStore struct {
// contains filtered or unexported fields
}
SQLStore keeps keys in one table.
func NewSQLStore ¶
NewSQLStore creates the table if it does not exist.
func (*SQLStore) Create ¶
Create implements Store. It also serves as the update path for rotation, so one row's lifecycle is written in one place.
func (*SQLStore) List ¶
List implements Store. An empty ownerID lists every key, which is what an operator surface asks for.
type SQLStoreConfig ¶
SQLStoreConfig configures the store. Table defaults to "nucleus_api_keys".
type Store ¶
type Store interface {
Create(ctx context.Context, key Key) error
// ByID returns the key regardless of its state; authentication
// decides what to do about revocation and expiry.
ByID(ctx context.Context, id string) (Key, error)
List(ctx context.Context, ownerID string) ([]Key, error)
Revoke(ctx context.Context, id string, at time.Time) error
// TouchLastUsed records use. It is called on the hot path, so an
// implementation is free to sample or batch — nothing depends on it
// being exact.
TouchLastUsed(ctx context.Context, id string, at time.Time) error
}
Store is where keys live.