fix(auth): pin hanzo IAM issuer to hanzo.id + reject missing exp (red FIX1/INFO)
FIX1 (red, HIGH — the deploy landmine): brand.go defaulted the `hanzo` brand IAMIssuer to https://iam.hanzo.ai, but the live .well-known/openid-configuration on BOTH hanzo.id and iam.hanzo.ai reports issuer=https://hanzo.id + jwks_uri=https://hanzo.id/v1/iam/.well-known/jwks (iam.hanzo.ai is a routing alias, not the token issuer). With the baked default, SanitizeIdentity's issuer check would fail on every real token -> every principal anonymized -> ALL global admin gets 403 (fail-secure, no forgery opened, but admin broken platform-wide). The cloud CLI already defaults to hanzo.id; lux/zoo/pars already point at their own .id issuers. Pin hanzo -> https://hanzo.id so the correct config is default-by-default. (JWKS derivation then yields the correct hanzo.id JWKS.) INFO (red): go-jose ValidateWithLeeway only enforces exp when present (`if c.Expiry != nil`), so a token with NO exp would never expire. Reject a missing exp explicitly, exactly like a missing iss. +1 test (22 green). No subsystem reads cfg.IAMIssuer except SanitizeIdentity + a log line, so the brand default change is contained.
This commit is contained in:
@@ -104,6 +104,12 @@ func (v *identityValidator) validate(raw string) (*idClaims, error) {
|
||||
if claims.Issuer == "" {
|
||||
return nil, fmt.Errorf("missing issuer")
|
||||
}
|
||||
// Reject a missing expiry: ValidateWithLeeway only enforces exp when present
|
||||
// (it checks `if c.Expiry != nil`), so a token with NO exp would never expire.
|
||||
// An IAM access token always carries exp; require it.
|
||||
if claims.Expiry == nil {
|
||||
return nil, fmt.Errorf("missing expiry")
|
||||
}
|
||||
expected := jwt.Expected{Issuer: v.issuer}
|
||||
if len(v.audiences) > 0 {
|
||||
expected.AnyAudience = jwt.Audience(v.audiences)
|
||||
|
||||
@@ -29,10 +29,19 @@ type BrandInfo struct {
|
||||
}
|
||||
|
||||
// brands is the brand→IAM registry. Keys are the canonical brand IDs accepted
|
||||
// by CLOUD_BRAND. Per HIP-0111 §Brands: hanzo→iam.hanzo.ai, lux→lux.id,
|
||||
// by CLOUD_BRAND. Per HIP-0111 §Brands: hanzo→hanzo.id, lux→lux.id,
|
||||
// zoo→zoo.id, pars→pars.id, bootnode→id.bootno.de.
|
||||
//
|
||||
// IAMIssuer MUST equal the `iss` IAM actually stamps AND host the signing JWKS.
|
||||
// For hanzo the live .well-known/openid-configuration on BOTH hanzo.id and
|
||||
// iam.hanzo.ai reports issuer=https://hanzo.id + jwks_uri=
|
||||
// https://hanzo.id/v1/iam/.well-known/jwks (iam.hanzo.ai is a routing alias, not
|
||||
// the issuer), and the cloud CLI already defaults to hanzo.id. Pinning
|
||||
// iam.hanzo.ai here would fail the issuer check on every real token, anonymizing
|
||||
// every principal — global admin would 403 platform-wide (fail-secure, but
|
||||
// broken). lux/zoo/pars already correctly point at their own .id issuers.
|
||||
var brands = map[string]BrandInfo{
|
||||
"hanzo": {ID: "hanzo", IAMIssuer: "https://iam.hanzo.ai", Domain: "hanzo.ai"},
|
||||
"hanzo": {ID: "hanzo", IAMIssuer: "https://hanzo.id", Domain: "hanzo.ai"},
|
||||
"lux": {ID: "lux", IAMIssuer: "https://lux.id", Domain: "lux.network"},
|
||||
"zoo": {ID: "zoo", IAMIssuer: "https://zoo.id", Domain: "zoo.ngo"},
|
||||
"pars": {ID: "pars", IAMIssuer: "https://pars.id", Domain: "pars.network"},
|
||||
|
||||
@@ -306,6 +306,15 @@ func TestIdentityValidator(t *testing.T) {
|
||||
t.Fatal("expired token must be rejected")
|
||||
}
|
||||
})
|
||||
t.Run("missing expiry rejected", func(t *testing.T) {
|
||||
// go-jose only enforces exp when present; a token with NO exp would never
|
||||
// expire. We reject it explicitly.
|
||||
c := tokenClaims("hanzo-console", "admin", "", true, future)
|
||||
c.Expiry = nil
|
||||
if _, err := v.validate(signWith(t, key, c)); err == nil {
|
||||
t.Fatal("token without exp must be rejected")
|
||||
}
|
||||
})
|
||||
t.Run("bad signature rejected", func(t *testing.T) {
|
||||
if _, err := v.validate(signWith(t, other, tokenClaims("hanzo-console", "admin", "", true, future))); err == nil {
|
||||
t.Fatal("token signed by an unknown key must be rejected")
|
||||
|
||||
Reference in New Issue
Block a user