bff

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Mar 8, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package bff implements the Backend for Frontend (BFF) pattern for secure session management. The BFF holds tokens server-side and uses HTTP-only cookies to identify browser sessions.

Index

Constants

View Source
const (
	// ContextKeySession is the context key for the session.
	ContextKeySession contextKey = "bff_session"
)
View Source
const SessionIDLength = 32

SessionIDLength is the length of generated session IDs in bytes.

Variables

View Source
var (
	// ErrSessionNotFound is returned when a session is not found.
	ErrSessionNotFound = errors.New("session not found")
	// ErrSessionExpired is returned when a session has expired.
	ErrSessionExpired = errors.New("session expired")
	// ErrInvalidSession is returned when a session is invalid.
	ErrInvalidSession = errors.New("invalid session")
)
View Source
var ErrNoDPoPKeyPair = errors.New("session has no DPoP key pair")

ErrNoDPoPKeyPair is returned when the session has no DPoP key pair.

View Source
var ErrRefreshFailed = errors.New("token refresh failed")

ErrRefreshFailed is returned when token refresh fails.

View Source
var ErrRefreshTokenExpired = errors.New("refresh token expired")

ErrRefreshTokenExpired is returned when the refresh token has expired.

View Source
var ErrTokenEndpointRequired = errors.New("token endpoint required")

ErrTokenEndpointRequired is returned when the token endpoint is not configured.

Functions

func APIProxyMiddleware

func APIProxyMiddleware(targetURL string) (func(http.Handler) http.Handler, error)

APIProxyMiddleware creates middleware that proxies all requests to an API backend. This is useful for wrapping the proxy handler with session middleware.

func AutoRefreshMiddleware

func AutoRefreshMiddleware(config RefreshConfig) (func(http.Handler) http.Handler, error)

AutoRefreshMiddleware creates middleware that automatically refreshes tokens before expiry.

func GenerateSessionID

func GenerateSessionID() (string, error)

GenerateSessionID generates a cryptographically secure session ID.

func OptionalSessionMiddleware

func OptionalSessionMiddleware(store Store, cookieManager *CookieManager) func(http.Handler) http.Handler

OptionalSessionMiddleware creates middleware that loads sessions if present. Requests without sessions are allowed through.

func OriginMiddleware

func OriginMiddleware(allowedOrigins ...string) func(http.Handler) http.Handler

OriginMiddleware creates origin validation middleware with the given allowed origins. This is a convenience function for simple use cases.

func RefreshHandler

func RefreshHandler(config RefreshConfig) http.Handler

RefreshHandler returns an HTTP handler for explicit token refresh requests. This can be used by the frontend to proactively refresh tokens.

func RequireSessionMiddleware

func RequireSessionMiddleware(store Store, cookieManager *CookieManager) func(http.Handler) http.Handler

RequireSessionMiddleware creates middleware that requires a valid session. This is a convenience function that wraps SessionMiddleware.

func SessionMiddleware

func SessionMiddleware(config MiddlewareConfig) func(http.Handler) http.Handler

SessionMiddleware creates middleware that loads sessions from cookies.

func SimpleProxy

func SimpleProxy(targetURL string) (http.Handler, error)

SimpleProxy creates a simple proxy handler with default configuration.

Types

type CookieConfig

type CookieConfig struct {
	// Name is the cookie name. Default: "cf_session".
	Name string

	// Domain is the cookie domain. If empty, uses the request host.
	Domain string

	// Path is the cookie path. Default: "/".
	Path string

	// MaxAge is the cookie max age in seconds. Default: 0 (session cookie).
	// Set to -1 to delete the cookie.
	MaxAge int

	// Secure indicates the cookie should only be sent over HTTPS.
	// Default: true.
	Secure bool

	// HTTPOnly prevents JavaScript access to the cookie.
	// Default: true (required for security).
	HTTPOnly bool

	// SameSite controls cross-site cookie behavior.
	// Default: SameSiteStrictMode.
	SameSite http.SameSite
}

CookieConfig contains configuration for session cookies.

func DefaultCookieConfig

func DefaultCookieConfig() CookieConfig

DefaultCookieConfig returns secure default cookie configuration.

type CookieManager

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

CookieManager handles session cookie operations.

func NewCookieManager

func NewCookieManager(config CookieConfig) *CookieManager

NewCookieManager creates a new cookie manager with the given configuration.

func (*CookieManager) ClearSessionCookie

func (m *CookieManager) ClearSessionCookie(w http.ResponseWriter)

ClearSessionCookie removes the session cookie from the response.

func (*CookieManager) Config

func (m *CookieManager) Config() CookieConfig

Config returns the cookie configuration.

func (*CookieManager) GetSessionID

func (m *CookieManager) GetSessionID(r *http.Request) string

GetSessionID extracts the session ID from the request cookie. Returns empty string if the cookie is not present.

func (*CookieManager) SetSessionCookie

func (m *CookieManager) SetSessionCookie(w http.ResponseWriter, sessionID string, expiry time.Time)

SetSessionCookie creates and sets a session cookie on the response.

type MemoryStore

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

MemoryStore is an in-memory session store for development and testing. It is thread-safe and supports automatic cleanup of expired sessions.

func NewMemoryStore

func NewMemoryStore(config StoreConfig) *MemoryStore

NewMemoryStore creates a new in-memory session store.

func (*MemoryStore) Cleanup

func (s *MemoryStore) Cleanup(ctx context.Context) (int, error)

Cleanup removes expired sessions.

func (*MemoryStore) Close

func (s *MemoryStore) Close() error

Close stops the cleanup goroutine and releases resources.

func (*MemoryStore) Count

func (s *MemoryStore) Count() int

Count returns the current number of sessions (for testing).

func (*MemoryStore) Create

func (s *MemoryStore) Create(ctx context.Context, session *Session) error

Create stores a new session.

func (*MemoryStore) Delete

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

Delete removes a session by ID.

func (*MemoryStore) DeleteByUserID

func (s *MemoryStore) DeleteByUserID(ctx context.Context, userID string) (int, error)

DeleteByUserID removes all sessions for a user.

func (*MemoryStore) Get

func (s *MemoryStore) Get(ctx context.Context, id string) (*Session, error)

Get retrieves a session by ID.

func (*MemoryStore) Touch

func (s *MemoryStore) Touch(ctx context.Context, id string) error

Touch updates the LastAccessedAt timestamp.

func (*MemoryStore) Update

func (s *MemoryStore) Update(ctx context.Context, session *Session) error

Update updates an existing session.

type MiddlewareConfig

type MiddlewareConfig struct {
	// Store is the session store.
	Store Store

	// CookieManager handles session cookies.
	CookieManager *CookieManager

	// RefreshThreshold is how long before access token expiry to trigger refresh.
	// Default: 5 minutes.
	RefreshThreshold time.Duration

	// OnSessionLoad is called after a session is loaded.
	// Can be used for logging or session modification.
	OnSessionLoad func(ctx context.Context, session *Session) error

	// OnSessionExpired is called when a session is expired.
	OnSessionExpired func(w http.ResponseWriter, r *http.Request)

	// OnSessionInvalid is called when a session is invalid.
	OnSessionInvalid func(w http.ResponseWriter, r *http.Request)

	// OnNoSession is called when no session is found and RequireSession is true.
	OnNoSession func(w http.ResponseWriter, r *http.Request)

	// RequireSession when true rejects requests without valid sessions.
	RequireSession bool

	// TouchOnAccess when true updates LastAccessedAt on each request.
	TouchOnAccess bool
}

MiddlewareConfig contains configuration for the BFF middleware.

type OriginConfig

type OriginConfig struct {
	// AllowedOrigins is a list of allowed origins.
	// Origins should be in the format "https://example.com" (no trailing slash).
	AllowedOrigins []string

	// AllowedHosts is a list of allowed hosts (without scheme).
	// This is an alternative to AllowedOrigins for simpler configuration.
	AllowedHosts []string

	// OnError is called when origin validation fails.
	// If nil, returns 403 Forbidden.
	OnError func(w http.ResponseWriter, r *http.Request)

	// AllowMissingOrigin allows requests without Origin header.
	// Default: false (more secure).
	AllowMissingOrigin bool

	// CheckReferer uses Referer header as fallback when Origin is missing.
	// Default: true.
	CheckReferer bool

	// SkipMethods is a list of HTTP methods to skip origin validation.
	// Typically safe methods (GET, HEAD, OPTIONS) can be skipped.
	// Default: none (validate all methods).
	SkipMethods []string
}

OriginConfig contains configuration for origin validation.

func DefaultOriginConfig

func DefaultOriginConfig() OriginConfig

DefaultOriginConfig returns default origin validation configuration.

type OriginValidator

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

OriginValidator validates request origins.

func NewOriginValidator

func NewOriginValidator(config OriginConfig) *OriginValidator

NewOriginValidator creates a new origin validator.

func (*OriginValidator) Middleware

func (v *OriginValidator) Middleware() func(http.Handler) http.Handler

Middleware returns HTTP middleware that validates request origins.

func (*OriginValidator) ValidateRequest

func (v *OriginValidator) ValidateRequest(r *http.Request) bool

ValidateRequest validates the request origin.

type Proxy

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

Proxy proxies requests to an API backend with session-based authentication.

func NewProxy

func NewProxy(config ProxyConfig) (*Proxy, error)

NewProxy creates a new API proxy.

func (*Proxy) Handler

func (p *Proxy) Handler() http.Handler

