Files
zeekayandHanzo Dev e3636baf8e tasks: the trusted issuer is a set, and an empty entry is not one
TASKSD_WHITELABEL_ISSUERS adds the brands this tasksd accepts tokens from beyond
the primary, each of which signs its own. Adding one is configuration.

The sharp edge is the empty entry: splitting an UNSET env var yields one empty
string, and an empty trusted issuer would match a token that carries no `iss` at
all. Entries are trimmed and dropped, pinned by
TestIssuerAllowlistDropsEmptyEntries — which builds the verifier through the same
code path production uses, so the guard is tested where it runs rather than in
isolation.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-30 05:20:14 -07:00

70 lines
2.6 KiB
Go

// Copyright © 2026 Hanzo AI. MIT License.
// The credential policy, from tasksd's own environment.
//
// The CHECK itself — keys, issuer, audience, the header contract — is
// hanzoai/authz/edge. This file is only the part that is genuinely tasksd's: which
// environment variables the values come from.
//
// A reader used to live here, and it had two defects a shared one does not: it
// verified against EVERY key in the published set rather than the one the token's
// `kid` named, so a token naming key A was accepted on a signature from key B; and
// its issuer comparison was conditional on the configured issuer being non-empty, so
// an unset TASKSD_JWT_ISSUER accepted tokens from anyone instead of failing closed.
package auth
import (
"slices"
"strings"
"time"
"github.com/hanzoai/authz/edge"
)
// JWTConfig configures the validator. An empty JWKSURL disables JWT validation, and
// RequireIdentity then refuses every request when require is set.
type JWTConfig struct {
JWKSURL string // e.g. https://hanzo.id/v1/iam/.well-known/jwks
// Issuer is the primary trusted issuer, e.g. https://hanzo.id. It is widened by
// Issuers for a deployment that fronts more than one brand; an empty result
// refuses every token rather than silently disabling the check.
Issuer string
// Issuers are ADDITIONAL trusted issuers beyond Issuer — the white-label brands
// this tasksd accepts tokens from, each of which signs its own.
Issuers []string
Audience string // optional; "" → audience check skipped
TTL time.Duration // JWKS cache TTL; 0 → the edge's default
}
// NewValidator returns nil when cfg names no JWKS URL (JWT disabled).
//
// The audience is optional here and that is deliberate, not an oversight: IAM sets
// `aud` per RFC 8707 to the requesting CLIENT, so it names who asked for the token
// rather than who may accept it. tasksd is not a client id, so it pins the issuer and
// the signing keys and lets any first-party client's token through.
func NewValidator(cfg JWTConfig) *edge.Verifier {
if cfg.JWKSURL == "" {
return nil
}
var audiences []string
if cfg.Audience != "" {
audiences = []string{cfg.Audience}
}
issuers := make([]string, 0, 1+len(cfg.Issuers))
for _, iss := range append([]string{cfg.Issuer}, cfg.Issuers...) {
// An empty entry is dropped, not trusted: splitting an unset
// TASKSD_WHITELABEL_ISSUERS yields one empty string, and an empty issuer would
// match a token that carries none.
if iss = strings.TrimSpace(iss); iss == "" {
continue
}
if !slices.Contains(issuers, iss) {
issuers = append(issuers, iss)
}
}
return edge.NewVerifier(cfg.JWKSURL, issuers, audiences, cfg.TTL)
}