auth

package
v1.3.1 Latest Latest
Warning

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

Go to latest
Published: Jun 11, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package auth is the Harbor Protocol's JWT validation surface — the Phase 61 transport-edge cryptographic identity check that turns the Phase 60 wire transports' trust-based identity carriers into verified ones (RFC §5.5: "JWT, asymmetric algorithms only ... the triple (tenant, user, session) is in the JWT claims; the Protocol rejects any request without an identity scope").

The two-piece surface

auth ships two pieces that compose:

  • Validator (this file) — transport-agnostic. Takes a raw JWT string, parses + verifies it against a configured KeySet, asserts the signing algorithm is in the asymmetric allowlist, extracts the (tenant, user, session) claim triple + scope claims, and returns a Verified struct. Every failure is one of the eight typed sentinels.

  • Middleware (middleware.go) — net/http binding. Reads the `Authorization: Bearer <token>` header, calls Validator.Validate, injects the verified identity + scopes into r.Context() (via identity.With + auth.WithScopes), and calls the wrapped handler. A failure writes a JSON Protocol error body with HTTP 401 (or 403 for a scope mismatch) and emits an audit-redacted slog.Warn.

The asymmetric-algorithm allowlist (CLAUDE.md §7 rule 1)

Six algorithms — three RSA + three ECDSA — are accepted:

RS256 / RS384 / RS512      ECDSA-P-256/384/521 = ES256 / ES384 / ES512

HS* (HMAC) and `none` are rejected at the **parser level** via `jwt.WithValidMethods`, BEFORE the Keyfunc is consulted — so the classical algorithm-confusion CVE family (an `HS256` token signed with an `RS256` public key as the HMAC secret) is structurally impossible. The security_test.go suite pins this.

The Protocol identity claim shape

JWT claims map onto identity.Identity by name:

{
    "iss":    "https://idp.example.com",  // optional, audited
    "sub":    "user-12345",               // optional, audited
    "aud":    "harbor-runtime",           // optional, validated
    "exp":    1746662400,                 // mandatory
    "nbf":    1746576000,                 // optional
    "tenant": "tenant-acme",              // mandatory
    "user":   "user-12345",               // mandatory
    "session":"sess-01HX...",             // mandatory
    "scopes": ["admin", "console:fleet"]  // optional
}

The triple (tenant, user, session) is mandatory — a missing claim returns ErrIdentityClaimMissing, which the middleware surfaces as a 401 with the canonical CodeIdentityRequired Protocol code. Scopes are optional — a token with no scopes is still authenticated, just not entitled to elevated subscriptions.

Concurrent reuse (D-025)

Validator is a compiled artifact: the KeySet, the parser configuration, the clock, and the redactor are set once at construction and never mutated. Validate holds no per-call state on the struct — every per-call value lives on the function's stack / the returned Verified. One Validator is safe to share across N concurrent Validate goroutines; concurrent_test.go pins N≥120 under -race.

Index

Constants

View Source
const AdminImpersonationReason = "impersonation"

AdminImpersonationReason is the stable sentinel name for an `audit.admin_scope_used` event emitted by the Phase 72b admin-impersonation path. The Reason field of AdminScopeUsedPayload is set to this constant when the bus event comes from the impersonation gate (vs. the Phase 05 events.Subscribe admin-filter emit, which carries the events.AdminScopeUsedPayload shape).

Other emit sites under audit.admin_scope_used MAY add new sentinels (e.g. delegated-impersonation post-V1); a Protocol client branches on Reason, never on the wrapped human-readable detail.

View Source
const EventTypeAuthRejected events.EventType = "auth.rejected"

EventTypeAuthRejected is the canonical EventType emitted whenever the Phase 61 JWT auth pipeline rejects a request at the transport edge (a missing token, an algorithm-confusion attack, an expired bearer, an unknown kid, a scope mismatch, etc.). The event lives on the bus alongside every other rejection-class signal — the same observability surface Phase 05 + Phase 57 ship for the rest of the runtime — so a Console (or any Protocol client) can subscribe to auth rejections through the canonical events.EventBus rather than scraping slog output.

Payload is AuthRejectedPayload; the body NEVER carries the raw token (CLAUDE.md §7 rule 7), only the reason sentinel name, the `kid` (a public header), and the optional `iss` / `sub` audited identifiers — all run through the audit.Redactor at the middleware edge before the publish.