Handler returns an HTTP handler that proxies requests.

func (*Proxy) ProxyRequest

func (p *Proxy) ProxyRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error)

ProxyRequest proxies a single request (for non-streaming use cases).

type ProxyConfig

type ProxyConfig struct {
	// TargetURL is the base URL of the API backend.
	TargetURL string

	// UseDPoP enables DPoP proof injection.
	// Default: true
	UseDPoP bool

	// Client is the HTTP client to use for proxied requests.
	// If nil, uses http.DefaultClient.
	Client *http.Client

	// Timeout is the request timeout.
	// Default: 30 seconds.
	Timeout time.Duration

	// OnError is called when a proxy error occurs.
	// If nil, returns 502 Bad Gateway.
	OnError func(w http.ResponseWriter, r *http.Request, err error)

	// OnRequestRewrite allows modifying the proxied request before sending.
	OnRequestRewrite func(r *http.Request, session *Session)

	// PathRewrite allows modifying the request path.
	// The function receives the original path and returns the rewritten path.
	PathRewrite func(path string) string

	// StripPrefix removes a prefix from the request path before proxying.
	StripPrefix string

	// HeadersToForward specifies which request headers to forward.
	// If empty, forwards all headers except hop-by-hop headers.
	HeadersToForward []string

	// HeadersToRemove specifies headers to remove from the proxied request.
	HeadersToRemove []string

	// ResponseHeadersToRemove specifies headers to remove from the response.
	ResponseHeadersToRemove []string
}

ProxyConfig contains configuration for the API proxy.

func DefaultProxyConfig

func DefaultProxyConfig() ProxyConfig

DefaultProxyConfig returns default proxy configuration.

type RefreshConfig

type RefreshConfig struct {
	// Store is the session store.
	Store Store

	// CookieManager handles session cookies.
	CookieManager *CookieManager

	// TokenEndpoint is the OAuth token endpoint URL.
	TokenEndpoint string

	// ClientID is the OAuth client ID.
	ClientID string

	// ClientSecret is the OAuth client secret (optional for public clients).
	ClientSecret string //nolint:gosec // G117: config field, not a hardcoded secret

	// UseDPoP enables DPoP for token refresh requests.
	// When true, generates a new DPoP key pair and binds the new tokens to it.
	// Default: true.
	UseDPoP bool

	// Client is the HTTP client to use for refresh requests.
	// If nil, uses http.DefaultClient.
	Client *http.Client

	// Timeout is the refresh request timeout.
	// Default: 30 seconds.
	Timeout time.Duration

	// RefreshThreshold is how early before expiry to refresh tokens.
	// Default: 5 minutes.
	RefreshThreshold time.Duration

	// OnRefreshSuccess is called when tokens are successfully refreshed.
	OnRefreshSuccess func(ctx context.Context, session *Session)

	// OnRefreshError is called when token refresh fails.
	OnRefreshError func(w http.ResponseWriter, r *http.Request, err error)

	// ParseTokenResponse allows custom parsing of the token response.
	// If nil, uses the default OAuth2 token response format.
	ParseTokenResponse func(body []byte) (*TokenResponse, error)
}

RefreshConfig contains configuration for the token refresh handler.

func DefaultRefreshConfig

func DefaultRefreshConfig() RefreshConfig

DefaultRefreshConfig returns default refresh configuration.

type Refresher

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

Refresher handles automatic token refresh.

func NewRefresher

func NewRefresher(config RefreshConfig) (*Refresher, error)

NewRefresher creates a new token refresher.

func (*Refresher) Middleware

func (r *Refresher) Middleware() func(http.Handler) http.Handler

Middleware returns HTTP middleware that automatically refreshes expired access tokens.

func (*Refresher) RefreshSession

func (r *Refresher) RefreshSession(ctx context.Context, session *Session) error

RefreshSession refreshes the tokens for a session.

type Session

type Session struct {
	// ID is the unique session identifier.
	ID string `json:"id"`

	// UserID is the authenticated user's ID.
	UserID uuid.UUID `json:"user_id"`

	// OrganizationID is the current organization context (optional).
	OrganizationID *uuid.UUID `json:"organization_id,omitempty"`

	// AccessToken is the OAuth access token.
	AccessToken string `json:"access_token"`

	// RefreshToken is the OAuth refresh token.
	RefreshToken string `json:"refresh_token"`

	// AccessTokenExpiresAt is when the access token expires.
	AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`

	// RefreshTokenExpiresAt is when the refresh token expires.
	RefreshTokenExpiresAt time.Time `json:"refresh_token_expires_at"`

	// DPoPKeyPairJSON is the serialized DPoP key pair for this session.
	// The BFF uses this to sign DPoP proofs when proxying requests to the API.
	DPoPKeyPairJSON []byte `json:"dpop_key_pair,omitempty"`

	// DPoPThumbprint is the JWK thumbprint of the DPoP key pair.
	DPoPThumbprint string `json:"dpop_thumbprint,omitempty"`

	// Metadata contains optional session metadata.
	Metadata map[string]string `json:"metadata,omitempty"`

	// CreatedAt is when the session was created.
	CreatedAt time.Time `json:"created_at"`

	// UpdatedAt is when the session was last updated.
	UpdatedAt time.Time `json:"updated_at"`

	// LastAccessedAt is when the session was last accessed.
	LastAccessedAt time.Time `json:"last_accessed_at"`

	// ExpiresAt is when the session expires (based on refresh token or absolute timeout).
	ExpiresAt time.Time `json:"expires_at"`

	// IPAddress is the IP address that created the session.
	IPAddress string `json:"ip_address,omitempty"`

	// UserAgent is the user agent that created the session.
	UserAgent string `json:"user_agent,omitempty"`
}

Session represents a server-side session with stored tokens and DPoP keys.

func GetSession

func GetSession(ctx context.Context) *Session

GetSession retrieves the session from the request context.

func NewSession

func NewSession(userID uuid.UUID, accessToken, refreshToken string, accessExpiry, refreshExpiry time.Duration) (*Session, error)

NewSession creates a new session with the given parameters.

func (*Session) GetDPoPKeyPair

func (s *Session) GetDPoPKeyPair() (*dpop.KeyPair, error)

GetDPoPKeyPair deserializes and returns the session's DPoP key pair.

func (*Session) HasDPoP

func (s *Session) HasDPoP() bool

HasDPoP returns true if the session has a DPoP key pair.

func (*Session) IsAccessTokenExpired

func (s *Session) IsAccessTokenExpired() bool

IsAccessTokenExpired returns true if the access token has expired.

func (*Session) IsExpired

func (s *Session) IsExpired() bool

IsExpired returns true if the session has expired.

func (*Session) IsRefreshTokenExpired

func (s *Session) IsRefreshTokenExpired() bool

IsRefreshTokenExpired returns true if the refresh token has expired.

func (*Session) NeedsRefresh

func (s *Session) NeedsRefresh(threshold time.Duration) bool

NeedsRefresh returns true if the access token needs to be refreshed. This returns true if the access token is expired or will expire soon.

func (*Session) SetDPoPKeyPair

func (s *Session) SetDPoPKeyPair(kp *dpop.KeyPair) error

SetDPoPKeyPair stores the DPoP key pair in the session.

type Store

type Store interface {
	// Create stores a new session and returns the session ID.
	Create(ctx context.Context, session *Session) error

	// Get retrieves a session by ID.
	// Returns ErrSessionNotFound if the session doesn't exist.
	// Returns ErrSessionExpired if the session has expired.
	Get(ctx context.Context, id string) (*Session, error)

	// Update updates an existing session.
	// Returns ErrSessionNotFound if the session doesn't exist.
	Update(ctx context.Context, session *Session) error

	// Delete removes a session by ID.
	// Returns ErrSessionNotFound if the session doesn't exist.
	Delete(ctx context.Context, id string) error

	// DeleteByUserID removes all sessions for a user.
	// Returns the number of sessions deleted.
	DeleteByUserID(ctx context.Context, userID string) (int, error)

	// Touch updates the LastAccessedAt timestamp.
	// This is used to track session activity without modifying other fields.
	Touch(ctx context.Context, id string) error

	// Cleanup removes expired sessions.
	// Returns the number of sessions removed.
	Cleanup(ctx context.Context) (int, error)

	// Close closes the store and releases any resources.
	Close() error
}

Store defines the interface for session storage. Implementations must be safe for concurrent use.

type StoreConfig

type StoreConfig struct {
	// CleanupInterval is how often to run automatic cleanup.
	// Set to 0 to disable automatic cleanup.
	CleanupInterval int

	// MaxSessions is the maximum number of sessions to store (0 = unlimited).
	MaxSessions int

	// EncryptionKey is used to encrypt sensitive session data.
	// If nil, session data is stored unencrypted.
	EncryptionKey []byte
}

StoreConfig contains common configuration for session stores.

func DefaultStoreConfig

func DefaultStoreConfig() StoreConfig

DefaultStoreConfig returns sensible default configuration.

type TokenErrorResponse

type TokenErrorResponse struct {
	Error            string `json:"error"`
	ErrorDescription string `json:"error_description,omitempty"`
}

TokenErrorResponse represents an OAuth2 error response.

type TokenResponse

type TokenResponse struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int    `json:"expires_in"`
	RefreshToken string `json:"refresh_token,omitempty"`
	Scope        string `json:"scope,omitempty"`
}

TokenResponse represents the OAuth2 token response.

Jump to

Keyboard shortcuts

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