auth

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: May 27, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package auth issues and verifies the HMAC-SHA256 JWTs that gate access to Parsec. There are three token types, distinguished by the typ claim:

  • access — short-lived, used to connect over the websocket and to subscribe to listed private channels
  • refresh — exchanged at the RefreshToken RPC for a fresh access token
  • mgmt — operator token presented as Authorization: Bearer on the management RPC surface

The wire format is the standard JWT compact serialization (base64url(header).base64url(claims).base64url(hmac)), but Parsec uses a fixed header — alg=HS256, typ=JWT — and refuses any other.

No JWT library: the implementation is ~150 lines of stdlib crypto/hmac and crypto/sha256.

Index

Constants

View Source
const KeyringFileName = "keyring.json"

KeyringFileName is the conventional file name inside the state dir.

Variables

View Source
var (
	ErrMalformedToken   = errors.New("auth: malformed token")
	ErrInvalidSignature = errors.New("auth: signature does not verify")
	ErrUnsupportedAlg   = errors.New("auth: unsupported algorithm")
	ErrExpired          = errors.New("auth: token expired")
	ErrNotYetValid      = errors.New("auth: token not yet valid")
	ErrTypeMismatch     = errors.New("auth: token type mismatch")
	ErrChannelMismatch  = errors.New("auth: token does not authorize this channel")
)

Sentinel errors. Callers errors.Is against these instead of string-matching.

AllVerbs lists every recognized verb in stable order. Used by the manifest so client SDKs can discover the surface.

Functions

func GenerateSecret

func GenerateSecret() ([]byte, error)

GenerateSecret returns a 32-byte cryptographically random secret suitable for Signer / Verifier. The caller persists it (env var, secrets store); regenerating wipes every previously-issued token.

func MapErr

func MapErr(err error) error

MapErr translates an auth sentinel into a parsec coded error. Exposed so the library facade and surface code can reuse the mapping.

func NewSubscribeAuthorizer

func NewSubscribeAuthorizer(v *Verifier) func(ctx context.Context, userID string, ch channels.Name, event centrifuge.SubscribeEvent) error

NewSubscribeAuthorizer returns a broker subscribe authorizer that allows any well-formed public channel and verifies an access token for any private channel. The token must list the requested channel in its chs claim.

The returned function matches broker.SubscribeAuthorizer.

func NewSubscribeAuthorizerWithLimiter

func NewSubscribeAuthorizerWithLimiter(v *Verifier, limiter ratelimit.Limiter, defaultLimit ratelimit.Limit) func(ctx context.Context, userID string, ch channels.Name, event centrifuge.SubscribeEvent) error

NewSubscribeAuthorizerWithLimiter is NewSubscribeAuthorizer plus a per-key rate-limit gate that runs BEFORE token verification (so a stream of bad-token attempts cannot exhaust CPU on HMAC verifies).

The key is the userID when set, otherwise the centrifuge client ID (which encodes the connection — best-effort proxy for IP when running behind the centrifuge transport).

Passing limiter == nil or a zero Limit reverts to the no-rate-limit behaviour.

func ReloadInto

func ReloadInto(path string, ring *KeyRing) error

ReloadInto re-reads path into the existing ring, swapping its contents atomically. The pointer identity is preserved so any Signer / Verifier already bound to ring picks up the new keys.

func SaveKeyRing

func SaveKeyRing(path string, r *KeyRing) error

SaveKeyRing writes the ring's snapshot to path, atomically. The file is created with mode 0600; the parent directory is created with mode 0700 if missing.

func WatchKeyRingFile

func WatchKeyRingFile(ctx context.Context, path string, ring *KeyRing, interval time.Duration, onReload func(activeID string), onError func(error))

WatchKeyRingFile polls path's mtime every interval; when it changes, reload is called with the new contents. The function blocks until ctx is canceled. interval == 0 disables polling (returns immediately).

onReload is invoked on every successful reload (after the snapshot swap). Errors during reload are passed to onError; the watcher keeps running so a transient parse failure does not kill the loop.

Types

type Claims