PR #91 / D-082: surfaced by the Wave 10 audit's WARN-3. Before this addition, auth rejections only emitted a structured `slog.Warn` — observable to an operator with log access but NOT to a Console subscribing through the Protocol's canonical event channel.

View Source
const HeaderSession = "X-Harbor-Session"

HeaderSession is the per-request session selector (D-171). The connection token authenticates the (tenant, user, scopes) — it is a per-backend credential, like an API key, NOT a single-session pin. The session is chosen per-conversation by the client and supplied on every request via this header. When present, the middleware REPLACES the token's `session` claim with the header value (keeping the token's verified tenant + user), so one connection drives many isolated sessions. The token's `session` claim is a DEFAULT only: when the header is absent, the claim's session is used.

The value MUST be identical to the SSE transport's `stream.HeaderSession`; the constant is duplicated here (rather than imported) because `stream` imports `auth` and the reverse would be an import cycle.

Variables

View Source
var (
	// ErrTokenMissing — the request carried no JWT (the Authorization
	// header was absent or empty). Mapped onto CodeIdentityRequired
	// (HTTP 401) by the middleware.
	ErrTokenMissing = errors.New("auth: token missing")
	// ErrTokenMalformed — the JWT was not a valid three-segment string
	// or could not be base64-decoded. Mapped onto CodeAuthRejected
	// (HTTP 401).
	ErrTokenMalformed = errors.New("auth: token malformed")
	// ErrAlgNotAllowed — the JWT's `alg` header was not in the
	// asymmetric allowlist (HS*, `none`, or anything else). The parser
	// rejects this BEFORE the Keyfunc is consulted, so an algorithm-
	// confusion attack is structurally impossible. Mapped onto
	// CodeAuthRejected (HTTP 401).
	ErrAlgNotAllowed = errors.New("auth: signing algorithm not in asymmetric allowlist")
	// ErrSignatureInvalid — the JWT's signature did not verify against
	// the resolved key. Mapped onto CodeAuthRejected (HTTP 401).
	ErrSignatureInvalid = errors.New("auth: signature invalid")
	// ErrTokenExpired — the JWT's `exp` claim is in the past relative
	// to the validator's clock. Mapped onto CodeAuthRejected (HTTP 401).
	ErrTokenExpired = errors.New("auth: token expired")
	// ErrTokenNotYetValid — the JWT's `nbf` claim is in the future
	// relative to the validator's clock. Mapped onto CodeAuthRejected
	// (HTTP 401).
	ErrTokenNotYetValid = errors.New("auth: token not yet valid")
	// ErrUnknownKey — the JWT's `kid` header did not resolve to a
	// public key in the configured KeySet. Mapped onto
	// CodeAuthRejected (HTTP 401).
	ErrUnknownKey = errors.New("auth: kid does not resolve in key set")
	// ErrIdentityClaimMissing — the JWT verified but its claims did not
	// carry the mandatory (tenant, user, session) triple. Mapped onto
	// CodeIdentityRequired (HTTP 401) — RFC §5.5: "the Protocol rejects
	// any request without an identity scope."
	ErrIdentityClaimMissing = errors.New("auth: identity claim missing")
	// ErrAudienceMismatch — the JWT's `aud` claim did not match the
	// validator's configured audience (when WithAudience was supplied).
	// Mapped onto CodeAuthRejected (HTTP 401).
	ErrAudienceMismatch = errors.New("auth: audience mismatch")
	// ErrIssuerMismatch — the JWT's `iss` claim did not match the
	// validator's configured issuer (when WithIssuer was supplied).
	// Mapped onto CodeAuthRejected (HTTP 401).
	ErrIssuerMismatch = errors.New("auth: issuer mismatch")
)

Sentinel errors. Callers compare via errors.Is.

Each rejection path returns exactly one sentinel, wrapped with context — so a middleware mapping a Validate error onto a Protocol error code branches on the sentinel, not on the wrapped detail.

