auth

package module
v0.1.0-alpha.1 Latest Latest
Warning

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

Go to latest
Published: Jun 5, 2026 License: MIT Imports: 8 Imported by: 0

README

auth

auth is a Go package for issuing, verifying, listing, and revoking API keys for users or groups. It is designed so applications can bring their own storage while the package handles the security-sensitive workflow: API key generation, HMAC lookup hashing, expiration, revocation, scope checks, pagination, and audit events.

The package currently includes native SQLite, PostgreSQL, MySQL/MariaDB, and MongoDB stores plus Redis schema marker helpers.

Features

  • API keys for user and group principals.
  • Raw API keys are returned once and are never stored by the core service.
  • Stored lookup hashes use HMAC-SHA-256 with an application-controlled secret key.
  • Prefix lookup keeps verification efficient without storing raw keys.
  • Default API key TTL is 90 days unless overridden.
  • Scope checks are deny-by-default when required scopes are missing.
  • Structured audit events for create, verify, failed verify, and revoke.
  • Optional atomic key/audit writes through AtomicAPIKeyAuditStore.
  • Cursor pagination with bounded page sizes.
  • Non-destructive migrations that create missing tables/indexes only.
  • Explicit delete helpers for callers that intentionally want to clear auth data.

Install

go get github.com/lechefran/auth

Integration Guides

Quick Start With SQLite

SQLite, PostgreSQL, MySQL/MariaDB, and MongoDB are complete built-in store adapters for principal, API key, audit, and pagination workflows. SQLite, PostgreSQL, MySQL/MariaDB, and MongoDB's explicit TransactionalStore support atomic key/audit operations.

package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"time"

	auth "github.com/lechefran/auth"
	"github.com/lechefran/auth/keys"
	"github.com/lechefran/auth/sqlite"
)