type Claims struct {
	// Sub is the subject — user id for client tokens, operator id for mgmt.
	Sub string `json:"sub,omitempty"`
	// Typ is the Parsec token type discriminator.
	Typ Type `json:"typ"`
	// Chs is the list of channels this token authorizes. Empty means
	// "no client-channel authorization." For mgmt tokens it is ignored.
	Chs []string `json:"chs,omitempty"`
	// Iat is the issued-at time (Unix seconds).
	Iat int64 `json:"iat"`
	// Exp is the expiration time (Unix seconds).
	Exp int64 `json:"exp"`
	// RateLimitOverride, when non-nil, overrides the server's default
	// per-subject rate limit for this token. Useful to hand a noisy
	// integration a tighter budget without touching server config. The
	// claim is private to Parsec (no JWT-RFC equivalent); absence means
	// "use the operator-configured default."
	RateLimitOverride *ratelimit.Limit `json:"rl,omitempty"`
	// Scopes is the pattern-based grant set. Each Scope binds a
	// channel-name Pattern to a set of Verbs (subscribe, publish,
	// manage). Empty means "no pattern grants" — only the Chs exact
	// list applies. A token carrying both Chs and Scopes is authorized
	// for the union.
	Scopes []Scope `json:"scopes,omitempty"`
}

Claims is the Parsec payload. Fields use JWT-conventional short names so the wire shape is compact.

func (Claims) Authorizes

func (c Claims) Authorizes(channel string, v Verb) bool

Authorizes reports whether the holder is granted verb v on the named channel. The check unions two grant sources, with deny-wins precedence applied uniformly:

  • Chs (exact match): any name in c.Chs authorizes every verb.
  • Scopes (pattern match): each Scope grants (or, when Deny=true, subtracts) its listed verbs on channel names that match its Pattern.

Evaluation order:

  1. Walk the Scopes for any deny scope whose pattern matches and whose verb list contains v — if found, return false immediately ("deny wins", consistent with AWS IAM and similar systems). Denies override both Chs entries and overlapping allow scopes.
  2. Walk c.Chs for an exact-name match.
  3. Walk the Scopes for an allow scope whose pattern matches and whose verb list contains v.
  4. Otherwise return false.

channel is the raw wire-form channel name; it is parsed once and compared against any applicable Scope. A malformed channel name returns false (fail closed). An empty c.Chs and empty c.Scopes return false for every input.

A Scope with Deny=true is visible in the token like any other claim — operators should not treat deny patterns as confidential channel metadata (see docs/src/channels/acl.md).

func (Claims) ExpiresAt

func (c Claims) ExpiresAt() time.Time

ExpiresAt returns Exp as a time.Time.

func (Claims) IssuedAt

func (c Claims) IssuedAt() time.Time

IssuedAt returns Iat as a time.Time.

type CompositeVerifier

type CompositeVerifier struct {
	// HMAC is the parsec-issued JWT verifier. Required.
	HMAC *Verifier

	// OIDC validates ID tokens from a configured IdP. Optional —
	// when nil the composite is HMAC-only.
	OIDC *OIDCVerifier
}

CompositeVerifier tries the parsec HMAC verifier first; if that fails AND an OIDC verifier is configured, it falls back to OIDC. A success from either path wins. When both fail, the HMAC error is returned (the HMAC path is the primary verification surface — its error code maps cleanly onto PARSEC_AUTH_*).

A nil OIDC field reduces this to a pass-through over HMAC, so deployments without OIDCConfig accept only HMAC-signed tokens.

func NewCompositeVerifier

func NewCompositeVerifier(hmac *Verifier, oidc *OIDCVerifier) (*CompositeVerifier, error)

NewCompositeVerifier returns a verifier that composes an HMAC verifier with an optional OIDC verifier. hmac must be non-nil (parsec.New always constructs one).

func (*CompositeVerifier) HMACVerify

func (c *CompositeVerifier) HMACVerify(token string, expected Type) (Claims, error)

HMACVerify validates token strictly via the HMAC path. Useful for surface code that must reject OIDC-shaped tokens (e.g. refresh-token RPC, which has no OIDC analog).

func (*CompositeVerifier) OIDCEnabled

func (c *CompositeVerifier) OIDCEnabled() bool

OIDCEnabled reports whether an OIDC verifier is wired.

func (*CompositeVerifier) Verify

func (c *CompositeVerifier) Verify(ctx context.Context, token string, expected Type) (Claims, error)

Verify tries the HMAC verifier first; on any failure, and if OIDC is wired, it tries the OIDC verifier. The first success wins. When both fail, the HMAC error is returned because it is the more specific path (operator-minted tokens dominate inbound traffic on a typical deployment).

