cache

package
v0.7.2 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: BSD-2-Clause Imports: 19 Imported by: 0

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrNoDocuments = errors.New("no documents found")

Functions

This section is empty.

Types

type AuthContextStore

type AuthContextStore interface {
	// Save stores an authorization context with sessionID as primary key.
	Save(ctx context.Context, doc *AuthorizationContext) error

	// Create is an alias for Save.
	Create(ctx context.Context, doc *AuthorizationContext) error

	// Get retrieves an authorization context by query fields (sessionID, requestURI, code, state, etc.).
	Get(ctx context.Context, query *AuthorizationContext) (*AuthorizationContext, error)

	// GetByID retrieves an authorization context by session ID.
	GetByID(ctx context.Context, id string) (*AuthorizationContext, error)

	// GetByAuthorizationCode retrieves an authorization context by authorization code.
	GetByAuthorizationCode(ctx context.Context, code string) (*AuthorizationContext, error)

	// GetByAccessToken retrieves an authorization context by access token.
	GetByAccessToken(ctx context.Context, token string) (*AuthorizationContext, error)

	// GetWithAccessToken retrieves an authorization context by access token (legacy method).
	GetWithAccessToken(ctx context.Context, token string) (*AuthorizationContext, error)

	// Update updates an existing authorization context.
	Update(ctx context.Context, doc *AuthorizationContext) error

	// Delete removes an authorization context by session ID.
	Delete(ctx context.Context, id string) error

	// ForfeitAuthorizationCode marks an authorization code as used and returns the updated context.
	ForfeitAuthorizationCode(ctx context.Context, query *AuthorizationContext) (*AuthorizationContext, error)

	// RedeemPreAuthorizedCode allows a pre-authorized code to be redeemed by multiple
	// distinct clients (per DPoP thumbprint). Returns the auth context or an error if
	// the same client attempts to redeem the code twice.
	RedeemPreAuthorizedCode(ctx context.Context, code, dpopThumbprint string) (*AuthorizationContext, error)

	// MarkCodeAsForfeited marks an authorization code as forfeited by session ID.
	MarkCodeAsForfeited(ctx context.Context, id string) error

	// Consent marks an authorization context as consented.
	Consent(ctx context.Context, query *AuthorizationContext) error

	// AddToken adds a token to an authorization context identified by code.
	AddToken(ctx context.Context, code string, token *Token) error

	// GetByRefreshToken retrieves an authorization context by refresh token.
	GetByRefreshToken(ctx context.Context, refreshToken string) (*AuthorizationContext, error)

	// RotateRefreshToken atomically replaces oldToken with the fields in updated,
	// but only if the document still holds oldToken. Returns ErrNoDocuments if the
	// old token was already consumed by a concurrent request, enforcing one-time use.
	RotateRefreshToken(ctx context.Context, oldToken string, updated *AuthorizationContext) error

	// SetAuthenticSource sets the authentic source for an authorization context.
	SetAuthenticSource(ctx context.Context, query *AuthorizationContext, authenticSource string) error

	// SetIdentifier sets the resolved identifier on an authorization context.
	SetIdentifier(ctx context.Context, query *AuthorizationContext, identifier string) error
}

AuthContextStore defines the interface for authorization context storage. Implementations can use in-memory caching, MongoDB, or other backends. This abstraction enables horizontal scaling (HA) by allowing shared storage backends.

type AuthorizationContext