func main() {
	ctx := context.Background()

	db, err := sqlite.Open(ctx, "auth.db")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	if err := sqlite.Migrate(ctx, db); err != nil {
		log.Fatal(err)
	}

	store := sqlite.NewStore(db)

	now := time.Now().UTC()
	err = store.CreatePrincipal(ctx, auth.Principal{
		ID:        "user_123",
		Type:      auth.PrincipalTypeUser,
		Name:      "Example User",
		CreatedAt: now,
		UpdatedAt: now,
	})
	if err != nil && !errors.Is(err, auth.ErrAlreadyExists) {
		log.Fatal(err)
	}

	lookupKey, err := keys.GenerateHMACKey()
	if err != nil {
		log.Fatal(err)
	}

	service, err := auth.New(auth.Config{
		Issuer:          "example-api",
		KeyPrefix:       "ak",
		APIKeyLookupKey: lookupKey,
		Principals:      store,
		APIKeys:         store,
		Audit:           store,
	})
	if err != nil {
		log.Fatal(err)
	}

	created, err := service.CreateAPIKey(ctx, auth.CreateAPIKeyRequest{
		OwnerType: auth.PrincipalTypeUser,
		OwnerID:   "user_123",
		Name:      "local development",
		Scopes:    []string{"read:widgets", "write:widgets"},
	})
	if err != nil {
		log.Fatal(err)
	}

	// Show RawKey once to the caller. Do not log or store it.
	fmt.Println(created.RawKey)

	verified, err := service.VerifyAPIKey(ctx, auth.VerifyAPIKeyRequest{
		RawKey:         created.RawKey,
		RequiredScopes: []string{"read:widgets"},
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(verified.Principal.ID)
}

Core API

Create a service with auth.New:

service, err := auth.New(auth.Config{
	Issuer:          "billing-api",
	KeyPrefix:       "ak",
	APIKeyTTL:       30 * 24 * time.Hour,
	APIKeyLookupKey: lookupKey,
	Principals:      principalStore,
	APIKeys:         apiKeyStore,
	Audit:           auditStore,
})

Important configuration rules:

  • Issuer is required and should be stable for a deployment.
  • APIKeyLookupKey is required when APIKeys is configured. Load it from secret management; do not hard-code it.
  • KeyPrefix defaults to ak and may contain ASCII letters, digits, and hyphens.
  • APIKeyTTL defaults to 90 days.
  • Audit is optional, but recommended for operational visibility.

Generate a lookup key:

lookupKey, err := keys.GenerateHMACKey()

In production, generate this once and store it in a secret manager. Rotating it without a migration strategy will make existing API key hashes unverifiable.

Create, Verify, Revoke

created, err := service.CreateAPIKey(ctx, auth.CreateAPIKeyRequest{
	OwnerType: auth.PrincipalTypeGroup,
	OwnerID:   "group_ops",
	Name:      "deploy automation",
	Scopes:    []string{"deploy:read", "deploy:write"},
})

created.RawKey is the only copy of the credential. created.APIKey.Hash is redacted from service results.

verified, err := service.VerifyAPIKey(ctx, auth.VerifyAPIKeyRequest{
	RawKey:         rawKeyFromRequest,
	RequiredScopes: []string{"deploy:write"},
})

Verification rejects malformed keys, wrong secrets, expired keys, revoked keys, disabled principals, and missing required scopes.

err := service.RevokeAPIKey(ctx, auth.RevokeAPIKeyRequest{
	APIKeyID: verified.APIKey.ID,
})

Revocation flow:

  • Callers revoke by stored APIKey.ID, not by raw API key.
  • The store sets RevokedAt; the raw API key cannot verify after that point.
  • Verification returns auth.ErrInvalidCredentials for revoked keys so callers do not learn whether the key was revoked, expired, missing, or malformed.
  • Revoke returns store errors such as auth.ErrNotFound or auth.ErrInvalidState when the key does not exist or cannot transition.
  • Revoke records api_key.revoked when an audit store is configured.

Atomic revoke behavior:

  • If the same store value is configured as both APIKeys and Audit, and that store implements AtomicAPIKeyAuditStore, the service uses RevokeAPIKeyWithAudit.
  • In that path, the store reads the key, revokes it, and writes the audit event in one store-owned atomic operation.
  • If the stores are different values or the interface is not implemented, the service performs a normal read, revoke, and best-effort audit write.

SQLite implements the atomic path. Custom stores should implement AtomicAPIKeyAuditStore when API key metadata and audit events live in the same transactional database.

Scope Enforcement

Scopes are simple strings attached to each API key at creation time. The service normalizes scopes by trimming whitespace, rejects malformed scopes, and removes duplicates.

created, err := service.CreateAPIKey(ctx, auth.CreateAPIKeyRequest{
	OwnerType: auth.PrincipalTypeUser,
	OwnerID:   "user_123",
	Name:      "read-only dashboard",
	Scopes:    []string{"reports:read"},
})

Enforce scopes during verification by passing RequiredScopes. Verification is deny-by-default for requested permissions: every required scope must be present on the API key.

verified, err := service.VerifyAPIKey(ctx, auth.VerifyAPIKeyRequest{
	RawKey:         rawKeyFromRequest,
	RequiredScopes: []string{"reports:read"},
})
if errors.Is(err, auth.ErrPermissionDenied) {
	// The key is valid, but it does not have every required scope.
	return err
}
if err != nil {
	return err
}

_ = verified

Scope enforcement behavior:

  • RequiredScopes empty means authentication only; no authorization scope is required by the service call.
  • RequiredScopes non-empty requires every listed scope.
  • Extra scopes on the key are allowed.
  • Missing scopes return auth.ErrPermissionDenied.
  • Missing-scope denials record api_key.verification_failed with reason=missing_scope when audit is configured.

Pagination

List APIs use cursor pagination.

page, err := service.ListAPIKeys(ctx, auth.ListAPIKeysRequest{
	OwnerType: auth.PrincipalTypeUser,
	OwnerID:   "user_123",
	Page: auth.PageRequest{
		Limit: 50,
	},
})
if err != nil {
	return err
}

for _, key := range page.Items {
	fmt.Println(key.ID, key.Name)
}

if page.HasMore() {
	nextPage, err := service.ListAPIKeys(ctx, auth.ListAPIKeysRequest{
		OwnerType: auth.PrincipalTypeUser,
		OwnerID:   "user_123",
		Page: auth.PageRequest{
			Limit:  50,
			Cursor: page.NextCursor,
		},
	})
	_ = nextPage
	_ = err
}

Defaults and limits:

  • Limit == 0 uses auth.DefaultPageLimit (50).
  • Limits above auth.MaxPageLimit (200) are capped.
  • Cursors are opaque. Pass them back unchanged.

Storage Interfaces

Applications can supply their own database implementation by satisfying these interfaces:

type PrincipalStore interface {
	GetPrincipal(ctx context.Context, principalType auth.PrincipalType, principalID string) (auth.Principal, error)
}

type APIKeyStore interface {
	CreateAPIKey(ctx context.Context, key auth.APIKey) error
	GetAPIKeyByID(ctx context.Context, keyID string) (auth.APIKey, error)
	GetAPIKeyByPrefix(ctx context.Context, prefix string) (auth.APIKey, error)
	ListAPIKeys(ctx context.Context, ownerType auth.PrincipalType, ownerID string, page auth.PageRequest) (auth.Page[auth.APIKey], error)
	RevokeAPIKey(ctx context.Context, keyID string, revokedAt time.Time) error
	TouchAPIKey(ctx context.Context, keyID string, usedAt time.Time) error
}

type AuditStore interface {
	RecordAuditEvent(ctx context.Context, event auth.AuditEvent) error
}

Store implementation requirements:

  • Never persist raw API keys.
  • Index API keys by Prefix.
  • Store Hash exactly as provided by the service.
  • Return auth.ErrNotFound for missing rows/documents.
  • Return auth.ErrAlreadyExists for duplicate IDs or unique prefixes.
  • Make TouchAPIKey best-effort safe; verification does not fail if touch storage fails.
  • Keep cursor ordering stable and deterministic.

Native Adapter Setup

Adapter setup helpers are non-destructive. They create missing tables, collections, indexes, or marker records only. Existing SQL schemas are compatibility-checked before migrations are recorded.

SQLite
db, err := sqlite.Open(ctx, "auth.db")
if err != nil {
	return err
}
defer db.Close()

if err := sqlite.Migrate(ctx, db); err != nil {
	return err
}

store := sqlite.NewStore(db)

Clear auth data explicitly:

err := sqlite.DeleteData(ctx, db)
// or:
err = store.DeleteData(ctx)
MySQL / MariaDB
db, err := mysql.Open(ctx, dsn)
if err != nil {
	return err
}
defer db.Close()

if err := mysql.Migrate(ctx, db); err != nil {
	return err
}

store := mysql.NewStore(db)

service, err := auth.New(auth.Config{
	Issuer:          "example-api",
	APIKeyLookupKey: lookupKey,
	Principals:      store,
	APIKeys:         store,
	Audit:           store,
})

Useful helpers:

err := mysql.ValidateSchema(ctx, db)
err = mysql.DeleteData(ctx, db)
// or:
err = store.DeleteData(ctx)
PostgreSQL
db, err := postgres.Open(ctx, dsn)
if err != nil {
	return err
}
defer db.Close()

if err := postgres.Migrate(ctx, db); err != nil {
	return err
}

store := postgres.NewStore(db)

service, err := auth.New(auth.Config{
	Issuer:          "example-api",
	APIKeyLookupKey: lookupKey,
	Principals:      store,
	APIKeys:         store,
	Audit:           store,
})

Useful helpers:

err := postgres.ValidateSchema(ctx, db)
err = postgres.DeleteData(ctx, db)
// or:
err = store.DeleteData(ctx)
Redis

Redis migrations use namespaced marker keys. Redis does not currently provide a full API key store adapter in this package.

client, err := redis.Open(ctx, &goredis.Options{
	Addr: "127.0.0.1:6379",
})
if err != nil {
	return err
}
defer client.Close()

if err := redis.MigrateNamespace(ctx, client, "prod"); err != nil {
	return err
}

Delete auth data in a namespace:

err := redis.DeleteNamespaceData(ctx, client, "prod")

For reset/shutdown workflows where writers have been quiesced, drain until multiple scan passes observe no keys:

err := redis.DrainNamespaceData(ctx, client, "prod", redis.DrainOptions{
	EmptyPasses: 2,
	MaxPasses:   10,
})

DrainNamespaceData is bounded. It returns redis.ErrNamespaceNotDrained if keys continue to appear through the configured pass limit.

MongoDB

MongoDB is a native store adapter. Migrations create indexes with simple collation for string identity indexes. The default mongodb.Store uses normal writes with best-effort audit. Use mongodb.TransactionalStore when you want atomic create/revoke with audit and your MongoDB deployment supports transactions.

conn, err := mongodb.Open(ctx, uri, "auth")
if err != nil {
	return err
}
defer conn.Close(ctx)

if err := conn.Migrate(ctx); err != nil {
	return err
}

store := conn.Store()

service, err := auth.New(auth.Config{
	Issuer:          "example-api",
	APIKeyLookupKey: lookupKey,
	Principals:      store,
	APIKeys:         store,
	Audit:           store,
})

For transaction-backed audit:

store := conn.TransactionalStore()

If your application already owns the MongoDB client:

db := client.Database("auth")
if err := mongodb.Migrate(ctx, db); err != nil {
	return err
}

store := mongodb.NewStore(db)
// or, on a replica set / sharded cluster:
transactionalStore := mongodb.NewTransactionalStore(db)

Clear auth data explicitly:

err := conn.DeleteData(ctx)
// or:
err = mongodb.DeleteData(ctx, db)

Audit Behavior

Audit events are structured and must not include secrets. The service records:

  • api_key.created
  • api_key.verified
  • api_key.verification_failed
  • api_key.revoked

Failed verification audit events may include non-secret metadata such as the API key prefix and denial reason. Scope denial records reason=missing_scope.

Security Notes

  • Treat raw API keys as bearer credentials.
  • Never log RawKey, key hashes, lookup keys, database credentials, or private key material.
  • Use TLS and authenticated connections for remote databases.
  • Keep APIKeyLookupKey in secret management and load it at process startup.
  • Use short, explicit scopes and check required scopes server-side.
  • Quiesce writers before destructive delete/drain operations.
  • Prefer stores that implement AtomicAPIKeyAuditStore when API key metadata and audit events live in the same database.

Testing

Run:

go test ./...

See TESTING.md for the package testing matrix and the rules for keeping it current as behavior changes.

Documentation

Overview

Package auth provides database-independent API key workflows for users and groups.

The service generates raw API keys, stores only HMAC-SHA-256 lookup hashes, verifies credentials, checks required scopes, revokes keys, lists keys with cursor pagination, and records structured audit events. Applications provide storage through small interfaces, or use an included native store adapter.

Key generation and service setup

Generate the API key lookup key once, store it in secret management, and load it at process startup. Do not generate a new lookup key on every boot unless you intentionally want existing API keys to stop verifying.

lookupKey, err := keys.GenerateHMACKey()
if err != nil {
	return err
}

service, err := auth.New(auth.Config{
	Issuer:          "example-api",
	KeyPrefix:       "ak",
	APIKeyLookupKey: lookupKey,
	Principals:      principalStore,
	APIKeys:         apiKeyStore,
	Audit:           auditStore,
})
if err != nil {
	return err
}

Create and verify API keys

API keys can be owned by users or groups. RawKey is returned once and must not be logged or stored.

created, err := service.CreateAPIKey(ctx, auth.CreateAPIKeyRequest{
	OwnerType: auth.PrincipalTypeUser,
	OwnerID:   "user_123",
	Name:      "production client",
	Scopes:    []string{"widgets:read", "widgets:write"},
})
if err != nil {
	return err
}

rawKey := created.RawKey

verified, err := service.VerifyAPIKey(ctx, auth.VerifyAPIKeyRequest{
	RawKey:         rawKey,
	RequiredScopes: []string{"widgets:read"},
})
if err != nil {
	return err
}

_ = verified.Principal

Revoke and list API keys

err := service.RevokeAPIKey(ctx, auth.RevokeAPIKeyRequest{
	APIKeyID: verified.APIKey.ID,
})
if err != nil {
	return err
}

page, err := service.ListAPIKeys(ctx, auth.ListAPIKeysRequest{
	OwnerType: auth.PrincipalTypeUser,
	OwnerID:   "user_123",
	Page: auth.PageRequest{
		Limit: 50,
	},
})
if err != nil {
	return err
}

if page.HasMore() {
	nextPage, err := service.ListAPIKeys(ctx, auth.ListAPIKeysRequest{
		OwnerType: auth.PrincipalTypeUser,
		OwnerID:   "user_123",
		Page: auth.PageRequest{
			Limit:  50,
			Cursor: page.NextCursor,
		},
	})
	_ = nextPage
	_ = err
}

SQLite setup

SQLite is the complete built-in store adapter. It implements PrincipalStore, APIKeyStore, AuditStore, and AtomicAPIKeyAuditStore.

db, err := sqlite.Open(ctx, "auth.db")
if err != nil {
	return err
}
defer db.Close()

if err := sqlite.Migrate(ctx, db); err != nil {
	return err
}

store := sqlite.NewStore(db)

service, err := auth.New(auth.Config{
	Issuer:          "example-api",
	APIKeyLookupKey: lookupKey,
	Principals:      store,
	APIKeys:         store,
	Audit:           store,
})

MySQL and MariaDB setup

MySQL/MariaDB is a complete built-in store adapter. It implements PrincipalStore, APIKeyStore, AuditStore, and AtomicAPIKeyAuditStore.

db, err := mysql.Open(ctx, dsn)
if err != nil {
	return err
}
defer db.Close()

if err := mysql.Migrate(ctx, db); err != nil {
	return err
}

if err := mysql.ValidateSchema(ctx, db); err != nil {
	return err
}

store := mysql.NewStore(db)

service, err := auth.New(auth.Config{
	Issuer:          "example-api",
	APIKeyLookupKey: lookupKey,
	Principals:      store,
	APIKeys:         store,
	Audit:           store,
})

PostgreSQL setup

PostgreSQL is a complete built-in store adapter. It implements PrincipalStore, APIKeyStore, AuditStore, and AtomicAPIKeyAuditStore.

db, err := postgres.Open(ctx, dsn)
if err != nil {
	return err
}
defer db.Close()

if err := postgres.Migrate(ctx, db); err != nil {
	return err
}

if err := postgres.ValidateSchema(ctx, db); err != nil {
	return err
}

store := postgres.NewStore(db)

service, err := auth.New(auth.Config{
	Issuer:          "example-api",
	APIKeyLookupKey: lookupKey,
	Principals:      store,
	APIKeys:         store,
	Audit:           store,
})

Redis setup

Redis support currently covers namespaced migration markers and explicit namespace deletion helpers.

client, err := redis.Open(ctx, &goredis.Options{
	Addr: "127.0.0.1:6379",
})
if err != nil {
	return err
}
defer client.Close()

if err := redis.MigrateNamespace(ctx, client, "prod"); err != nil {
	return err
}

// For reset workflows, quiesce writers before draining.
err = redis.DrainNamespaceData(ctx, client, "prod", redis.DrainOptions{
	EmptyPasses: 2,
	MaxPasses:   10,
})

MongoDB setup

MongoDB is a complete built-in store adapter. The default Store implements PrincipalStore, APIKeyStore, and AuditStore. Use TransactionalStore for atomic key/audit operations on deployments that support MongoDB transactions.

conn, err := mongodb.Open(ctx, uri, "auth")
if err != nil {
	return err
}
defer conn.Close(ctx)

if err := conn.Migrate(ctx); err != nil {
	return err
}

store := conn.Store()

service, err := auth.New(auth.Config{
	Issuer:          "example-api",
	APIKeyLookupKey: lookupKey,
	Principals:      store,
	APIKeys:         store,
	Audit:           store,
})

For transaction-backed audit:

store := conn.TransactionalStore()

If your application already owns the MongoDB client, pass its database handle directly:

db := client.Database("auth")
if err := mongodb.Migrate(ctx, db); err != nil {
	return err
}

store := mongodb.NewStore(db)
transactionalStore := mongodb.NewTransactionalStore(db)
_ = transactionalStore

Custom stores

To use another database, implement PrincipalStore, APIKeyStore, and optionally AuditStore. Stores must never persist raw API keys. Store APIKey.Hash exactly as provided, index by APIKey.Prefix, return ErrNotFound for missing records, and return ErrAlreadyExists for uniqueness conflicts. If key metadata and audit events live in the same database, implement AtomicAPIKeyAuditStore so create and revoke operations can commit their audit event atomically.

Index

Constants

View Source
const (
	// DefaultPageLimit is used when callers do not provide a limit.
	DefaultPageLimit = 50

	// MaxPageLimit is the largest page size accepted by core workflows.
	MaxPageLimit = 200
)

Variables

View Source
var (
	// ErrNotFound reports that a requested record does not exist.
	ErrNotFound = errors.New("auth: not found")

	// ErrAlreadyExists reports that creating a record would violate uniqueness.
	ErrAlreadyExists = errors.New("auth: already exists")

	// ErrConflict reports that a write could not be applied because the stored
	// state changed or conflicts with the requested state.
	ErrConflict = errors.New("auth: conflict")

	// ErrInvalidState reports that a requested transition is not allowed for the
	// current resource state, such as revoking an already-revoked token.
	ErrInvalidState = errors.New("auth: invalid state")

	// ErrInvalidRequest reports malformed or incomplete caller input.
	ErrInvalidRequest = errors.New("auth: invalid request")

	// ErrInvalidCredentials reports failed authentication without revealing
	// whether an API key prefix, hash, owner, or state was the failing factor.
	ErrInvalidCredentials = errors.New("auth: invalid credentials")

	// ErrDisabledPrincipal reports that an otherwise valid principal is disabled.
	ErrDisabledPrincipal = errors.New("auth: disabled principal")

	// ErrPermissionDenied reports that a valid API key lacks required scope.
	ErrPermissionDenied = errors.New("auth: permission denied")

	// ErrMissingStore reports that a workflow was called without the required
	// storage dependency configured.
	ErrMissingStore = errors.New("auth: missing store")
)
View Source
var (
	// ErrInvalidConfig is returned when a service configuration is unsafe or
	// incomplete.
	ErrInvalidConfig = errors.New("auth: invalid config")
)

Functions

This section is empty.

Types

type APIKey

type APIKey struct {
	ID         string
	Issuer     string
	Prefix     string
	Name       string
	OwnerType  PrincipalType
	OwnerID    string
	Hash       []byte
	Scopes     []string
	CreatedAt  time.Time
	ExpiresAt  *time.Time
	RevokedAt  *time.Time
	LastUsedAt *time.Time
}

APIKey represents stored API key metadata.

Raw key material must never be stored in this type. Store adapters persist Hash for lookup verification and Prefix for efficient key lookup.

func (APIKey) IsActive

func (k APIKey) IsActive(now time.Time) bool

IsActive reports whether the API key is neither expired nor revoked.

func (APIKey) IsExpired

func (k APIKey) IsExpired(now time.Time) bool

IsExpired reports whether the API key has expired at now.

func (APIKey) IsRevoked

func (k APIKey) IsRevoked() bool

IsRevoked reports whether the API key was explicitly revoked.

type APIKeyStore

type APIKeyStore interface {
	// CreateAPIKey stores key metadata. It returns ErrAlreadyExists when the key
	// ID or prefix is already present.
	CreateAPIKey(ctx context.Context, key APIKey) error

	// GetAPIKeyByID returns ErrNotFound when keyID does not exist.
	GetAPIKeyByID(ctx context.Context, keyID string) (APIKey, error)

	// GetAPIKeyByPrefix returns ErrNotFound when prefix does not exist.
	GetAPIKeyByPrefix(ctx context.Context, prefix string) (APIKey, error)

	// ListAPIKeys returns keys for a principal in a stable deterministic order.
	// It returns an empty page when the principal has no keys. Cursor values are
	// opaque and store-defined.
	ListAPIKeys(ctx context.Context, ownerType PrincipalType, ownerID string, page PageRequest) (Page[APIKey], error)

	// RevokeAPIKey returns ErrNotFound when keyID does not exist and
	// ErrInvalidState when the key cannot be revoked.
	RevokeAPIKey(ctx context.Context, keyID string, revokedAt time.Time) error

	// TouchAPIKey records successful use. The core service treats this as
	// best-effort metadata and does not fail verification when it returns an
	// error.
	TouchAPIKey(ctx context.Context, keyID string, usedAt time.Time) error
}

APIKeyStore persists API key metadata and hashed key lookups.

Raw API key values must never be persisted. Store adapters should index by Prefix and store only Hash for verification.

type AtomicAPIKeyAuditStore

type AtomicAPIKeyAuditStore interface {
	// CreateAPIKeyWithAudit stores key metadata and its creation audit event in
	// one atomic operation.
	CreateAPIKeyWithAudit(ctx context.Context, key APIKey, event AuditEvent) error

	// RevokeAPIKeyWithAudit reads and revokes an API key, stores its revocation
	// audit event, and returns the revoked key metadata in one atomic operation.
	//
	// Implementations should populate event API key and principal fields from
	// the key read inside the atomic operation.
	RevokeAPIKeyWithAudit(ctx context.Context, keyID string, revokedAt time.Time, event AuditEvent) (APIKey, error)
}

AtomicAPIKeyAuditStore optionally persists API key mutations and their audit event in one store-owned atomic operation.

Store adapters that can provide transactions should implement this interface when their APIKeyStore and AuditStore data live in the same database.

type AuditEvent

type AuditEvent struct {
	ID            string
	Type          AuditEventType
	ActorID       string
	PrincipalType PrincipalType
	PrincipalID   string
	APIKeyID      string
	Occurred      time.Time
	Metadata      map[string]string
}

AuditEvent is a structured security event.

Metadata must not contain secrets, raw API keys, key hashes, private keys, or sensitive personal data.

type AuditEventType

type AuditEventType string

AuditEventType identifies a security-relevant action or state transition.

const (
	AuditEventAPIKeyCreated            AuditEventType = "api_key.created"
	AuditEventAPIKeyVerified           AuditEventType = "api_key.verified"
	AuditEventAPIKeyVerificationFailed AuditEventType = "api_key.verification_failed"
	AuditEventAPIKeyRevoked            AuditEventType = "api_key.revoked"
)

type AuditStore

type AuditStore interface {
	// RecordAuditEvent stores an audit event. It returns ErrAlreadyExists when
	// the event ID is already present.
	RecordAuditEvent(ctx context.Context, event AuditEvent) error
}

AuditStore records security-relevant events.

type Clock

type Clock interface {
	Now() time.Time
}

Clock supplies time for auth workflows.

It is injectable so tests can avoid sleeps and production code can use the system clock by default.

type Config

type Config struct {
	// Issuer identifies this auth service in generated credentials and audit
	// records. It must be stable within a deployment.
	Issuer string

	// Clock supplies time for token and session lifecycles. The system clock is
	// used when this is nil.
	Clock Clock

	// KeyPrefix is the public prefix used in generated API keys. It may contain
	// ASCII letters, digits, and hyphens.
	KeyPrefix string

	// APIKeyTTL is the default lifetime for generated API keys when callers do
	// not provide an explicit expiration.
	APIKeyTTL time.Duration

	// APIKeyLookupKey is the application-controlled HMAC key used to hash API
	// keys before storage. It is required when APIKeys is configured and must
	// come from secret management, not source code.
	APIKeyLookupKey []byte

	// Principals stores users and groups that can own API keys.
	Principals PrincipalStore

	// APIKeys stores API key metadata and lookup hashes.
	APIKeys APIKeyStore

	// Audit records security-relevant events. Audit writes and last-used updates
	// are best-effort for completed workflows so metadata storage failures do
	// not orphan newly created API keys or deny otherwise valid keys.
	Audit AuditStore
}

Config controls the core authentication service.

type CreateAPIKeyRequest

type CreateAPIKeyRequest struct {
	OwnerType PrincipalType
	OwnerID   string
	Name      string
	Scopes    []string
	ExpiresAt *time.Time
}

CreateAPIKeyRequest contains metadata for a new API key.

type CreateAPIKeyResult

type CreateAPIKeyResult struct {
	APIKey APIKey
	RawKey string
}

CreateAPIKeyResult returns the stored key metadata and the raw API key.

RawKey is shown once. It must not be logged or stored.

type ListAPIKeysRequest

type ListAPIKeysRequest struct {
	OwnerType PrincipalType
	OwnerID   string
	Page      PageRequest
}

ListAPIKeysRequest identifies the principal whose keys should be listed.

type Page

type Page[T any] struct {
	Items      []T
	NextCursor string
}

Page contains one page of items and the cursor for the next page.

NextCursor is empty when there are no more results.

func (Page[T]) HasMore

func (p Page[T]) HasMore() bool

HasMore reports whether another page is available.

type PageRequest

type PageRequest struct {
	Limit  int
	Cursor string
}

PageRequest requests a bounded page of results after Cursor.

Cursor is an opaque value returned by a previous page. Stores define the cursor encoding, but callers must pass it through unchanged.

type Principal

type Principal struct {
	ID         string
	Type       PrincipalType
	Name       string
	CreatedAt  time.Time
	UpdatedAt  time.Time
	DisabledAt *time.Time
}

Principal is a user or group that can own API keys.

func (Principal) IsDisabled

func (p Principal) IsDisabled() bool

IsDisabled reports whether the principal is disabled.

type PrincipalStore

type PrincipalStore interface {
	// GetPrincipal returns ErrNotFound when the principal does not exist.
	GetPrincipal(ctx context.Context, principalType PrincipalType, principalID string) (Principal, error)
}

PrincipalStore persists users and groups that can own API keys.

type PrincipalType

type PrincipalType string

PrincipalType identifies the kind of entity that can own an API key.

const (
	PrincipalTypeUser  PrincipalType = "user"
	PrincipalTypeGroup PrincipalType = "group"
)

type RevokeAPIKeyRequest

type RevokeAPIKeyRequest struct {
	APIKeyID string
}

RevokeAPIKeyRequest identifies an API key to revoke.

type Service

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

Service coordinates authentication workflows.

The service is deliberately database-independent. Future steps will add storage interfaces to Config and implement workflows against those interfaces.

func New

func New(cfg Config) (*Service, error)

New creates a Service with secure defaults for omitted optional settings.

func (*Service) Config

func (s *Service) Config() Config

Config returns the normalized service configuration.

func (*Service) CreateAPIKey

func (s *Service) CreateAPIKey(ctx context.Context, req CreateAPIKeyRequest) (CreateAPIKeyResult, error)

CreateAPIKey creates a new API key for a user or group.

func (*Service) ListAPIKeys

func (s *Service) ListAPIKeys(ctx context.Context, req ListAPIKeysRequest) (Page[APIKey], error)

ListAPIKeys lists API keys for a user or group.

func (*Service) RevokeAPIKey

func (s *Service) RevokeAPIKey(ctx context.Context, req RevokeAPIKeyRequest) error

RevokeAPIKey revokes an API key by ID.

func (*Service) VerifyAPIKey

func (s *Service) VerifyAPIKey(ctx context.Context, req VerifyAPIKeyRequest) (VerifyAPIKeyResult, error)

VerifyAPIKey verifies a raw API key and checks required scopes.

type VerifyAPIKeyRequest

type VerifyAPIKeyRequest struct {
	RawKey         string
	RequiredScopes []string
}

VerifyAPIKeyRequest contains a raw API key and optional required scopes.

type VerifyAPIKeyResult

type VerifyAPIKeyResult struct {
	APIKey    APIKey
	Principal Principal
}

VerifyAPIKeyResult contains the verified key and owning principal.

Directories

Path Synopsis
cmd
testbench command
Package keys provides secure signing and symmetric key generation.
Package keys provides secure signing and symmetric key generation.
Package migrate provides explicit migration planning and execution.
Package migrate provides explicit migration planning and execution.
Package mongodb provides a native MongoDB adapter for auth stores.
Package mongodb provides a native MongoDB adapter for auth stores.
Package mysql provides a native MySQL/MariaDB adapter for auth stores.
Package mysql provides a native MySQL/MariaDB adapter for auth stores.
Package postgres provides a native PostgreSQL adapter for auth stores.
Package postgres provides a native PostgreSQL adapter for auth stores.
Package redis provides Redis migration helpers for auth stores.
Package redis provides Redis migration helpers for auth stores.
Package sqlite provides a SQLite adapter for auth stores.
Package sqlite provides a SQLite adapter for auth stores.
Package token provides secure opaque token generation and lookup hashing.
Package token provides secure opaque token generation and lookup hashing.

Jump to

Keyboard shortcuts

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