Verify is safe for concurrent use. The expected Type is enforced against the HMAC payload; OIDC tokens always synthesize Typ=TypeMgmt and so are only honored when expected is empty or TypeMgmt.

type FileKeyRingStore

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

FileKeyRingStore reads and writes the KeyRing as a JSON file at <state-dir>/keyring.json. Single-node deployments use this; multi-node deployments swap for RedisKeyRingStore.

func NewFileKeyRingStore

func NewFileKeyRingStore(path string, pollEvery time.Duration) *FileKeyRingStore

NewFileKeyRingStore constructs a store at path. pollEvery controls how frequently Watch checks the file's mtime. Pass 0 to disable polling (Watch becomes a no-op).

func (*FileKeyRingStore) Ensure

func (s *FileKeyRingStore) Ensure(ctx context.Context) (*KeyRing, bool, error)

Ensure satisfies the bootstrap pattern: load or generate a fresh ring, persist, return.

func (*FileKeyRingStore) Load

func (s *FileKeyRingStore) Load(_ context.Context) (*KeyRing, error)

Load reads the file.

func (*FileKeyRingStore) Path

func (s *FileKeyRingStore) Path() string

Path returns the on-disk path.

func (*FileKeyRingStore) Save

func (s *FileKeyRingStore) Save(_ context.Context, r *KeyRing) error

Save writes the ring atomically.

func (*FileKeyRingStore) Watch

func (s *FileKeyRingStore) Watch(ctx context.Context, onChange func(*KeyRing)) error

Watch polls the file's mtime and re-loads on change.

type Issuer

type Issuer struct {

	// AccessTTL is the lifetime of access tokens minted for clients.
	// Default 5 minutes. Clamped to [1m, 1h].
	AccessTTL time.Duration
	// MaxRefreshTTL is the upper bound on refresh tokens. The actual
	// refresh TTL is min(channelTTL, MaxRefreshTTL). Default 1h.
	MaxRefreshTTL time.Duration
	// contains filtered or unexported fields
}

Issuer mints Parsec JWTs with the right TTL bounds. It wraps a Signer with policy: refresh tokens cannot outlive their channel; access tokens cannot outlive their refresh token.

func NewIssuer

func NewIssuer(signer *Signer) *Issuer

NewIssuer constructs an Issuer over signer.

func (*Issuer) IssueAccess

func (i *Issuer) IssueAccess(sub, channel string, refreshExp time.Time, scopes []Scope) (string, time.Time, error)

IssueAccess mints a single access token from a verified refresh. The caller is responsible for verifying the refresh token first; this method trusts its inputs (sub, channel, refreshExp, scopes).

The new access expiry is min(now+AccessTTL, refreshExp) so a refresh can never extend a session past the refresh token's own lifetime.

A nil/empty scopes slice produces a token with no pattern grants — the channel listed in Chs is still authorized for every verb. Each scope's Pattern is validated against the channel grammar before signing.

func (*Issuer) IssueAccessForChannels

func (i *Issuer) IssueAccessForChannels(sub string, chs []string, refreshExp time.Time) (string, time.Time, error)

IssueAccessForChannels mints a fresh access token authorizing chs. The expiry is min(now+AccessTTL, refreshExp).

func (*Issuer) IssueMgmt

func (i *Issuer) IssueMgmt(sub string, ttl time.Duration) (string, time.Time, error)

IssueMgmt mints an operator token. ttl is clamped to [1h, 7d]; default 24h when zero.

func (*Issuer) IssuePair

func (i *Issuer) IssuePair(sub, channel string, channelTTL time.Duration, scopes []Scope) (PairResult, error)

IssuePair mints an access + refresh token pair for sub on the named channel and stamps the provided scope grants into both tokens. The refresh token is bounded by min(channelTTL, MaxRefreshTTL); the access token by min(AccessTTL, refreshTTL).

A nil/empty scopes slice produces tokens with no pattern grants — the channel listed in Chs is still authorized for every verb.

Each scope's Pattern is validated against the channel grammar before signing; an invalid pattern yields PARSEC_INVALID_ARGUMENT.

func (*Issuer) IssuePairForChannels

func (i *Issuer) IssuePairForChannels(sub string, chs []string, ttl time.Duration, scopes []Scope) (PairResult, error)