type AuthorizationContext struct {
	// Core session fields
	SessionID string        `json:"session_id" bson:"session_id" validate:"required,max=128,printascii"`
	Status    SessionStatus `json:"status,omitempty" bson:"status,omitempty" validate:"omitempty,max=32,printascii"`
	CreatedAt time.Time     `json:"created_at" bson:"created_at,omitempty"`
	ExpiresAt int64         `json:"expires_at" bson:"expires_at"`

	// SourceSessionID references the parent session from which this session was
	// derived. Used in the pre-authorized code flow where each client redemption
	// creates a new child session that still needs access to the original
	// session's credential documents.
	SourceSessionID string `json:"source_session_id,omitempty" bson:"source_session_id,omitempty" validate:"omitempty,max=128,printascii"`

	// Client and authorization fields
	ClientID            string   `json:"client_id" bson:"client_id" validate:"omitempty,max=128,printascii"`
	WalletClientID      string   `json:"wallet_client_id,omitempty" bson:"wallet_client_id,omitempty" validate:"omitempty,max=128,printascii"`
	Scopes              []string `json:"scopes,omitempty" bson:"scopes,omitempty"`
	State               string   `json:"state,omitempty" bson:"state,omitempty" validate:"omitempty,max=500,printascii"`
	Nonce               string   `json:"nonce,omitempty" bson:"nonce,omitempty" validate:"omitempty,max=128,printascii"`
	CodeChallenge       string   `json:"code_challenge,omitempty" bson:"code_challenge,omitempty" validate:"omitempty,max=128,printascii"`
	CodeChallengeMethod string   `json:"code_challenge_method,omitempty" bson:"code_challenge_method,omitempty" validate:"omitempty,max=16,printascii"`

	// Authorization code fields
	Code      string `json:"code,omitempty" bson:"code,omitempty" validate:"omitempty,max=128,printascii"`
	Forfeited bool   `json:"forfeited,omitempty" bson:"forfeited,omitempty"`

	// RedeemedBy tracks DPoP thumbprints that have redeemed a pre-authorized code.
	// Pre-authorized codes may be redeemed by multiple distinct clients (each
	// identified by a unique DPoP key), but a given client must not redeem
	// the same code twice.
	RedeemedBy []string `json:"redeemed_by,omitempty" bson:"redeemed_by,omitempty"`

	// Token fields
	Token       *Token `json:"token,omitempty" bson:"token,omitempty"`
	AccessToken string `json:"access_token,omitempty" bson:"access_token,omitempty" validate:"omitempty,max=16384,printascii"`
	IDToken     string `json:"id_token,omitempty" bson:"id_token,omitempty" validate:"omitempty,max=32768,printascii"`

	// Issuer-specific fields (credential issuance)
	AuthorizationDetails []openid4vci.AuthorizationDetailsParameter `json:"authorization_details,omitempty" bson:"authorization_details,omitempty"`
	RequestURI           string                                     `json:"request_uri,omitempty" bson:"request_uri,omitempty" validate:"omitempty,max=2048,printascii"`
	WalletURI            string                                     `json:"redirect_url,omitempty" bson:"redirect_url,omitempty" validate:"omitempty,max=2048,printascii"`
	Consent              bool                                       `json:"consent,omitempty" bson:"consent,omitempty"`
	AuthenticSource      string                                     `json:"authentic_source,omitempty" bson:"authentic_source,omitempty" validate:"omitempty,max=128,printascii"`
	Scope                string                                     `json:"scope,omitempty" bson:"scope,omitempty" validate:"omitempty,max=128,printascii"`
	Identifier           string                                     `json:"identifier,omitempty" bson:"identifier,omitempty" validate:"omitempty,max=256,printascii"`
	AuthProvider         string                                     `json:"auth_provider,omitempty" bson:"auth_provider,omitempty" validate:"omitempty,max=32,printascii"`
	DataSource           string                                     `json:"data_source,omitempty" bson:"data_source,omitempty" validate:"omitempty,max=32,printascii"`
	RemoteName           string                                     `json:"remote_name,omitempty" bson:"remote_name,omitempty" validate:"omitempty,max=128,printascii"`

	// Verifier-specific fields (presentation/RP flows)
	RedirectURI           string `json:"redirect_uri,omitempty" bson:"redirect_uri,omitempty" validate:"omitempty,max=2048,printascii"`
	ResponseType          string `json:"response_type,omitempty" bson:"response_type,omitempty" validate:"omitempty,max=32,printascii"`
	ResponseMode          string `json:"response_mode,omitempty" bson:"response_mode,omitempty" validate:"omitempty,max=32,printascii"`
	ShowCredentialDetails bool   `json:"show_credential_details,omitempty" bson:"show_credential_details,omitempty"`
	// WalletFollowsRedirect is set when the user leaves /authorize for a
	// same-device web wallet. ProcessDirectPost then returns redirect_uri so
	// the wallet can send the browser back to the RP. Cross-device flows
	// leave this false; the /authorize page poller performs the redirect.
	WalletFollowsRedirect  bool           `json:"wallet_follows_redirect,omitempty" bson:"wallet_follows_redirect,omitempty"`
	CodeExpiresAt          int64          `json:"code_expires_at,omitempty" bson:"code_expires_at,omitempty"`                 // Unix timestamp
	AccessTokenExpiresAt   int64          `json:"access_token_expires_at,omitempty" bson:"access_token_expires_at,omitempty"` // Unix timestamp
	RefreshToken           string         `json:"refresh_token,omitempty" bson:"refresh_token,omitempty" validate:"omitempty,max=4096,printascii"`
	RefreshTokenExpiresAt  int64          `json:"refresh_token_expires_at,omitempty" bson:"refresh_token_expires_at,omitempty"` // Unix timestamp
	VerifiedClaims         map[string]any `json:"verified_claims,omitempty" bson:"verified_claims,omitempty"`
	VPToken                string         `json:"vp_token,omitempty" bson:"vp_token,omitempty" validate:"omitempty,max=65536,printascii"`
	PresentationSubmission any            `json:"presentation_submission,omitempty" bson:"presentation_submission,omitempty"`

	// OpenID4VP fields (wallet interaction)
	EphemeralEncryptionKeyID string                                 `` /* 129-byte string literal not displayed */
	VerifierResponseCode     string                                 `json:"verifier_response_code,omitempty" bson:"verifier_response_code,omitempty" validate:"omitempty,max=128,printascii"`
	RequestObjectID          string                                 `json:"request_object_id,omitempty" bson:"request_object_id,omitempty" validate:"omitempty,max=128,printascii"`
	RequestObjectNonce       string                                 `json:"request_object_nonce,omitempty" bson:"request_object_nonce,omitempty" validate:"omitempty,max=128,printascii"`
	DCQLQuery                *openid4vp.DCQL                        `json:"dcql_query,omitempty" bson:"dcql_query,omitempty"`
	Validations              map[string][]openid4vp.ClaimValidation `json:"validations,omitempty" bson:"validations,omitempty" validate:"omitempty,dive,dive"`
	WalletID                 string                                 `json:"wallet_id,omitempty" bson:"wallet_id,omitempty" validate:"omitempty,max=128,printascii"`
}

