Documentation
¶
Overview ¶
Package identity verifies who a gateway request is from. Cohort assignment must only ever use a Subject produced here: never a header a client could have set itself.
Index ¶
- Constants
- Variables
- func BearerToken(h http.Header) string
- func CacheOnly(ctx context.Context) context.Context
- func CheckChain(chain []policy.Actor) error
- func CheckHumanID(id string) error
- func IsWorkloadToken(tok string) bool
- func ParseWorkloadKeys(pemData string) ([]ed25519.PublicKey, error)
- func SignDelegatedToken(key ed25519.PrivateKey, c DelegatedClaims) (string, error)
- func SignMCPToken(key ed25519.PrivateKey, c MCPClaims) (string, error)
- func SignWorkloadToken(key ed25519.PrivateKey, c WorkloadClaims) (string, error)
- func SubjectFromClaims(claims map[string]any, cfg Options) (policy.Subject, error)
- func TokenType(tok string) string
- func WorkloadKeyID(pub ed25519.PublicKey) string
- type Act
- type DelegatedClaims
- type MCPClaims
- type OIDCJWT
- type Options
- type Resolver
- type TrustedHeader
- type Verifier
- type WorkloadClaims
- type WorkloadJWT
Constants ¶
const ( // DelegatedTokenType is the JWT typ header of a delegated credential. DelegatedTokenType = "halo-agent+jwt" //nolint:gosec // JWT typ value, not a credential // KindAgent is the kind claim of a delegated credential. KindAgent = "agent" )
Delegated agent credentials (ADR-0024): halo-server exchanges a verified Halos credential (a human session, a workload credential, or another delegated credential) for a short-lived JWT naming the agent that acts for the root principal. The act claim is RFC 8693 section 4.1: the top-level act is the current actor and each nested act a prior one. Signed with the workload key and verified by the same code at halo-server and the gateways.
const ( // MCPTokenType is the JWT typ header of an MCP access token. MCPTokenType = "halo-mcp+jwt" //nolint:gosec // JWT typ value, not a credential // MCPTokenTTL is the lifetime cap of an MCP access token: harness header // helpers run on every connection, so a short token costs nothing. MCPTokenTTL = 5 * time.Minute )
MCP access tokens (ADR-0029): for a registry server with auth mode halos, halo-server is the authorization server. It mints a short-lived token whose audience is the server's canonical URL (RFC 8707), so the token is good at that one server only: gateways refuse it (wrong typ and aud) and the server never receives, or passes on, the caller's own Halos credential.
const ( ModeJWT = "jwt" ModeTrustedHeader = "trusted_header" ModeNone = "none" // no verification: every caller is anonymous (default routing) )
Modes.
const ( // WorkloadTokenType is the JWT typ header. Gateways dispatch on it; it is // never trusted for verification. WorkloadTokenType = "halo-workload+jwt" //nolint:gosec // JWT typ value, not a credential WorkloadIssuer = "halos" WorkloadAudience = "halos-gateway" )
Workload credentials (ADR-0023): halo-server exchanges a CI / cloud OIDC token for a short-lived JWT it signs with an ed25519 key; gateways verify it with the public half. EdDSA only: there is no alg negotiation to confuse.
Variables ¶
var AllowedAlgs = []string{"RS256", "ES256", "EdDSA"}
AllowedAlgs is the JWT signature allowlist. "none" and HS* are never accepted: a shared-secret alg would let anyone holding the (public) JWKS key forge tokens (key confusion).
var ClientCredentialHeaders = []string{"Authorization", "X-Api-Key", "Api-Key", "X-Goog-Api-Key", "Proxy-Authorization"}
ClientCredentialHeaders are the headers a caller can use to present a credential to the gateway. None is ever a gateway-to-provider credential.
var ErrAmbiguous = errors.New("identity: identity header sent more than once")
ErrAmbiguous is returned when a trusted identity/groups header appears more than once; adapters answer 400 rather than guess which value is real.
var ErrClaimsRejected = errors.New("identity: token signature valid, claims rejected")
ErrClaimsRejected marks a token whose signature verified against a trusted key but whose claims did not pass (expired, wrong audience, not yet valid, a revoked session, ...). It is not a forgery: callers that rate-limit failed attempts per client IP do not count it, because anyone on a shared CI runner can mint such tokens and would otherwise lock out its other jobs.
var ErrDiscovery = errors.New("identity: oidc discovery")
ErrDiscovery: the issuer's discovery document could not be fetched, so no token was judged (an outage, not a bad credential).
var ErrNoCredentials = errors.New("identity: no credentials")
ErrNoCredentials is returned when the request carries nothing to verify.
var ErrTokenExpired = errors.New("expired")
ErrTokenExpired wraps the expiry refusal of a halo-server workload or delegated credential. Its signature has already verified when it is returned, so the holder is genuine and only late.
Functions ¶
func BearerToken ¶
BearerToken returns the caller's token from "Authorization: Bearer" or, for Anthropic SDKs, "x-api-key".
func CacheOnly ¶ added in v0.3.0
CacheOnly marks ctx so OIDC verification uses only discovery documents and JWKS keys already cached, never fetching: a request from a client IP blocked for failed attempts can still prove it holds a signed token without being able to make halo-server call the issuer.
func CheckChain ¶ added in v0.3.0
CheckChain enforces the structure every delegation chain must have: 1 to policy.MaxAgentDepth actors, each a valid agent name, none twice (a cycle), harness and session labels within their length and character set.
func CheckHumanID ¶ added in v0.3.0
CheckHumanID refuses a human identity that carries the workload subject prefix (any case): only a verified workload credential may produce one, so a human can never take a workload's rules, budgets or audit trail.
func IsWorkloadToken ¶ added in v0.3.0
IsWorkloadToken reports whether tok claims (unverified) to be a workload credential. Used only to pick a verifier.
func ParseWorkloadKeys ¶ added in v0.3.0
ParseWorkloadKeys reads one or more PEM "PUBLIC KEY" blocks (PKIX ed25519, as `halo keys generate` writes). Several keys allow rotation.
func SignDelegatedToken ¶ added in v0.3.0
func SignDelegatedToken(key ed25519.PrivateKey, c DelegatedClaims) (string, error)
SignDelegatedToken signs c with the workload key. It refuses claims VerifyDelegatedToken would reject.
func SignMCPToken ¶ added in v0.4.0
func SignMCPToken(key ed25519.PrivateKey, c MCPClaims) (string, error)
SignMCPToken signs c with the workload key. It refuses claims VerifyMCPToken would reject.
func SignWorkloadToken ¶ added in v0.3.0
func SignWorkloadToken(key ed25519.PrivateKey, c WorkloadClaims) (string, error)
SignWorkloadToken signs c. It refuses claims Verify would reject, so a bug upstream can never mint an over-long or non-workload credential.
func SubjectFromClaims ¶
SubjectFromClaims derives the caller from verified token claims using cfg.UserClaim / cfg.GroupsClaim (defaults "email" / "groups"). When the user claim is "email" the token must also carry email_verified=true (bool or "true"): many IdPs let users set an unverified email, which would let them pick someone else's identity. cfg.TrustUnverifiedEmail opts out.
func TokenType ¶ added in v0.3.0
TokenType returns the (unverified) typ header of a compact JWT, "" when unreadable. Used only to pick a verifier.
func WorkloadKeyID ¶ added in v0.3.0
WorkloadKeyID is the kid for pub: lets gateways hold several keys (rotation).
Types ¶
type Act ¶ added in v0.3.0
type Act struct {
Subject string `json:"sub"` // "agent:<name>"
Harness string `json:"harness,omitempty"` // where it runs, e.g. claude-code
Session string `json:"session,omitempty"` // the harness session id
Act *Act `json:"act,omitempty"`
}
Act is one RFC 8693 actor claim: the actor, and in Act the actor before it.
func ActFromChain ¶ added in v0.3.0
ActFromChain builds the nested act claim from a chain listed root-side first.
type DelegatedClaims ¶ added in v0.3.0
type DelegatedClaims struct {
Issuer string `json:"iss"`
Subject string `json:"sub"`
Audience string `json:"aud"`
Groups []string `json:"groups,omitempty"`
Kind string `json:"kind"`
RootKind string `json:"root_kind,omitempty"`
Act *Act `json:"act"`
// Models narrows the aliases the credential may call; absent = not
// narrowed by the credential (policy still applies). halo-server never
// mints an empty scope: an empty intersection is refused, not widened.
Models []string `json:"models,omitempty"`
// AuthTime is when the root authenticated (OIDC auth_time): a session
// revoked after it kills the credential at its next exchange.
AuthTime int64 `json:"auth_time,omitempty"`
Parent string `json:"parent_jti,omitempty"` // jti of the credential it was exchanged from
// Ancestors are the jtis of the credentials before the parent (its
// parent first), so revoking any ancestor's jti (ADR-0026) also kills
// this credential. Absent when the parent was minted from a session or
// from nothing with a jti.
Ancestors []string `json:"anc_jti,omitempty"`
IssuedAt int64 `json:"iat"`
NotBefore int64 `json:"nbf"`
Expiry int64 `json:"exp"`
ID string `json:"jti"`
}
DelegatedClaims is the payload of a delegated credential. Subject and Groups are the root principal's (RootKind "" human, "workload"); Act is the chain.
func VerifyDelegatedToken ¶ added in v0.3.0
func VerifyDelegatedToken(keys []ed25519.PublicKey, tok string, now time.Time) (DelegatedClaims, error)
VerifyDelegatedToken checks the signature against keys (by kid), then the claims: iss, aud, kind, root subject, act chain (depth <= policy.MaxAgentDepth, no agent twice), exp, nbf, iat and the lifetime cap.
func (DelegatedClaims) AsSubject ¶ added in v0.3.0
func (c DelegatedClaims) AsSubject() policy.Subject
AsSubject is the verified caller the gateway routes: the root principal with the delegation and the credential's lineage (for revocation) attached.
func (DelegatedClaims) Chain ¶ added in v0.3.0
func (c DelegatedClaims) Chain() []policy.Actor
Chain returns the actors root-side first (the current actor last).
func (DelegatedClaims) Lineage ¶ added in v0.4.0
func (c DelegatedClaims) Lineage() []string
Lineage is the credential's jti, then its parent's and the ancestors'.
func (DelegatedClaims) ModelScope ¶ added in v0.3.0
func (c DelegatedClaims) ModelScope() []string
ModelScope returns the credential's model scope (nil = not narrowed).
type MCPClaims ¶ added in v0.4.0
type MCPClaims struct {
Issuer string `json:"iss"`
Subject string `json:"sub"`
Audience string `json:"aud"` // canonical server URL (policy.CanonicalResource)
Groups []string `json:"groups,omitempty"`
// Kind is the subject's kind ("" human, workload); Actors the delegation
// chain when an agent asked, root-side first. Attribution for the server.
Kind string `json:"kind,omitempty"`
Actors []string `json:"actors,omitempty"`
// Server is the registry id the token was minted for.
Server string `json:"mcp_server"`
IssuedAt int64 `json:"iat"`
NotBefore int64 `json:"nbf"`
Expiry int64 `json:"exp"`
ID string `json:"jti"`
}
MCPClaims is the payload of an MCP access token.
func VerifyMCPToken ¶ added in v0.4.0
func VerifyMCPToken(keys []ed25519.PublicKey, tok, resource string, now time.Time) (MCPClaims, error)
VerifyMCPToken is what an MCP server whose authorization server is Halos runs on each request: the signature against halo-server's workload public keys (by kid), then iss, aud (must be resource, canonicalised), exp, nbf, iat and the lifetime cap.
type OIDCJWT ¶
type OIDCJWT struct {
Issuer, Audience string
UserClaim, GroupsClaim string
// TrustUnverifiedEmail accepts an "email" user claim without
// email_verified=true. Off by default: see SubjectFromClaims.
TrustUnverifiedEmail bool
// contains filtered or unexported fields
}
OIDCJWT verifies a bearer JWT issued by the org's IdP: discovery + JWKS (cached, refetched when no cached key verifies so key rotation works; never fetched under CacheOnly), then iss / aud / exp / nbf and the alg allowlist.
func NewOIDCJWT ¶
func NewOIDCJWT(issuer, audience, userClaim, groupsClaim string, client *http.Client) (*OIDCJWT, error)
NewOIDCJWT never touches the network; discovery happens on first Verify (and is retried while it fails), so a proxy can start before the IdP is reachable. client may be nil.
func (*OIDCJWT) Claims ¶ added in v0.3.0
Claims verifies tok (signature via JWKS, alg allowlist, iss, aud, exp, nbf) and returns its claims. A token whose signature verified but whose claims failed is reported with ErrClaimsRejected. Under CacheOnly(ctx) nothing is fetched: no discovery, no JWKS refetch.
type Options ¶
type Options struct {
Mode string
Issuer, Audience string
UserClaim, GroupsClaim string
IdentityHeader string
GroupsHeader string
TrustedProxyCIDRs []string
// TrustUnverifiedEmail: accept userClaim "email" without email_verified=true.
// Default false; only for IdPs that never issue unverified emails.
TrustUnverifiedEmail bool
// WorkloadKeys is PEM (one or more ed25519 PUBLIC KEY blocks) of
// halo-server's workload signing key(s). When set, workload credentials
// (ADR-0023) are accepted besides the mode's own callers. Ignored in mode none.
WorkloadKeys string
}
Options is per-binary identity config. Empty fields fall back to the org's policy (Org.Identity / Org.Gateway.Auth), so most deployments set nothing.
type Resolver ¶
type Resolver struct {
// contains filtered or unexported fields
}
Resolver caches the Verifier across policy hot-reloads: it is rebuilt only when the effective options change (so the JWKS cache survives reloads).
type TrustedHeader ¶
type TrustedHeader struct {
IdentityHeader string
GroupsHeader string
// contains filtered or unexported fields
}
TrustedHeader reads identity from headers set by an auth gateway. That is only safe when the request provably came from that gateway, so it refuses to exist without a CIDR allowlist for the peer address.
func NewTrustedHeader ¶
func NewTrustedHeader(identityHeader, groupsHeader string, trustedProxyCIDRs []string) (*TrustedHeader, error)
type Verifier ¶
Verifier authenticates a request and returns the caller. Any error means the caller is unverified.
type WorkloadClaims ¶ added in v0.3.0
type WorkloadClaims struct {
Issuer string `json:"iss"`
Subject string `json:"sub"`
Audience string `json:"aud"`
Groups []string `json:"groups,omitempty"`
Kind string `json:"kind"`
Rule string `json:"rule"` // workloads.rules name that matched
IssuedAt int64 `json:"iat"`
NotBefore int64 `json:"nbf"`
Expiry int64 `json:"exp"`
ID string `json:"jti"`
}
WorkloadClaims is the payload of a workload credential.
func VerifyWorkloadToken ¶ added in v0.3.0
func VerifyWorkloadToken(keys []ed25519.PublicKey, tok string, now time.Time) (WorkloadClaims, error)
VerifyWorkloadToken checks the signature against keys (by kid), then iss, aud, kind, subject prefix, exp, nbf, iat and the lifetime cap.
func (WorkloadClaims) AsSubject ¶ added in v0.4.0
func (c WorkloadClaims) AsSubject() policy.Subject
AsSubject is the verified workload caller, with its credential attached for revocation checks (ADR-0026).
type WorkloadJWT ¶ added in v0.3.0
type WorkloadJWT struct {
Keys []ed25519.PublicKey
Next Verifier
Now func() time.Time // nil = time.Now
}
WorkloadJWT accepts halo-server workload and delegated agent credentials and hands every other caller to Next (the human verifier). A token whose typ claims to be either is verified as such or refused: never a fall-through.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package identitytest is a tiny OIDC issuer (discovery + JWKS + token minting + an auto-approving authorization-code/PKCE flow) for tests and the compose demo.
|
Package identitytest is a tiny OIDC issuer (discovery + JWKS + token minting + an auto-approving authorization-code/PKCE flow) for tests and the compose demo. |