IssuePairForChannels mints an access + refresh token pair that authorizes the listed channels (rather than a single channel like IssuePair). The refresh TTL is bounded by min(ttl, MaxRefreshTTL); the access TTL by min(AccessTTL, refresh). Used by the token broker for multi-channel issuance.

func (*Issuer) IssuePairWithRateLimit

func (i *Issuer) IssuePairWithRateLimit(sub, channel string, channelTTL time.Duration, override ratelimit.Limit) (PairResult, error)

IssuePairWithRateLimit is IssuePair with an additional per-token rate limit embedded in the access + refresh claims (rl). When the rate limiter is consulted at publish/subscribe time the override beats the server default for this subject.

Passing a zero Limit (Rate == 0) is the same as IssuePair — no override claim is written.

func (*Issuer) SetClock

func (i *Issuer) SetClock(c func() time.Time)

SetClock overrides the time source. Used in tests.

type Key

type Key struct {
	ID        string
	Secret    []byte
	Role      Role
	CreatedAt time.Time
	RetiredAt *time.Time
	// contains filtered or unexported fields
}

Key is one HMAC secret with rotation metadata. Construct via KeyRing, never directly.

type KeyRing

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

KeyRing is the live set of HMAC keys parsec uses to sign and verify tokens. Exactly one key holds RoleActive. The ring is safe for concurrent use.

func EnsureKeyRing

func EnsureKeyRing(path string) (*KeyRing, bool, error)

EnsureKeyRing returns a usable KeyRing for path. If the file exists, it is loaded. If not, a fresh ring with one active key is generated and saved. The bool return is true when bootstrap happened.

func LoadKeyRing

func LoadKeyRing(path string) (*KeyRing, error)

LoadKeyRing reads path into a fresh ring. If the file does not exist, returns (nil, os.ErrNotExist) so the caller can decide whether to bootstrap.

func NewKeyRing

func NewKeyRing() *KeyRing

NewKeyRing returns an empty KeyRing.

func NewKeyRingFromSecret

func NewKeyRingFromSecret(secret []byte) (*KeyRing, error)

NewKeyRingFromSecret seeds a ring with one active key whose Secret is the supplied bytes. Convenient for tests that want a stable HMAC across a parsec.New / recreate pair without using a state directory.

func (*KeyRing) Active

func (r *KeyRing) Active() (Key, error)

Active returns a copy of the active key. Errors if no key is active.

func (*KeyRing) ActiveID

func (r *KeyRing) ActiveID() string

ActiveID returns the active key's ID, or "" if there is none.

func (*KeyRing) Add

func (r *KeyRing) Add(id string, secret []byte) (Key, error)

Add installs id+secret into the ring. If the ring was empty the new key becomes active; otherwise it joins as verify-only and must be Promoted before it signs.

func (*KeyRing) Generate

func (r *KeyRing) Generate() (Key, error)

Generate creates a fresh 32-byte secret, installs it as a new key in the ring, and returns a copy. If the ring was empty the new key becomes active; otherwise it joins as verify-only.

func (*KeyRing) Get

func (r *KeyRing) Get(id string) (Key, error)

Get returns a copy of the key with id, or an error if no such key exists OR the key is retired (callers should treat both the same).

func (*KeyRing) List

func (r *KeyRing) List() []Key

List returns a snapshot of every key in the ring, sorted by CreatedAt. Retired keys are included so the operator can audit recent deletions.

func (*KeyRing) LoadSnapshot

func (r *KeyRing) LoadSnapshot(s Snapshot) error

LoadSnapshot replaces the ring's contents with the snapshot's. Safe to call on a running ring — the swap is atomic from the caller's perspective.

func (*KeyRing) Promote

func (r *KeyRing) Promote(id string) error

Promote makes id the active key. The previously-active key (if any) transitions to verify-only. Promoting an unknown or retired key errors.

func (*KeyRing) Retire

func (r *KeyRing) Retire(id string) error

Retire marks id as retired. Retiring the active key errors — Promote another key first. Retiring an already-retired key is a no-op.

func (*KeyRing) Snapshot

func (r *KeyRing) Snapshot() Snapshot

Snapshot returns a deep copy of the ring suitable for persistence or transfer. Excludes retired keys.

type KeyRingStore

type KeyRingStore interface {
	// Load returns the persisted ring, or (nil, os.ErrNotExist) when the
	// store is empty so the caller can decide whether to bootstrap.
	Load(ctx context.Context) (*KeyRing, error)

	// Save replaces the persisted snapshot with the supplied ring's.
	Save(ctx context.Context, r *KeyRing) error

	// Watch subscribes to remote modifications. onChange fires with each
	// freshly loaded ring snapshot after a remote write. Blocks until ctx
	// is canceled or the underlying transport errors fatally. File-backed
	// implementations may emit periodically via mtime polling; Redis
	// implementations use pub/sub.
	Watch(ctx context.Context, onChange func(*KeyRing)) error
}

KeyRingStore is the persistence interface for the HMAC KeyRing. Two implementations ship: a file-backed store (single-node) and a Redis- backed store that lets multiple Parsec nodes share the same keys and observe rotation events without manual SIGHUPs.

type OIDCConfig

type OIDCConfig struct {
	// Issuer is the IdP's OpenID issuer URL (e.g.
	// "https://accounts.google.com"). NewOIDCVerifier fetches
	// <Issuer>/.well-known/openid-configuration to discover the
	// JWKS endpoint and supported algorithms.
	Issuer string

	// Audience is the expected `aud` claim. Set to the parsec
	// deployment's client identifier (e.g. "parsec-prod"). If empty
	// the verifier rejects every token (defensive — every IdP
	// requires an audience).
	Audience string

	// SubjectClaim names the ID-token claim to copy into the
	// synthetic Claims.Sub field. Defaults to "sub" when empty.
	// Operators frequently set this to "email" so the access log
	// records the operator's address.
	SubjectClaim string

	// ScopesClaim names the ID-token claim that carries the
	// operator's group memberships. Defaults to "groups" when
	// empty. The claim is expected to be a JSON array of strings.
	ScopesClaim string

	// Grants maps IdP group names onto parsec scope patterns. When
	// an incoming token's ScopesClaim contains a group listed here,
	// the matching grant's Scope + Verbs is added to the synthetic
	// Claims.Scopes set. Multiple grants may match a single token.
	Grants []OIDCGrant

	// Clock overrides time.Now for verification, primarily for
	// tests. nil = time.Now.
	Clock func() time.Time
}

OIDCConfig captures the operator-supplied configuration for an OIDC identity provider. Empty Issuer means "OIDC is disabled" — the composite verifier short-circuits past it.

type OIDCGrant

type OIDCGrant struct {
	// IfGroup is the literal group name to match against the
	// token's ScopesClaim list. Comparison is byte-exact — no
	// wildcards, no case folding.
	IfGroup string

	// Scope is the channel-name pattern to grant (parsec scope
	// grammar — see channels.ParsePattern).
	Scope string

	// Verbs is the set of actions the matching operator may take
	// on channels covered by Scope.
	Verbs []Verb
}

OIDCGrant maps one IdP group name onto one parsec scope. When the incoming ID token's group list contains IfGroup, the synthetic Claims gains a Scope with Pattern=Scope and Verbs=Verbs.

type OIDCVerifier

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

OIDCVerifier wraps go-oidc's provider + id-token verifier behind a parsec-friendly Verify call that returns synthetic auth.Claims.

One OIDCVerifier is constructed per parsec.New (issuer discovery happens at boot, NOT per request). The underlying *oidc.IDTokenVerifier is safe for concurrent use, so a single OIDCVerifier services every inbound request.

func NewOIDCVerifier

func NewOIDCVerifier(ctx context.Context, cfg OIDCConfig) (*OIDCVerifier, error)

NewOIDCVerifier discovers the issuer's OpenID configuration and returns a verifier ready to validate ID tokens. The ctx is used for the discovery roundtrip only — once NewOIDCVerifier returns, the verifier is decoupled from ctx and can outlive it.

Returns an error when cfg.Issuer is empty (callers should construct no verifier in that case), when discovery fails, or when cfg.Audience is empty.

func (*OIDCVerifier) Audience

func (v *OIDCVerifier) Audience() string

Audience reports the configured audience. Exposed for the manifest and tests.

func (*OIDCVerifier) Issuer

func (v *OIDCVerifier) Issuer() string

Issuer reports the configured issuer URL. Exposed for the manifest.

func (*OIDCVerifier) Verify

func (v *OIDCVerifier) Verify(ctx context.Context, token string) (Claims, error)