AuthorizationContext is the unified model for OIDC/OpenID4VP sessions It supports both issuer credential issuance flows and verifier presentation/RP flows

func (*AuthorizationContext) Validate

func (a *AuthorizationContext) Validate() error

Validate checks the AuthorizationContext against its struct validation tags.

type Cache

type Cache[V any] interface {
	// Get retrieves a value by key. Returns the value and true if found,
	// or the zero value and false if not found or expired.
	Get(ctx context.Context, key string) (V, bool)

	// Set stores a value with the default TTL configured at creation time.
	Set(ctx context.Context, key string, value V)

	// SetNX stores a value only if the key does not already exist (atomic).
	// Returns true if the value was set, false if the key already existed.
	// Returns an error on backend failures so callers can distinguish
	// "already exists" from operational errors.
	SetNX(ctx context.Context, key string, value V) (bool, error)

	// SetNXWithTTL stores a value only if the key does not already exist (atomic),
	// using a custom TTL instead of the default. Returns true if the value was set,
	// false if the key already existed.
	// If ttl <= 0, implementations MUST fall back to SetNX (default TTL).
	SetNXWithTTL(ctx context.Context, key string, value V, ttl time.Duration) (bool, error)

	// SetWithTTL stores a value with a custom TTL, overriding the default.
	SetWithTTL(ctx context.Context, key string, value V, ttl time.Duration)

	// Delete removes a value by key.
	Delete(ctx context.Context, key string)

	// GetAndDelete atomically retrieves and removes a value by key.
	// Returns the value and true if found, or the zero value and false if not.
	GetAndDelete(ctx context.Context, key string) (V, bool)

	// Len returns the number of items currently in the cache.
	Len() int
}

Cache is a generic key-value cache interface with TTL support. All in-memory caches MUST use this interface to allow swapping backends for HA deployments (e.g. memory → mongo).

V is the value type. Keys are always strings, which covers all current usage across the codebase.

func NewGenericCache

func NewGenericCache[V any](s *Service, ctx context.Context, collection string, ttl time.Duration, opts ...MongoCacheOption[V]) (Cache[V], error)

NewGenericCache creates a Cache[V] backed by the service's backend.

type Logger

type Logger interface {
	Error(err error, msg string, keysAndValues ...any)
}

Logger is a minimal logging interface for operational error reporting. Both *logger.Log and logr.Logger satisfy this interface.

type MemoryCache

type MemoryCache[V any] struct {
	// contains filtered or unexported fields
}

MemoryCache is a generic in-memory cache backed by ttlcache. Suitable for single-instance deployments. For HA, swap with MongoCache.

func NewMemoryCache

func NewMemoryCache[V any](ttl time.Duration) *MemoryCache[V]

NewMemoryCache creates a new in-memory generic cache with the given default TTL.

Example
package main

import (
	"fmt"
	"time"

	"github.com/SUNET/vc/pkg/cache"
)

func main() {
	c := cache.NewMemoryCache[string](5 * time.Minute)
	defer c.Stop()

	fmt.Printf("%T\n", c)
}
Output:
*cache.MemoryCache[string]

func (*MemoryCache[V]) Delete

func (m *MemoryCache[V]) Delete(_ context.Context, key string)

Delete removes a value by key.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/SUNET/vc/pkg/cache"
)

func main() {
	ctx := context.Background()
	c := cache.NewMemoryCache[string](5 * time.Minute)
	defer c.Stop()

	c.Set(ctx, "key", "value")
	fmt.Println("before delete:", c.Len())

	c.Delete(ctx, "key")
	_, found := c.Get(ctx, "key")
	fmt.Println("after delete found:", found)
	fmt.Println("after delete len:", c.Len())
}
Output:
before delete: 1
after delete found: false
after delete len: 0

func (*MemoryCache[V]) Get

func (m *MemoryCache[V]) Get(_ context.Context, key string) (V, bool)

Get retrieves a value by key.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/SUNET/vc/pkg/cache"
)

func main() {
	ctx := context.Background()
	c := cache.NewMemoryCache[int](5 * time.Minute)
	defer c.Stop()

	// Get a key that does not exist
	_, found := c.Get(ctx, "missing")
	fmt.Println("missing key found:", found)

	// Set and get a key
	c.Set(ctx, "count", 42)
	val, found := c.Get(ctx, "count")
	fmt.Println("count found:", found)
	fmt.Println("count value:", val)
}
Output:
missing key found: false
count found: true
count value: 42

func (*MemoryCache[V]) GetAndDelete added in v0.6.1

func (m *MemoryCache[V]) GetAndDelete(_ context.Context, key string) (V, bool)

GetAndDelete atomically retrieves and removes a value by key.

func (*MemoryCache[V]) Len

func (m *MemoryCache[V]) Len() int

Len returns the number of items currently in the cache.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/SUNET/vc/pkg/cache"
)

func main() {
	ctx := context.Background()
	c := cache.NewMemoryCache[string](5 * time.Minute)
	defer c.Stop()

	fmt.Println("empty cache len:", c.Len())

	c.Set(ctx, "a", "alpha")
	c.Set(ctx, "b", "bravo")
	c.Set(ctx, "c", "charlie")
	fmt.Println("after 3 inserts len:", c.Len())
}
Output:
empty cache len: 0
after 3 inserts len: 3

func (*MemoryCache[V]) Set

func (m *MemoryCache[V]) Set(_ context.Context, key string, value V)

Set stores a value with the default TTL.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/SUNET/vc/pkg/cache"
)

func main() {
	ctx := context.Background()
	c := cache.NewMemoryCache[string](5 * time.Minute)
	defer c.Stop()

	c.Set(ctx, "greeting", "hello")

	val, found := c.Get(ctx, "greeting")
	fmt.Println("found:", found)
	fmt.Println("value:", val)
}
Output:
found: true
value: hello

func (*MemoryCache[V]) SetNX added in v0.5.10

func (m *MemoryCache[V]) SetNX(_ context.Context, key string, value V) (bool, error)

SetNX stores a value only if the key does not already exist. Returns true if the value was set, false if the key already existed.

func (*MemoryCache[V]) SetNXWithTTL added in v0.6.4

func (m *MemoryCache[V]) SetNXWithTTL(ctx context.Context, key string, value V, ttl time.Duration) (bool, error)

SetNXWithTTL stores a value only if the key does not already exist, using a custom TTL. Returns true if the value was set, false if the key already existed. If ttl <= 0, falls back to SetNX (default TTL).

func (*MemoryCache[V]) SetWithTTL

func (m *MemoryCache[V]) SetWithTTL(_ context.Context, key string, value V, ttl time.Duration)

SetWithTTL stores a value with a custom TTL.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/SUNET/vc/pkg/cache"
)

func main() {
	ctx := context.Background()
	c := cache.NewMemoryCache[string](5 * time.Minute)
	defer c.Stop()

	// Set a value with a custom TTL
	c.SetWithTTL(ctx, "session", "abc123", 30*time.Second)

	val, found := c.Get(ctx, "session")
	fmt.Println("found:", found)
	fmt.Println("value:", val)
}
Output:
found: true
value: abc123

func (*MemoryCache[V]) Stop

func (m *MemoryCache[V]) Stop()

Stop stops the background expiration goroutine.

type MemoryRateLimitCounter added in v0.7.0

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

MemoryRateLimitCounter is an in-memory sliding-window rate limit counter.

func NewMemoryRateLimitCounter added in v0.7.0

func NewMemoryRateLimitCounter() *MemoryRateLimitCounter

NewMemoryRateLimitCounter creates a new in-memory rate limit counter.

func (*MemoryRateLimitCounter) IncrementWithTTL added in v0.7.0

func (m *MemoryRateLimitCounter) IncrementWithTTL(_ context.Context, key string, window time.Duration) (int64, error)

IncrementWithTTL atomically increments the counter and returns the sliding-window estimate.

type MemoryStore

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

MemoryStore implements authorization context storage using an in-memory ttlcache. Suitable for single-instance deployments.

func NewMemoryStore

func NewMemoryStore(ttl time.Duration) *MemoryStore

NewMemoryStore creates a new in-memory authorization context store.

func (*MemoryStore) AddToken

func (c *MemoryStore) AddToken(ctx context.Context, code string, token *Token) error

AddToken adds a token to an authorization context

func (*MemoryStore) Consent

func (c *MemoryStore) Consent(ctx context.Context, query *AuthorizationContext) error

Consent marks an authorization context as consented

func (*MemoryStore) Create

func (c *MemoryStore) Create(ctx context.Context, doc *AuthorizationContext) error

Create is an alias for Save to match the Session API

func (*MemoryStore) Delete

func (c *MemoryStore) Delete(ctx context.Context, id string) error

Delete removes an authorization context from the cache

func (*MemoryStore) ForfeitAuthorizationCode

func (c *MemoryStore) ForfeitAuthorizationCode(ctx context.Context, query *AuthorizationContext) (*AuthorizationContext, error)

ForfeitAuthorizationCode marks an authorization code as used

func (*MemoryStore) Get

Get retrieves an authorization context by query fields

func (*MemoryStore) GetByAccessToken

func (c *MemoryStore) GetByAccessToken(ctx context.Context, token string) (*AuthorizationContext, error)

GetByAccessToken retrieves an authorization context by access token

func (*MemoryStore) GetByAuthorizationCode

func (c *MemoryStore) GetByAuthorizationCode(ctx context.Context, code string) (*AuthorizationContext, error)

GetByAuthorizationCode retrieves an authorization context by authorization code

func (*MemoryStore) GetByID

func (c *MemoryStore) GetByID(ctx context.Context, id string) (*AuthorizationContext, error)

GetByID retrieves an authorization context by session ID

func (*MemoryStore) GetByRefreshToken added in v0.7.0

func (c *MemoryStore) GetByRefreshToken(ctx context.Context, refreshToken string) (*AuthorizationContext, error)

GetByRefreshToken retrieves an authorization context by refresh token.

func (*MemoryStore) GetWithAccessToken

func (c *MemoryStore) GetWithAccessToken(ctx context.Context, token string) (*AuthorizationContext, error)

GetWithAccessToken retrieves an authorization context by access token

func (*MemoryStore) MarkCodeAsForfeited

func (c *MemoryStore) MarkCodeAsForfeited(ctx context.Context, id string) error

MarkCodeAsForfeited marks an authorization code as forfeited

func (*MemoryStore) RedeemPreAuthorizedCode added in v0.6.1

func (c *MemoryStore) RedeemPreAuthorizedCode(ctx context.Context, code, dpopThumbprint string) (*AuthorizationContext, error)

RedeemPreAuthorizedCode allows a pre-authorized code to be redeemed by multiple distinct clients (identified by DPoP thumbprint). Each client may redeem the code only once. This implements OID4VCI §4.1.1 "single use" on a per-client basis: the code is single-use for each wallet, but the same credential offer can serve multiple wallets.

func (*MemoryStore) RotateRefreshToken added in v0.7.0

func (c *MemoryStore) RotateRefreshToken(ctx context.Context, oldToken string, updated *AuthorizationContext) error

RotateRefreshToken atomically replaces oldToken with the fields in updated, but only if the document still holds oldToken.

func (*MemoryStore) Save

Save stores an authorization context in the cache with sessionID as primary key

func (*MemoryStore) SetAuthenticSource

func (c *MemoryStore) SetAuthenticSource(ctx context.Context, query *AuthorizationContext, authenticSource string) error

SetAuthenticSource sets the authentic source for an authorization context

func (*MemoryStore) SetIdentifier added in v0.5.7

func (c *MemoryStore) SetIdentifier(ctx context.Context, query *AuthorizationContext, identifier string) error

SetIdentifier sets the resolved identifier on an authorization context.

func (*MemoryStore) Update

func (c *MemoryStore) Update(ctx context.Context, doc *AuthorizationContext) error

Update updates an existing authorization context

type MongoCache

type MongoCache[V any] struct {
	// contains filtered or unexported fields
}

MongoCache is a generic cache backed by a MongoDB collection. Values are JSON-encoded before storage, which allows interface types (e.g. jwk.Key) to round-trip correctly. A TTL index on `created_at` provides automatic expiration. Enables HA by sharing state across instances.

V must be serializable by encoding/json. Interface or opaque types whose concrete type cannot be inferred by json.Unmarshal (e.g. jwk.Key) require a custom decoder supplied via WithDecoder.

func NewMongoCache

func NewMongoCache[V any](ctx context.Context, client *mongo.Client, database, collection string, ttl time.Duration, log Logger, opts ...MongoCacheOption[V]) (*MongoCache[V], error)

NewMongoCache creates a new MongoDB-backed generic cache. It creates the necessary indexes including a TTL index for automatic expiration. If log is nil operational errors are silently discarded.

func (*MongoCache[V]) Delete

func (m *MongoCache[V]) Delete(ctx context.Context, key string)

Delete removes a value by key.

func (*MongoCache[V]) Get

func (m *MongoCache[V]) Get(ctx context.Context, key string) (V, bool)

Get retrieves a value by key.

func (*MongoCache[V]) GetAndDelete added in v0.6.1

func (m *MongoCache[V]) GetAndDelete(ctx context.Context, key string) (V, bool)

GetAndDelete atomically retrieves and removes a value by key.

func (*MongoCache[V]) Len

func (m *MongoCache[V]) Len() int

Len returns the estimated number of items in the cache.

func (*MongoCache[V]) Set

func (m *MongoCache[V]) Set(ctx context.Context, key string, value V)

Set stores a value with the default TTL (uses upsert).

func (*MongoCache[V]) SetNX added in v0.5.10

func (m *MongoCache[V]) SetNX(ctx context.Context, key string, value V) (bool, error)

SetNX stores a value only if the key does not already exist (atomic). Returns true if the value was inserted, false if the key already existed. Returns a non-nil error on operational failures (e.g. connectivity issues).

func (*MongoCache[V]) SetNXWithTTL added in v0.6.4

func (m *MongoCache[V]) SetNXWithTTL(ctx context.Context, key string, value V, ttl time.Duration) (bool, error)

SetNXWithTTL stores a value only if the key does not already exist (atomic), using a custom TTL approximated via created_at shifting (same as SetWithTTL). If ttl <= 0, falls back to SetNX (default TTL).

func (*MongoCache[V]) SetWithTTL

func (m *MongoCache[V]) SetWithTTL(ctx context.Context, key string, value V, ttl time.Duration)

SetWithTTL stores a value with a custom TTL. MongoDB TTL indexes are collection-wide, so per-entry TTL is approximated by shifting created_at: the document expires when

now >= created_at + collection_ttl

Setting created_at = now - (collection_ttl - custom_ttl) makes the document expire ~custom_ttl from now.

type MongoCacheOption added in v0.5.10

type MongoCacheOption[V any] func(*MongoCache[V])

MongoCacheOption configures optional behaviour for MongoCache.

func WithDecoder added in v0.5.10

func WithDecoder[V any](fn func([]byte) (V, error)) MongoCacheOption[V]

WithDecoder supplies a custom JSON decoder for V. Use this for interface types (e.g. jwk.Key) where json.Unmarshal cannot infer the concrete type.

type MongoRateLimitCounter added in v0.7.0

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

MongoRateLimitCounter is a MongoDB-backed sliding-window rate limit counter. It uses two fixed sub-windows per key and weights them to approximate a sliding window. The current window is incremented atomically via $inc.

func NewMongoRateLimitCounter added in v0.7.0

func NewMongoRateLimitCounter(ctx context.Context, client *mongo.Client, database, collection string, log Logger) (*MongoRateLimitCounter, error)

NewMongoRateLimitCounter creates a new MongoDB-backed rate limit counter.

func (*MongoRateLimitCounter) IncrementWithTTL added in v0.7.0

func (m *MongoRateLimitCounter) IncrementWithTTL(ctx context.Context, key string, window time.Duration) (int64, error)

IncrementWithTTL atomically increments the current sub-window's counter and returns the sliding-window estimate.

type MongoStore

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

MongoStore implements AuthContextStore using MongoDB as the backend. This enables horizontal scaling (HA) by sharing session state across instances.

func NewMongoStore

func NewMongoStore(ctx context.Context, client *mongo.Client, database, collection string, ttl time.Duration) (*MongoStore, error)

NewMongoStore creates a new MongoDB-backed authorization context store. It sets up the collection with required indexes including a TTL index for automatic expiration.

func (*MongoStore) AddToken

func (s *MongoStore) AddToken(ctx context.Context, code string, token *Token) error

AddToken adds a token to an authorization context identified by code.

func (*MongoStore) Consent

func (s *MongoStore) Consent(ctx context.Context, query *AuthorizationContext) error

Consent marks an authorization context as consented.

func (*MongoStore) Create

func (s *MongoStore) Create(ctx context.Context, doc *AuthorizationContext) error

Create is an alias for Save.

func (*MongoStore) Delete

func (s *MongoStore) Delete(ctx context.Context, id string) error

Delete removes an authorization context by session ID.

func (*MongoStore) ForfeitAuthorizationCode

func (s *MongoStore) ForfeitAuthorizationCode(ctx context.Context, query *AuthorizationContext) (*AuthorizationContext, error)

ForfeitAuthorizationCode marks an authorization code as used.

func (*MongoStore) Get

Get retrieves an authorization context by query fields.

func (*MongoStore) GetByAccessToken

func (s *MongoStore) GetByAccessToken(ctx context.Context, token string) (*AuthorizationContext, error)

GetByAccessToken retrieves an authorization context by access token.

func (*MongoStore) GetByAuthorizationCode

func (s *MongoStore) GetByAuthorizationCode(ctx context.Context, code string) (*AuthorizationContext, error)

GetByAuthorizationCode retrieves an authorization context by authorization code.

func (*MongoStore) GetByID

func (s *MongoStore) GetByID(ctx context.Context, id string) (*AuthorizationContext, error)

GetByID retrieves an authorization context by session ID.

func (*MongoStore) GetByRefreshToken added in v0.7.0

func (s *MongoStore) GetByRefreshToken(ctx context.Context, refreshToken string) (*AuthorizationContext, error)

GetByRefreshToken retrieves an authorization context by refresh token.

func (*MongoStore) GetWithAccessToken

func (s *MongoStore) GetWithAccessToken(ctx context.Context, token string) (*AuthorizationContext, error)

GetWithAccessToken retrieves an authorization context by access token (legacy method).

func (*MongoStore) MarkCodeAsForfeited

func (s *MongoStore) MarkCodeAsForfeited(ctx context.Context, id string) error

MarkCodeAsForfeited marks an authorization code as forfeited by session ID.

func (*MongoStore) RedeemPreAuthorizedCode added in v0.6.1

func (s *MongoStore) RedeemPreAuthorizedCode(ctx context.Context, code, dpopThumbprint string) (*AuthorizationContext, error)

RedeemPreAuthorizedCode allows a pre-authorized code to be redeemed by multiple distinct clients (identified by DPoP thumbprint). Uses an atomic FindOneAndUpdate with $addToSet and a condition that the thumbprint is not already present. The $expr size guard is enforced atomically within the single-document FindOneAndUpdate operation, so the max-redeemer bound is strict.

func (*MongoStore) RotateRefreshToken added in v0.7.0

func (s *MongoStore) RotateRefreshToken(ctx context.Context, oldToken string, updated *AuthorizationContext) error

RotateRefreshToken atomically replaces oldToken with the fields in updated, but only if the document still holds oldToken. Uses a filter on both session_id and refresh_token so a concurrent rotation deterministically fails.

func (*MongoStore) Save

Save stores an authorization context in MongoDB with sessionID as primary key.

func (*MongoStore) SetAuthenticSource

func (s *MongoStore) SetAuthenticSource(ctx context.Context, query *AuthorizationContext, authenticSource string) error

SetAuthenticSource sets the authentic source for an authorization context.

func (*MongoStore) SetIdentifier added in v0.5.7

func (s *MongoStore) SetIdentifier(ctx context.Context, query *AuthorizationContext, identifier string) error

SetIdentifier sets the resolved identifier on an authorization context.

func (*MongoStore) Update

func (s *MongoStore) Update(ctx context.Context, doc *AuthorizationContext) error

Update updates an existing authorization context.

type RateLimitCounter added in v0.7.0

type RateLimitCounter interface {
	// IncrementWithTTL atomically increments the request count for key and
	// returns a sliding-window estimate of the request rate over the given
	// window duration. The estimate is weighted: requests in the previous
	// window are scaled by the fraction of the window that has not yet
	// elapsed, giving smooth rate limiting without boundary bursts.
	// Returns the estimated count and any operational error.
	IncrementWithTTL(ctx context.Context, key string, window time.Duration) (int64, error)
}

RateLimitCounter provides an atomic sliding-window rate limit counter. Implementations must be safe for concurrent use.

type RedisCache added in v0.7.1

type RedisCache[V any] struct {
	// contains filtered or unexported fields
}

