tokenbroker

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jun 5, 2026 License: MIT Imports: 10 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.

func NewMemoryRevocations

func NewMemoryRevocations() *MemoryRevocations

NewMemoryRevocations constructs an empty store.

func (*MemoryRevocations) IsRevoked

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

IsRevoked reports whether tokenID is in the revocation list.

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.

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.

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