Verify validates token against the configured IdP (signature, exp, iat, aud, iss) and translates the result into a synthetic mgmt Claims. Returns one of the auth sentinel errors on failure so the composite verifier and the HTTP middleware can map to PARSEC_AUTH_*.

The returned Claims always carries Typ=TypeMgmt. OIDC IdPs do not issue refresh / access tokens in the parsec sense; clients hit `parsec login oidc` once per session and present the resulting ID token as a mgmt bearer.

type PairResult

type PairResult struct {
	AccessToken    string
	RefreshToken   string
	AccessExpires  time.Time
	RefreshExpires time.Time
}

PairResult is what IssuePair returns: both tokens and their absolute expirations.

type RedisKeyRingStore

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

RedisKeyRingStore persists the KeyRing as a JSON blob in Redis with a monotonic version counter. Save uses WATCH/MULTI to detect concurrent writes; Watch listens on a pub/sub channel for cross-node fanout of rotation events.

func NewRedisKeyRingStore

func NewRedisKeyRingStore(client redis.UniversalClient) *RedisKeyRingStore

NewRedisKeyRingStore constructs a store backed by client. The default key prefix is "parsec"; the on-disk layout becomes:

<prefix>:keyring          string (JSON snapshot)
<prefix>:keyring:version  integer (monotonic)
<prefix>:keyring:events   pub/sub channel (version-number payloads)

func (*RedisKeyRingStore) Ensure

func (s *RedisKeyRingStore) Ensure(ctx context.Context) (*KeyRing, bool, error)

Ensure returns a ring, bootstrapping if empty. Mirrors the file store bootstrap pattern; the resulting ring is persisted on bootstrap.

func (*RedisKeyRingStore) Load

func (s *RedisKeyRingStore) Load(ctx context.Context) (*KeyRing, error)

Load reads the persisted snapshot. Returns (nil, os.ErrNotExist) when the key does not exist so the caller can bootstrap.

func (*RedisKeyRingStore) Save

func (s *RedisKeyRingStore) Save(ctx context.Context, r *KeyRing) error

Save persists ring with optimistic concurrency. Uses WATCH on the version key so concurrent writes from two nodes are detected and the loser must reload + retry. Bumps the version and publishes it on the pub/sub channel so other nodes can react.

func (*RedisKeyRingStore) Watch

func (s *RedisKeyRingStore) Watch(ctx context.Context, onChange func(*KeyRing)) error

Watch subscribes to the events channel and re-loads + fires onChange on every version bump it sees from another node. Own writes are observable too — callers may dedupe by comparing the resulting snapshot. Blocks until ctx is canceled.

func (*RedisKeyRingStore) WithKeyPrefix

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

WithKeyPrefix overrides the namespace.

type Role

type Role string

Role is a key's lifecycle position in a KeyRing.

const (
	// RoleActive is the signing key. Exactly one key in a ring holds this
	// role at a time. New tokens are minted with this key's kid.
	RoleActive Role = "active"
	// RoleVerifyOnly accepts existing tokens but does not sign new ones.
	// A key transitions to verify-only when superseded by Promote, or when
	// added via Add (which never installs as active).
	RoleVerifyOnly Role = "verify-only"
	// RoleRetired is a deleted key. Retired keys are dropped from the next
	// snapshot and stop verifying immediately.
	RoleRetired Role = "retired"
)

type Scope

type Scope struct {
	Pattern string `json:"pat"`
	Verbs   []Verb `json:"v"`
	Deny    bool   `json:"deny,omitempty"`
	// contains filtered or unexported fields
}

Scope is one entry in a Claims.Scopes set: a channel pattern plus the verbs the holder is granted on names that match.

The Pattern field stays as raw text on the wire so the token shape matches the spec; Compiled() lazily parses and caches the structured Pattern for the matcher's hot path.

A scope with Deny=true inverts the meaning: a matching (channel, verb) pair is REMOVED from the grant set with "deny wins" precedence. See Claims.Authorizes for the full evaluation order. The Deny field is omitted from the wire when false so allow-only tokens stay compact.

func (*Scope) AllowsVerb

func (s *Scope) AllowsVerb(v Verb) bool

AllowsVerb is a cheap check that only inspects the verb list, used by callers that have already matched the pattern.

func (*Scope) Authorizes

func (s *Scope) Authorizes(name channels.Name, v Verb) bool

Authorizes reports whether this scope grants verb v on the channel name. A malformed Pattern fails closed (returns false, never panics).

