mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
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]>
70 lines
2.6 KiB
Go
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)
|
|
}
|