apikeys

package
v1.28.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 12, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

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

View Source
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.

View Source
const Prefix = "nk"

Prefix is the fixed marker every key carries.

Variables

View Source
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

func Middleware(store Store) func(http.Handler) http.Handler

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.

func Require

func Require(scopes ...string) func(http.Handler) http.Handler

Require refuses a request that did not present a valid key, and — when scopes are named — one whose key does not carry them ALL.

r.With(apikeys.Require("billing:read")).Get("/invoices", listInvoices)

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.

const (
	FlavorSQLite   Flavor = "sqlite"
	FlavorPostgres Flavor = "postgres"
	FlavorMySQL    Flavor = "mysql"
)

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

func Authenticate(ctx context.Context, store Store, presented string) (Key, error)

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

func FromContext(ctx context.Context) (Key, bool)

FromContext returns the key that authenticated this request.

func Issue

func Issue(ctx context.Context, store Store, spec Key) (Key, string, error)

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

func Rotate(ctx context.Context, store Store, id string, grace time.Duration) (Key, string, error)

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.

func (Key) Active

func (k Key) Active(now time.Time) bool

Active reports whether the key may authenticate at this moment.

func (Key) HasScope

func (k Key) HasScope(scope string) bool

HasScope reports whether the key carries a scope. A key with NO scopes carries none: an empty list is "this key may do nothing in particular", never "this key may do anything". The opposite default is how an unscoped key ends up with more power than a scoped one.

type SQLStore

type SQLStore struct {
	// contains filtered or unexported fields
}

SQLStore keeps keys in one table.

func NewSQLStore

func NewSQLStore(ctx context.Context, db *sql.DB, cfg SQLStoreConfig) (*SQLStore, error)

NewSQLStore creates the table if it does not exist.

func (*SQLStore) ByID

func (s *SQLStore) ByID(ctx context.Context, id string) (Key, error)

ByID implements Store.

func (*SQLStore) Create

func (s *SQLStore) Create(ctx context.Context, key Key) error

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

func (s *SQLStore) List(ctx context.Context, ownerID string) ([]Key, error)

List implements Store. An empty ownerID lists every key, which is what an operator surface asks for.

func (*SQLStore) Revoke

func (s *SQLStore) Revoke(ctx context.Context, id string, at time.Time) error

Revoke implements Store.

func (*SQLStore) TouchLastUsed

func (s *SQLStore) TouchLastUsed(ctx context.Context, id string, at time.Time) error

TouchLastUsed implements Store.

type SQLStoreConfig

type SQLStoreConfig struct {
	Flavor Flavor
	Table  string
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL