tokenbroker

package
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Jun 16, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package tokenbroker is the policy point for issuing Parsec / Centrifugo connection tokens. A subscriber asks the broker for a token scoped to a list of channels; the broker consults a pluggable Authorizer to decide which channels the requesting principal is allowed to access, then mints a JWT that authorizes the granted subset.

The broker is built on top of the auth.Issuer primitive — it does NOT reimplement signing. What it adds is:

  • An HTTP surface (/parsec/token, /parsec/revoke, /parsec/token/delegate) that user-facing clients hit before opening their websocket.
  • An Authorizer interface so each Parsec deployment can plug its own ACL model (per-team, role-based, public-everything-private- to-tenant, etc.).
  • A revocation list so a mis-issued token can be invalidated before its natural expiry.
  • Delegated issuance so a backend service can mint a token "on behalf of" a user, with both identities captured in the audit log.

The broker depends on but does not own the schema registry — channel validity is the registry's concern, and channel-level ACL is the Authorizer's concern. Token signing belongs to auth.

Index

Constants

This section is empty.

Variables

View Source
var ErrUnauthenticated = errors.New("tokenbroker: unauthenticated")

ErrUnauthenticated is returned by Authenticator when the bearer cannot be resolved to a UserID.

Functions

This section is empty.

Types

type AuditEntry

type AuditEntry struct {
	TokenID    string
	IssuedTo   UserID
	Delegator  UserID // service identity for delegated issuance; empty otherwise
	Channels   []string
	ExpiresAt  time.Time
	IssuedAt   time.Time
	RemoteAddr string
}

AuditEntry captures the identity, channels, and lifetime of an issued token. Both the requester and the on-behalf-of identity are recorded for delegated issuance so audit trails capture both.

type AuditLogger

type AuditLogger interface {
	Audit(ctx context.Context, entry AuditEntry)
}

AuditLogger receives one structured record per issued token. Pure observation — no return value affects issuance.

type AuditLoggerFunc

type AuditLoggerFunc func(ctx context.Context, entry AuditEntry)

AuditLoggerFunc is the function form of AuditLogger.

func (AuditLoggerFunc) Audit

func (f AuditLoggerFunc) Audit(ctx context.Context, e AuditEntry)

Audit implements AuditLogger.

type AuthDecision

type AuthDecision struct {
	Granted []string        `json:"granted"`
	Denied  []DeniedChannel `json:"denied,omitempty"`
}

AuthDecision is the Authorizer's answer.

type Authenticator

type Authenticator interface {
	Authenticate(ctx context.Context, bearer string) (UserID, error)
}

Authenticator resolves the caller's identity from a request bearer. The broker delegates user authentication entirely — Parsec deployments already have an auth system; the broker just consumes its tokens.

type AuthenticatorFunc

type AuthenticatorFunc func(ctx context.Context, bearer string) (UserID, error)

AuthenticatorFunc is the function form of Authenticator.

func (AuthenticatorFunc) Authenticate

func (f AuthenticatorFunc) Authenticate(ctx context.Context, bearer string) (UserID, error)

Authenticate implements Authenticator.

type Authorizer

type Authorizer interface {
	Authorize(ctx context.Context, user UserID, channels []string) AuthDecision
}

Authorizer decides which channels a user may access. The broker calls Authorize on each /parsec/token request.

var AllowAll Authorizer = AuthorizerFunc(func(_ context.Context, _ UserID, ch []string) AuthDecision {
	out := make([]string, len(ch))
	copy(out, ch)
	return AuthDecision{Granted: out}
})

AllowAll is the trivial authorizer that grants every requested channel. Useful in dev / single-tenant deployments; production deployments should plug in their own.

type AuthorizerFunc

type AuthorizerFunc func(ctx context.Context, user UserID, channels []string) AuthDecision

AuthorizerFunc is the function form of Authorizer.

func (AuthorizerFunc) Authorize

func (f AuthorizerFunc) Authorize(ctx context.Context, user UserID, channels []string) AuthDecision

Authorize implements Authorizer.

type Broker

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

Broker is the running token broker.

func New

func New(opts Options) (*Broker, error)

New constructs a Broker. opts.Issuer, opts.Authorizer, and opts.Authenticator are required; opts.Revocations defaults to MemoryRevocations.

func (*Broker) Delegate

func (b *Broker) Delegate(ctx context.Context, bearer string, req DelegateRequest) (IssueResponse, error)

Delegate mints a token on behalf of req.OnBehalfOf. The bearer is the service's token. The DelegateAuthorizer (if configured) decides whether the service may act for the target user.

func (*Broker) Handler

func (b *Broker) Handler() http.Handler

Handler returns an http.Handler that mounts the broker's three routes. The handler is path-prefix agnostic — mount it under whatever prefix the deployment prefers (the canonical mount is "/parsec").

Routes (relative to the mount):