View Source
var (
	// ErrRotateMisconfigured — NewRotateSurface was called with a nil
	// TokenIssuer or redactor.
	ErrRotateMisconfigured = stderrors.New("auth: rotate-token surface missing a mandatory dependency")
	// ErrRotateIdentityRequired — the request carried an incomplete
	// identity triple. Maps onto CodeIdentityRequired (HTTP 401).
	ErrRotateIdentityRequired = stderrors.New("auth: rotate-token identity scope incomplete")
	// ErrRotateScopeRequired — the caller lacks the verified
	// `admin` scope claim. Maps onto CodeIdentityScopeRequired (403).
	ErrRotateScopeRequired = stderrors.New("auth: rotate-token requires the verified `admin` scope claim")
	// ErrRotateIdentityMismatch — the request body's identity scope
	// disagreed with the verified-JWT identity. Maps onto
	// CodeIdentityRequired (HTTP 401) — defence-in-depth.
	ErrRotateIdentityMismatch = stderrors.New("auth: rotate-token body identity disagrees with the verified token")
	// ErrRotateIssueFailed — the TokenIssuer failed to mint a token.
	// Maps onto CodeRuntimeError (HTTP 500).
	ErrRotateIssueFailed = stderrors.New("auth: rotate-token issuer failed to mint a token")
)

Rotation-surface sentinel errors. Callers (the wire handler) compare via errors.Is and map onto the canonical Protocol Code.

View Source
var AllowedAlgorithms = []string{
	"RS256", "RS384", "RS512",
	"ES256", "ES384", "ES512",
}

AllowedAlgorithms is the asymmetric-algorithm allowlist Harbor enforces (CLAUDE.md §7 rule 1). Six algorithms — three RSA-PKCS#1v1.5 (RS256/RS384/RS512) and three ECDSA (ES256/ES384/ES512). HS* and `none` are rejected at the parser level via jwt.WithValidMethods.

The list is exported (a) so tests pin it, (b) so an operator inspecting the binary can confirm the surface, (c) so a later phase adding a JWKS driver inherits the same list.

View Source
var ErrMisconfigured = errors.New("auth: NewValidator missing a mandatory dependency")

ErrMisconfigured — NewValidator was called with a nil KeySet. A validator without a key source cannot verify any token; fail closed rather than building one that rejects everything (CLAUDE.md §5).

Functions

func HasScope

func HasScope(ctx context.Context, s Scope) bool

HasScope reports whether ctx carries scope s. A request that has not been through the auth middleware (no scope set on ctx) returns false — the safe default for a privilege check is "absent = denied".

func IsValidScope

func IsValidScope(s Scope) bool

IsValidScope reports whether s is one of the canonical scopes. An unknown scope on a JWT is silently dropped from the verified set — a token that claims "future:scope" reads back as having no scopes rather than failing. The closed set means an attacker cannot grant themselves an undocumented privilege by inventing a scope name.

func Middleware

func Middleware(v Validator, opts ...MiddlewareOption) func(http.Handler) http.Handler

Middleware returns an http.Handler decorator that enforces JWT-bearer auth on every request.

The middleware:

  1. Reads the `Authorization: Bearer <token>` header. A missing or malformed header writes a 401 + CodeIdentityRequired Protocol error body and returns — `next` is never called.
  2. Calls Validator.Validate(ctx, token). A failure writes a 401 + the appropriate Protocol error code (CodeIdentityRequired for ErrIdentityClaimMissing / ErrTokenMissing; CodeAuthRejected for every other sentinel) and returns.
  3. On success, attaches the verified identity to r.Context() (via identity.With) and the verified scope set (via WithScopes), then calls next with the augmented request.

Middleware is a compiled artifact: every field is set once at construction and never mutated. The decorator is safe to share across N concurrent requests (D-025).

func SSEAccessTokenShim

func SSEAccessTokenShim(next http.Handler) http.Handler

SSEAccessTokenShim returns an http.Handler decorator that promotes an `?access_token=<jwt>` URL query parameter to a synthesized `Authorization: Bearer <jwt>` header BEFORE delegating to the standard auth.Middleware. It is the wire-side counterpart of the Console's EventSource SSE-subscribe path (`web/console/src/lib/protocol/client.ts` — "the bearer token rides as `access_token` — `EventSource` cannot carry an `Authorization` header").

Why an SSE-only shim

The standard browser `EventSource` API cannot set request headers (https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface) — there is no `Authorization` header path for an EventSource subscriber. The conventional workaround is the `access_token` URL query parameter (OAuth 2.0 bearer-token URI usage, RFC 6750 §2.3) for SSE endpoints only.