RedisCache is a generic cache backed by Redis or any RESP-protocol server compatible with it (e.g. Valkey, the open-source Redis fork maintained under the Linux Foundation) - this implementation only ever issues standard GET/SET/DEL/GETDEL/SCAN/SETNX commands through redis.UniversalClient, none of which are Redis-specific. Values are JSON-encoded before storage (matching MongoCache's approach), which allows interface types (e.g. jwk.Key) to round-trip correctly via a custom decoder. Unlike MongoCache, which approximates per-entry TTL by shifting created_at under a collection-wide TTL index, Redis has native per-key expiration (EXPIRE), so every TTL here is exact, not approximated.

Keys are namespaced by collection ("<collection>:<key>") so multiple caches can safely share one Redis database/keyspace.

func NewRedisCache added in v0.7.1

func NewRedisCache[V any](client redis.UniversalClient, collection string, ttl time.Duration, log Logger, opts ...RedisCacheOption[V]) (*RedisCache[V], error)

NewRedisCache creates a new Redis-backed generic cache. client may be a *redis.Client (single node) or *redis.ClusterClient (cluster mode) - both satisfy redis.UniversalClient, so callers can switch topology without any change here. If log is nil, operational errors are silently discarded.

func (*RedisCache[V]) Delete added in v0.7.1

func (r *RedisCache[V]) Delete(ctx context.Context, key string)

Delete removes a value by key.

func (*RedisCache[V]) Get added in v0.7.1

func (r *RedisCache[V]) Get(ctx context.Context, key string) (V, bool)

Get retrieves a value by key.

func (*RedisCache[V]) GetAndDelete added in v0.7.1

func (r *RedisCache[V]) GetAndDelete(ctx context.Context, key string) (V, bool)

GetAndDelete atomically retrieves and removes a value by key.

func (*RedisCache[V]) Len added in v0.7.1

func (r *RedisCache[V]) Len() int

Len returns the number of items currently in this cache. Implemented via SCAN (not KEYS, which blocks the server on large keyspaces) with a MATCH pattern scoped to this cache's collection prefix - an O(n) walk over this cache's own keys, not the whole Redis keyspace. Like MongoCache.Len (EstimatedDocumentCount), this is an approximate, informational count, not a value to build correctness on. On any scan error, returns 0 rather than a partial count - matching MongoCache.Len, and avoiding misleading a caller with an undercount that looks like a real (if approximate) answer.

On a *redis.ClusterClient, a single SCAN only walks the node it's sent to, not the whole cluster - keys live on whichever shard they hash to. Fan out via ForEachMaster and sum, so this stays a whole-cache count under either topology.

func (*RedisCache[V]) Set added in v0.7.1

func (r *RedisCache[V]) Set(ctx context.Context, key string, value V)

Set stores a value with the default TTL configured at creation time.

func (*RedisCache[V]) SetNX added in v0.7.1

func (r *RedisCache[V]) SetNX(ctx context.Context, key string, value V) (bool, error)

SetNX stores a value only if the key does not already exist (atomic). Returns true if the value was set, false if the key already existed.

func (*RedisCache[V]) SetNXWithTTL added in v0.7.1

func (r *RedisCache[V]) SetNXWithTTL(ctx context.Context, key string, value V, ttl time.Duration) (bool, error)

SetNXWithTTL stores a value only if the key does not already exist (atomic), using a custom TTL instead of the default. If ttl <= 0, falls back to SetNX (default TTL).

func (*RedisCache[V]) SetWithTTL added in v0.7.1

func (r *RedisCache[V]) SetWithTTL(ctx context.Context, key string, value V, ttl time.Duration)

SetWithTTL stores a value with a custom TTL, overriding the default.

type RedisCacheOption added in v0.7.1

type RedisCacheOption[V any] func(*RedisCache[V])

RedisCacheOption configures optional behaviour for RedisCache.

func WithRedisDecoder added in v0.7.1

func WithRedisDecoder[V any](fn func([]byte) (V, error)) RedisCacheOption[V]

WithRedisDecoder supplies a custom JSON decoder for V - use this for interface types (e.g. jwk.Key) where json.Unmarshal cannot infer the concrete type. Mirrors generic_mongo.go's WithDecoder.

type RedisRateLimitCounter added in v0.7.1

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

RedisRateLimitCounter is a Redis-backed sliding-window rate limit counter. It also works unmodified against Valkey and other RESP-compatible servers, since its only backend-specific operation - incrWithTTLScript - is a plain Lua script run via EVAL, a standard RESP command Valkey executes identically. It mirrors MongoRateLimitCounter's algorithm exactly (two fixed sub-windows per key, weighted to approximate a sliding window), but the current sub-window's counter is incremented via Redis's native atomic INCR rather than a MongoDB FindOneAndUpdate upsert - the "insert or update" case Mongo needs an upsert for is simply INCR's normal behavior on a nonexistent key.

func NewRedisRateLimitCounter added in v0.7.1

func NewRedisRateLimitCounter(client redis.UniversalClient, prefix string, log Logger) (*RedisRateLimitCounter, error)

NewRedisRateLimitCounter creates a new Redis-backed rate limit counter. prefix namespaces every key this counter writes ("<prefix>:<key>:<windowID>") so it can't collide with unrelated keys in a shared Redis keyspace/instance - Mongo gets this isolation for free from its collection; Redis has no such boundary, so RedisRateLimitCounter needs one explicitly, same as RedisCache's collection.

func (*RedisRateLimitCounter) IncrementWithTTL added in v0.7.1

func (r *RedisRateLimitCounter) IncrementWithTTL(ctx context.Context, key string, window time.Duration) (int64, error)

IncrementWithTTL atomically increments the current sub-window's counter and returns the sliding-window estimate.

type Service

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

Service manages cache creation. When HA is enabled, caches are backed by MongoDB using the provided client; otherwise in-memory.

func New

func New(ha bool, databaseName string, client *mongo.Client, log Logger) *Service

New creates a cache Service. When ha is true the supplied mongo client is used for all caches; otherwise every cache is in-memory. The caller owns the client lifecycle. databaseName is the MongoDB database name to use for all caches. If log is nil operational errors from mongo-backed caches are silently discarded.

func (*Service) NewAuthContextCache

func (s *Service) NewAuthContextCache(ctx context.Context, collection string, ttl time.Duration) (AuthContextStore, error)

NewAuthContextCache creates an AuthContextStore backed by the service's backend.

func (*Service) NewRateLimitCounter added in v0.7.0

func (s *Service) NewRateLimitCounter(ctx context.Context, collection string) (RateLimitCounter, error)

NewRateLimitCounter creates a RateLimitCounter backed by the service's backend.

type SessionStatus

type SessionStatus string

SessionStatus represents the status of an OIDC session

const (
	SessionStatusPending              SessionStatus = "pending"
	SessionStatusAwaitingPresentation SessionStatus = "awaiting_presentation"
	SessionStatusCodeIssued           SessionStatus = "code_issued"
	SessionStatusTokenIssued          SessionStatus = "token_issued"
	SessionStatusCompleted            SessionStatus = "completed"
	SessionStatusExpired              SessionStatus = "expired"
	SessionStatusError                SessionStatus = "error"

	// MaxPreAuthRedeemers is the maximum number of distinct clients that can
	// redeem a single pre-authorized code. This prevents unbounded growth of
	// the RedeemedBy array and child sessions if a code leaks.
	MaxPreAuthRedeemers = 10
)

type SharedSecrets

type SharedSecrets struct {
	// ServiceName is the owning service (e.g. "apigw", "verifier"). Used as _id.
	ServiceName string `bson:"_id"`
	// SessionAuthKey is the HMAC authentication key for session cookies.
	SessionAuthKey string `bson:"session_auth_key"`
	// SessionEncKey is the AES encryption key for session cookies.
	SessionEncKey string `bson:"session_enc_key"`
}

SharedSecrets holds session keys that must be identical across all instances of a service in HA mode.

func EnsureSharedSecrets

func EnsureSharedSecrets(ctx context.Context, s *Service, serviceName string) (*SharedSecrets, error)

EnsureSharedSecrets returns session keys that are guaranteed to be identical across every instance that calls this function with the same serviceName.

When the Service is in HA mode it generates candidate keys and atomically inserts them into MongoDB using FindOneAndUpdate with $setOnInsert + upsert. If another instance races and inserts first, MongoDB returns that existing document instead — no conflicts.

When HA is disabled it simply generates ephemeral keys.

type Token

type Token struct {
	AccessToken    string `json:"access_token" bson:"access_token" validate:"required,max=4096,printascii"`
	ExpiresAt      int64  `json:"expires_at" bson:"expires_at" validate:"required"`
	DPoPThumbprint string `json:"dpop_thumbprint,omitempty" bson:"dpop_thumbprint,omitempty" validate:"omitempty,max=128,printascii"`
}

Token represents an access token with expiration

Jump to

Keyboard shortcuts

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