POST /token            — IssueRequest -> IssueResponse
POST /token/delegate   — DelegateRequest -> IssueResponse
POST /revoke           — RevokeRequest -> { "ok": true }

All routes require Authorization: Bearer <user-token>. The bearer is passed verbatim to the Authenticator; the broker itself does no signature verification.

func (*Broker) IsRevoked

func (b *Broker) IsRevoked(ctx context.Context, tokenID, userID string, issuedAt time.Time) (bool, error)

IsRevoked is the Centrifugo-side check hook: the connect/subscribe pipeline can consult this when a token presents itself. Token-level revocation is checked by ID; user-level revocation is checked against the issuedAt timestamp.

func (*Broker) Issue

func (b *Broker) Issue(ctx context.Context, bearer string, req IssueRequest) (IssueResponse, error)

Issue is the programmatic entry point — invoked by the HTTP handler for /parsec/token. The bearer is the requester's user-auth token; the broker resolves it to a UserID via the Authenticator.

func (*Broker) Revoke

func (b *Broker) Revoke(ctx context.Context, bearer string, req RevokeRequest) error

Revoke marks a token (or all of a user's tokens) invalid.

type DelegateRequest

type DelegateRequest struct {
	OnBehalfOf      string   `json:"on_behalf_of"`
	Channels        []string `json:"channels"`
	LifetimeSeconds int      `json:"lifetime_seconds,omitempty"`
}

DelegateRequest is the body of POST /parsec/token/delegate.

type DeniedChannel

type DeniedChannel struct {
	Channel string `json:"channel"`
	Reason  string `json:"reason"`
}

DeniedChannel pairs a rejected channel with a human-readable reason. The reason is surfaced in the API response so clients can show a useful error to the end user.

type IssueRequest

type IssueRequest struct {
	Channels []string      `json:"channels"`
	TTL      time.Duration `json:"ttl,omitempty"`
}

IssueRequest is the inbound shape of POST /parsec/token.

type IssueResponse

type IssueResponse struct {
	Token           string          `json:"token"`
	TokenID         string          `json:"token_id"`
	ExpiresAt       time.Time       `json:"expires_at"`
	ChannelsGranted []string        `json:"channels_granted"`
	ChannelsDenied  []DeniedChannel `json:"channels_denied,omitempty"`
}

IssueResponse is what the broker sends back.

type MemoryRevocations

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

MemoryRevocations is the default in-memory revocation store.

Entries are dropped when they age past MaxTTL — without that bound the map would grow forever even though every revoked token has long since expired. MaxTTL defaults to 24h (the mgmt-token clamp ceiling). Call StartPruner(interval) to run a background goroutine that purges aged entries, or call Prune(now) directly from a test harness.

func NewMemoryRevocations

func NewMemoryRevocations() *MemoryRevocations

NewMemoryRevocations constructs an empty store with the default 24h MaxTTL.

func (*MemoryRevocations) IsRevoked

func (m *MemoryRevocations) IsRevoked(_ context.Context, tokenID string) (bool, error)

IsRevoked reports whether tokenID is in the revocation list. Entries older than MaxTTL are treated as expired and reported false.

func (*MemoryRevocations) IsUserRevoked

func (m *MemoryRevocations) IsUserRevoked(_ context.Context, userID string, issuedAt time.Time) (bool, error)

IsUserRevoked reports whether userID has a blanket revocation issued at-or-after issuedAt. Entries older than MaxTTL are treated as expired and reported false.

func (*MemoryRevocations) Prune added in v0.3.0

func (m *MemoryRevocations) Prune(now time.Time)

Prune drops every entry older than MaxTTL relative to now. Safe to call from a test or any operator-owned goroutine.

func (*MemoryRevocations) Revoke

func (m *MemoryRevocations) Revoke(_ context.Context, tokenID, userID, reason string) error

Revoke marks tokenID as invalid.

func (*MemoryRevocations) RevokeAllForUser

func (m *MemoryRevocations) RevokeAllForUser(_ context.Context, userID string) error

RevokeAllForUser invalidates every token issued to userID before now.

func (*MemoryRevocations) SetClock added in v0.3.0

func (m *MemoryRevocations) SetClock(c func() time.Time)

SetClock overrides the time source. Used in tests.

func (*MemoryRevocations) StartPruner added in v0.3.0

func (m *MemoryRevocations) StartPruner(ctx context.Context, interval time.Duration)

StartPruner runs Prune every interval until ctx is cancelled. A zero or negative interval disables the pruner (entries still expire on read; only the map shrinkage is skipped). Intended for long-lived processes; tests can call Prune directly.

func (*MemoryRevocations) WithMaxTTL added in v0.3.0

WithMaxTTL overrides the per-entry TTL. A zero or negative value resets to the 24h default.

type Options

type Options struct {
	Issuer        *auth.Issuer
	Authorizer    Authorizer
	Authenticator Authenticator
	Revocations   RevocationStore
	// DefaultTTL is the access-token TTL when the caller did not
	// specify one. Zero falls back to auth.Issuer's own AccessTTL.
	DefaultTTL time.Duration
	// AuditLog, when non-nil, receives one entry per successful issuance.
	AuditLog AuditLogger
	// DelegateAuthorizer, when non-nil, gates /parsec/token/delegate.
	// The service identity is the bearer's UserID; the function returns
	// nil if the service is allowed to act on behalf of the target user.
	DelegateAuthorizer func(ctx context.Context, service UserID, onBehalfOf UserID) error
}

Options configures a Broker.

type RedisRevocations added in v0.3.0

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

RedisRevocations implements RevocationStore against Redis using SET with EX so storage self-prunes when revoked tokens age out past their natural expiry. Keyspace (with default prefix "parsec"):

<prefix>:tb:revoked:<tokenID>     "<reason>"                   (TTL = MaxTTL)
<prefix>:tb:user_revoked_at:<uid> "<unix-nanos of revoke time>" (TTL = MaxTTL)

MaxTTL caps how long a revocation entry stays observable in Redis. Operators set it to at least the longest mgmt-token TTL plus a safety margin (24h is the package default — matches the mgmt clamp ceiling in auth.Issuer). Once a revocation entry ages out, the underlying token has expired anyway and Redis releases the bytes.

func NewRedisRevocations added in v0.3.0

func NewRedisRevocations(client redis.UniversalClient) *RedisRevocations

NewRedisRevocations constructs a store backed by client. MaxTTL defaults to 24h; override with WithMaxTTL.

func (*RedisRevocations) IsRevoked added in v0.3.0

func (s *RedisRevocations) IsRevoked(ctx context.Context, tokenID string) (bool, error)

IsRevoked implements RevocationStore.

func (*RedisRevocations) IsUserRevoked added in v0.3.0

func (s *RedisRevocations) IsUserRevoked(ctx context.Context, userID string, issuedAt time.Time) (bool, error)

IsUserRevoked implements RevocationStore.

func (*RedisRevocations) Revoke added in v0.3.0

func (s *RedisRevocations) Revoke(ctx context.Context, tokenID, userID, reason string) error

Revoke implements RevocationStore.

func (*RedisRevocations) RevokeAllForUser added in v0.3.0

func (s *RedisRevocations) RevokeAllForUser(ctx context.Context, userID string) error

RevokeAllForUser implements RevocationStore. The value stored is the unix-nano timestamp of the revoke moment; IsUserRevoked compares this against the token's issuedAt to decide whether a given token predates the blanket revocation.

func (*RedisRevocations) SetClock added in v0.3.0

func (s *RedisRevocations) SetClock(c func() time.Time)

SetClock overrides the time source. Used in tests.

func (*RedisRevocations) WithKeyPrefix added in v0.3.0

func (s *RedisRevocations) WithKeyPrefix(p string) *RedisRevocations

WithKeyPrefix overrides the namespace. Empty resets to "parsec".

func (*RedisRevocations) WithMaxTTL added in v0.3.0

func (s *RedisRevocations) WithMaxTTL(d time.Duration) *RedisRevocations

WithMaxTTL overrides the TTL applied to every revocation entry. A zero or negative value resets to the 24h default.

type RevocationStore

type RevocationStore interface {
	Revoke(ctx context.Context, tokenID, userID, reason string) error
	IsRevoked(ctx context.Context, tokenID string) (bool, error)
	RevokeAllForUser(ctx context.Context, userID string) error
	IsUserRevoked(ctx context.Context, userID string, issuedAt time.Time) (bool, error)
}

RevocationStore tracks revoked token IDs. The default is in-memory; production deployments should plug in Redis or a database.

type RevokeRequest

type RevokeRequest struct {
	TokenID string `json:"token_id,omitempty"`
	UserID  string `json:"user_id,omitempty"`
	Reason  string `json:"reason,omitempty"`
}

RevokeRequest is the body of POST /parsec/revoke.

type RoleAuthorizer

type RoleAuthorizer struct {
	UserRoles    func(ctx context.Context, user UserID) []string
	RolePatterns map[string][]string
}

RoleAuthorizer is a small built-in Authorizer that grants channels based on a role->pattern map. Each user has zero or more roles; each role has zero or more channel patterns that match the channels the role may access.

func (*RoleAuthorizer) Authorize

func (r *RoleAuthorizer) Authorize(ctx context.Context, user UserID, channels []string) AuthDecision

Authorize implements Authorizer. A channel is granted when at least one of the user's roles owns a pattern matching the channel.

type UserID

type UserID string

UserID is the opaque caller identity, supplied by the upstream authenticator (your user-auth system, OIDC IdP, internal session service, etc.). It is the input to the Authorizer.

Jump to

Keyboard shortcuts

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