The shim is SSE-ONLY by design. Accepting the query parameter on every endpoint would leak the bearer token into:

  • browser history,
  • intermediary access logs (proxies, CDNs, runtime stderr access-log middleware),
  • the Referer header on any embedded link,
  • server-side request-dump panics.

Limiting the shim to the SSE endpoint scopes the leakage to the surface where EventSource has no alternative.

Behavior

On a request with NO Authorization header AND a non-empty `access_token` query parameter, the shim clones the request, sets `Authorization: Bearer <access_token>` on the clone, and calls next with the clone. The original request — and the original query parameter — is not mutated; downstream code sees a synthesized Authorization header as if the client had supplied one. The query param is intentionally LEFT in the URL on the cloned request so the SSE handler's URL parsing (event-type filters, admin flag) sees the same shape it always did.

On a request that ALREADY has an Authorization header, the shim is a pass-through: an explicit Authorization header is always preferred, and a same-origin Console (or a non-browser SSE client) that already sets the header gets the standard contract.

On a request with neither Authorization nor access_token, the shim is a pass-through; the standard middleware rejects the request with CodeIdentityRequired as it always did.

Concurrent reuse (D-025)

The shim wraps next once at construction and holds no mutable state; it is safe to share across N concurrent requests.

Round-3 walkthrough fix: pre-shim, the Console's cross-origin SSE subscribe got 401 on every request because the standard auth.Middleware only read Authorization. The CORS preflight pass (Phase 83v / D-162) unblocked the REST surface; this shim unblocks the SSE surface for the same multi-process Console+Runtime posture.

func WithScopes

func WithScopes(ctx context.Context, scopes []Scope) context.Context

WithScopes attaches the verified scope set to ctx. The middleware calls this once per request after Validator.Validate succeeds; the SSE handler reads the set back via HasScope to gate cross-tenant fan-in.

A nil scopes slice is permitted (a token with no scopes is still authenticated) — HasScope will return false for any scope check.

Types

type AdminScopeUsedPayload

type AdminScopeUsedPayload struct {
	events.SafeSealed
	// Actor is the verified admin identity at the Protocol edge.
	// V1 invariant: equals the JWT's verified `(tenant, user,
	// session)` triple.
	Actor IdentityTriple
	// Requester is the originating admin identity. V1 invariant:
	// equals Actor (single-hop impersonation only); diverges
	// post-V1 for delegated-impersonation chains.
	Requester IdentityTriple
	// Impersonating is the target identity the run executes
	// under. Complete `(tenant, user, session)` triple — missing
	// components fail loudly at the Protocol edge with
	// CodeIdentityRequired.
	Impersonating IdentityTriple
	// Reason is the stable sentinel name (e.g.
	// `AdminImpersonationReason = "impersonation"`).
	Reason string
	// Method is the Protocol method that carried the
	// impersonation (e.g. methods.MethodStart,
	// methods.MethodRedirect, methods.MethodUserMessage —
	// canonical method names live in internal/protocol/methods).
	Method string
}

AdminScopeUsedPayload is the typed payload on the `audit.admin_scope_used` canonical event when the emit source is an impersonation request (Phase 72b). The pre-existing emit site (the `events.Subscribe` admin-filter, Phase 05 / `internal/events/drivers/inmem`) continues to use `events.AdminScopeUsedPayload`; this richer typed payload is what the Phase 72b impersonation path publishes.

SafePayload by construction: every field is a bounded-string shaped identity component plus two enum strings. No caller-controlled bytes reach the bus — the wire shape rejects any deviation at the Protocol edge before reaching the emit.