func (*Scope) Compiled

func (s *Scope) Compiled() (channels.Pattern, error)

Compiled returns the parsed channels.Pattern, caching it on first call. Errors are sticky: a Scope with a malformed Pattern returns the same error every time without re-parsing.

func (*Scope) IsDeny

func (s *Scope) IsDeny() bool

IsDeny reports whether the scope is a negative grant. Convenience accessor so callers don't need to reach for the field directly when iterating a Claims.Scopes slice.

func (*Scope) MatchesVerb

func (s *Scope) MatchesVerb(name channels.Name, v Verb) bool

MatchesVerb is the shared "pattern matches AND verb is listed" check. Used by both the allow-pass and the deny-pass in Claims.Authorizes so the precedence logic stays in one place. Returns false on any compile / match / verb miss (fail closed).

type Signer

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

Signer produces signed Parsec JWTs from Claims using a KeyRing's active key.

func NewSigner

func NewSigner(ring *KeyRing) (*Signer, error)

NewSigner returns a Signer bound to ring. The ring must have at least one active key — checked lazily at sign time.

func (*Signer) Sign

func (s *Signer) Sign(c Claims) (string, error)

Sign serializes claims and signs them with the keyring's currently-active key. The returned token's header embeds that key's kid.

type Snapshot

type Snapshot struct {
	FormatVersion string        `json:"format_version"`
	ActiveKeyID   string        `json:"active_key_id"`
	Keys          []SnapshotKey `json:"keys"`
}

Snapshot is the serializable view of a KeyRing.

type SnapshotKey

type SnapshotKey struct {
	ID        string `json:"id"`
	SecretHex string `json:"secret_hex"`
	Role      Role   `json:"role"`
	CreatedAt string `json:"created_at"`
	RetiredAt string `json:"retired_at,omitempty"`
}

SnapshotKey is one persisted key entry.

type Type

type Type string

Type is the JWT typ-extension claim Parsec uses to disambiguate token purpose. The standard JWT typ header stays "JWT"; the discriminator lives in the payload's "typ" field.

const (
	TypeAccess  Type = "access"
	TypeRefresh Type = "refresh"
	TypeMgmt    Type = "mgmt"
)

func (Type) Valid

func (t Type) Valid() error

Valid returns nil if t is one of the recognized token types.

type Verb

type Verb string

Verb is the typed action a Scope grants on a channel pattern. The wire values are the lowercase strings — they appear inside JWT payloads and the manifest, so changing them is a wire break.

const (
	// VerbSubscribe authorizes a subscribe over the websocket / SSE
	// surface. This is the verb the broker's subscribe authorizer
	// checks.
	VerbSubscribe Verb = "subscribe"
	// VerbPublish authorizes a client-side publish (server-side
	// publishes via the management RPC still go through the mgmt
	// bearer).
	VerbPublish Verb = "publish"
	// VerbManage authorizes per-channel management RPCs called with
	// an access token instead of a mgmt token — delegated administration.
	VerbManage Verb = "manage"
)

func (Verb) Valid

func (v Verb) Valid() bool

Valid reports whether v is one of the recognized verbs. Unknown verbs silently fail-closed at the authorize call — Scope.Authorizes will not honor them — but Valid is exposed for callers that want to validate at the boundary.

type Verifier

type Verifier struct {
	Clock  func() time.Time
	Leeway time.Duration
	// OnVerify, when non-nil, is invoked once per Verify call with the
	// token type that was checked (or the empty string when the token
	// failed to parse before the typ claim could be read), the expected
	// type (may be empty), and the resulting error (nil on success).
	// Used by the metrics layer to record verifications by type+result
	// without coupling the auth package to prometheus.
	OnVerify func(parsed Type, expected Type, err error)
	// contains filtered or unexported fields
}

Verifier validates Parsec JWTs against a KeyRing. The ring is followed by reference, so a reload that swaps the ring's contents takes effect on the next call.

func NewVerifier

func NewVerifier(ring *KeyRing) (*Verifier, error)

NewVerifier returns a Verifier bound to ring.

func (*Verifier) Verify

func (v *Verifier) Verify(token string, expected Type) (Claims, error)

Verify parses token, validates the header (must include kid, must use HS256+JWT), looks up the key in the ring, verifies the signature, and checks expiry. When expected != "" it also enforces the typ claim.

Jump to

Keyboard shortcuts

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