Brief 11 §PG-5 verbatim names the three identity fields. The `Reason` field is the stable sentinel (`AdminImpersonationReason`); the `Method` field is the Protocol method that carried the impersonation (one of the ten canonical methods, typically `start` but `redirect` / `user_message` are accepted too — Phase 72b's non-goal explicitly names per-tool-call impersonation downgrade as post-V1, so the method stays one of the ten).

Phase 72b, D-107.

type AuthRejectedPayload

type AuthRejectedPayload struct {
	events.SafeSealed
	// Reason is the stable sentinel name (e.g. "token_expired").
	Reason string
	// KID is the JWT's `kid` header when known. Empty when the
	// rejection fired before the keyfunc was consulted.
	KID string
	// Issuer is the JWT's `iss` claim when known. Empty when the
	// rejection fired before claim extraction.
	Issuer string
	// Subject is the JWT's `sub` claim when known. Empty when the
	// rejection fired before claim extraction. The triple
	// (tenant/user/session) is NEVER emitted on this payload — a
	// rejected request never carries a verified identity, and
	// echoing back unverified claims would let an attacker confirm
	// which (tenant, user, session) triples are valid.
	Subject string
}

AuthRejectedPayload is the wire-side audit body for the auth.rejected event. The fields mirror what `Validator.audit` already emits to slog, so a subscriber sees the same redacted surface a log-scraping operator sees.

Subject + Issuer are zero-value strings when the JWT was rejected before claim extraction (e.g. a malformed token / algorithm confusion). KID is zero-value when the rejection happened before the keyfunc was consulted (e.g. an `Authorization` header that didn't carry a Bearer scheme at all).

Reason is the stable sentinel name from `reasonForWire` — one of `token_missing` / `token_malformed` / `alg_not_allowed` / `signature_invalid` / `token_expired` / `token_not_yet_valid` / `unknown_key` / `identity_claim_missing` / `audience_mismatch` / `issuer_mismatch` / `verification_failed`. A Protocol client branches on Reason; never on the wrapped human-readable detail (which may include operator-specific data we deliberately do not echo to an unauthenticated caller).

type AuthRotateTokenPayload

type AuthRotateTokenPayload struct {
	events.SafeSealed
	// Actor is the verified admin identity at the Protocol edge.
	Actor identity.Identity
	// Method is the Protocol method that carried the action.
	Method string
}

AuthRotateTokenPayload is the typed SafePayload published on the canonical `audit.admin_scope_used` event when an operator rotates their token. Phase 73m / D-129.

SafePayload by construction: every field is a bounded identity component or a Protocol method name — the re-minted token itself is NEVER on the payload (CLAUDE.md §7 "never log secrets").

type IdentityTriple

type IdentityTriple struct {
	// Tenant / User / Session are the flattened `(tenant, user, session)`
	// isolation triple the audit payload records — the wire-adjacent
	// mirror of the runtime's identity quadruple (no Run: an audit row
	// records the principal, not the per-execution scope).
	Tenant  string
	User    string
	Session string
}

IdentityTriple is the flat audit-visible shape of an IdentityScope (no nested Actor / Requester / Impersonating — those collapse to their triple at the payload boundary). Used as the Actor / Requester / Impersonating field of AdminScopeUsedPayload so the audit shape is purely flat strings; no caller-controlled bytes reach the bus.

IdentityTriple is intentionally distinct from identity.Identity: the audit payload lives on the wire-adjacent bus surface, not on the runtime's identity-quadruple surface. Mirroring the runtime type 1:1 would couple the audit shape to internal storage refactors (the same anti-pattern RFC §5.1 names for the wire IdentityScope). Phase 72b, D-107.

type KeySet

type KeySet interface {
	// KeyByID returns the public key + the algorithm name for kid.
	// alg MUST be one of AllowedAlgorithms; an alg outside the
	// allowlist is treated as ErrUnknownKey by the Validator (the
	// allowlist gate is at the parser, not the KeySet, but a KeySet
	// that returned an HMAC key would be rejected here as well).
	//
	// Returning a wrapped ErrUnknownKey signals the kid is not known.
	// Any other error is wrapped as ErrUnknownKey by the Validator.
	KeyByID(kid string) (key crypto.PublicKey, alg string, err error)
}

KeySet maps a JWT `kid` header to the public key + algorithm name to verify the token's signature with. Implementations MUST be safe for concurrent reads — the Validator calls KeyByID on every Validate.

The static implementation suffices for V1 + the `harbor dev` dev-token use case. A later phase can ship a JWKS driver that auto-refreshes from a URL behind the same interface — additive, no reshape.

type MiddlewareOption

type MiddlewareOption func(*middlewareConfig)

MiddlewareOption configures Middleware.

func MWLogger

func MWLogger(l *slog.Logger) MiddlewareOption

MWLogger sets the slog.Logger the middleware logs rejection paths to. A nil logger keeps slog.Default(). The validator carries its own logger for the audit emit; this one logs the wire-side rejection (the chosen Protocol error code, the HTTP status).

type Option

type Option func(*validatorConfig)

Option configures NewValidator.

func WithAudience

func WithAudience(aud string) Option

WithAudience sets the expected JWT `aud` claim. A token whose `aud` claim does not contain the expected value is rejected with ErrAudienceMismatch. An empty configured audience disables the check.

func WithClock

func WithClock(now func() time.Time) Option

WithClock overrides the validator's clock — used by tests to drive expiration / nbf checks deterministically. A nil clock keeps the default (time.Now).

func WithEventBus

func WithEventBus(b events.EventBus) Option

WithEventBus wires an events.EventBus into the Validator so the audit emit on every rejection ALSO publishes a canonical `auth.rejected` event onto the bus (PR #91 amendment to D-079). The bus is OPTIONAL — when not supplied (or nil), rejections still emit a structured slog.Warn through the configured Redactor; the bus emit is an additive observability surface that lets a Console subscribe to auth rejections through the Protocol's canonical event channel rather than scraping logs.

Production wiring (the registry-path NewMux path) SHOULD inject the bus so the Console sees rejections; the test-only escape hatch (a Validator constructed without a bus) keeps the existing per-package tests pinning the slog-only contract.

func WithIssuer

func WithIssuer(iss string) Option

WithIssuer sets the expected JWT `iss` claim. A token whose `iss` claim does not match is rejected with ErrIssuerMismatch. An empty configured issuer disables the check.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the slog.Logger the validator emits redacted audit records to. A nil logger keeps slog.Default().

func WithRedactor

func WithRedactor(r audit.Redactor) Option

WithRedactor sets the audit.Redactor the validator runs audit payloads through before logging. The redactor is **mandatory** — NewValidator fails closed with ErrMisconfigured when this option is not supplied (CLAUDE.md §7 rule 6: "every payload goes through audit.Redactor"; CLAUDE.md §13 "Test stubs as production defaults on operator-facing seams"). A nil redactor is treated as "unsupplied" and is rejected by NewValidator the same way.

type RotateOption

type RotateOption func(*RotateSurface)

RotateOption configures NewRotateSurface.

func WithRotateBus

func WithRotateBus(b events.EventBus) RotateOption

WithRotateBus wires the events.EventBus the surface publishes the `audit.admin_scope_used` event onto on every successful rotation. OPTIONAL — when unwired, the rotation is logged at Info instead (never fully silent — CLAUDE.md §13). A nil bus is treated as "WithRotateBus not supplied".

func WithRotateLogger

func WithRotateLogger(l *slog.Logger) RotateOption

WithRotateLogger sets the slog.Logger the surface logs to. A nil logger keeps slog.Default().

type RotateSurface

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

RotateSurface is the transport-agnostic `auth.rotate_token` handler. It is built once per Runtime process via NewRotateSurface and shared across every Protocol request; Rotate is safe for concurrent use by N goroutines (D-025) — every field is set once at construction and never mutated.

func NewRotateSurface

func NewRotateSurface(issuer TokenIssuer, redactor audit.Redactor, opts ...RotateOption) (*RotateSurface, error)

NewRotateSurface builds the `auth.rotate_token` surface. The TokenIssuer and the audit.Redactor are MANDATORY — a nil fails loud with ErrRotateMisconfigured rather than building a surface that would nil-panic or emit an unredacted audit payload (CLAUDE.md §5, §7 rule 6, §13).

The returned *RotateSurface is immutable after construction (D-025) and safe for concurrent use by N goroutines.

func (*RotateSurface) Rotate

Rotate handles the `auth.rotate_token` method. `verified` is the caller's verified JWT identity + scopes (from auth.Middleware); `req` is the decoded wire request. The surface asserts the body identity against the verified identity, gates on the `admin` scope, re-mints the token, and emits the audit event.

Returns the wire response on success, or one of the package's typed sentinels on failure — the wire handler maps each onto a canonical Protocol Code.

type Scope

type Scope string

Scope is a verified JWT scope claim — a privilege the Protocol consults when granting cross-session / cross-tenant subscriptions or fleet-control privileges (RFC §4.2 + §5.5: "Extended scopes (admin, console:fleet) gate cross-session and cross-tenant subscriptions").

Scopes are not isolation principals — the (tenant, user, session) triple is and stays the isolation key (CLAUDE.md §6 rule 1). A scope is an *additional* entitlement carried alongside the triple.

const (
	ScopeAdmin        Scope = "admin"
	ScopeConsoleFleet Scope = "console:fleet"
)

Canonical scope constants. The set is closed at V1 — adding a new scope is a Protocol-surface phase, not an ad-hoc addition.

ScopeAdmin is the cross-tenant fan-in entitlement: a Subscribe call with `events.Filter.Admin = true` requires this scope (RFC §6.13 admin subscriptions). The Phase 05 events.ErrAdminScopeRequired sentinel is the corresponding error.

ScopeConsoleFleet is the fleet-observation entitlement (RFC §7 "Fleet privilege tiers"): a Console managing multiple Runtimes uses this scope to subscribe to events from outside its single (tenant, user, session) triple. Distinct from a hypothetical "fleet:control" scope (deferred per D-066).

func CanonicalScopes

func CanonicalScopes() []Scope

CanonicalScopes returns a copy of the closed canonical scope set. Used by tests to pin the surface and by the audit emitter to render the per-request scope set deterministically.

func ScopesFrom

func ScopesFrom(ctx context.Context) ([]Scope, bool)

ScopesFrom returns the verified scope set on ctx, and a presence bool. A request that has not been through the auth middleware has no scopes attached — ScopesFrom returns (nil, false), which is distinct from ScopesFrom returning (nil, true) for a token with no scopes.

type TokenIssuer

type TokenIssuer interface {
	// IssueToken mints a fresh Bearer-shaped JWT for the supplied
	// identity triple + scope set, expiring at `now + TTL`. The
	// returned string is the raw token; expiresAt is its expiry, UTC.
	// The caller has already verified the identity — IssueToken does
	// not re-validate it.
	IssueToken(ctx context.Context, id identity.Identity, scopes []Scope, now time.Time) (token string, expiresAt time.Time, err error)
}

TokenIssuer re-mints a Protocol-auth JWT for an already-verified identity. The V1 implementation is the `harbor dev` ephemeral signer; a post-V1 release-engineering phase fits an OIDC token-exchange issuer (RFC 8693) behind the same shape.

An implementation MUST be safe for concurrent use by N goroutines (D-025) — RotateSurface shares one issuer across every request.

type Validator

type Validator interface {
	// Validate parses + verifies the rawToken JWT and returns the
	// extracted identity + scopes. Every error wraps one of the
	// package's typed sentinels — callers compare via errors.Is.
	Validate(ctx context.Context, rawToken string) (Verified, error)
}

Validator is the JWT validation surface. Construct via NewValidator; do not construct directly. One Validator is safe to share across N concurrent Validate goroutines (D-025).

func NewValidator

func NewValidator(keys KeySet, opts ...Option) (Validator, error)

NewValidator builds a JWT Validator over the supplied KeySet.

Both the KeySet AND an audit.Redactor (via WithRedactor) are mandatory. A nil KeySet — or omission of WithRedactor — fails loud with ErrMisconfigured rather than building a validator that would reject every token (KeySet) or log raw payloads unredacted (Redactor; CLAUDE.md §7 rule 6 — "every payload goes through audit.Redactor" — and CLAUDE.md §13 "Test stubs as production defaults on operator-facing seams"). Production callers wire `audit/drivers/patterns.New()` as the redactor; tests wire a real or test-local Redactor via the `auth_test` package or a _test.go-local stub.

The returned Validator is immutable after construction (D-025) and safe for concurrent use by N goroutines.

type Verified

type Verified struct {
	// Identity is the (tenant, user, session) triple extracted from the
	// JWT's mandatory claims. Validates clean against identity.Validate
	// — the Validator already ran that check.
	Identity identity.Identity
	// Scopes is the verified scope set the JWT carried. May be empty;
	// a token with no scopes is still authenticated, just not entitled
	// to any elevated subscription. Membership is checked via
	// auth.HasScope.
	Scopes []Scope
	// Subject is the JWT's `sub` claim, if present. Audited; never used
	// as an isolation principal (the triple is the isolation key).
	Subject string
	// Issuer is the JWT's `iss` claim, if present. Audited.
	Issuer string
}

Verified is the result of a successful Validate call.

Jump to

Keyboard shortcuts

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