Files
zt/controller/model/token_provider_cache_test.go
zeekayandHanzo Dev 9ba31aa5c3 Bootstrap the controller and router from IAM
With no subcommand, `zt controller` and `zt router` run the two roles of
one pod from the environment. The controller creates its PKI, config and
database on first boot and makes Hanzo IAM the only way in: an ext-jwt
signer keyed on `sub`, an auth policy admitting only it, an admin per
ZT_IAM_ADMINS subject, and password login refused by the default policy.
The default admin the database requires gets a random password held
only as its hash. Later boots create what is missing and touch nothing
else.

An ext-jwt signer demanded an audience match even when none was set, so
one IAM issuing tokens to many clients could not be trusted as a whole.
An unset audience now accepts any; issuer and signature still decide.

Router enrollment verifies its token against the certificate the
token's issuer serves and posts back there. Behind a TLS-terminating
proxy that is the proxy's certificate, so the router beside its
controller enrolls over loopback instead.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 00:58:14 -07:00

51 lines
1.7 KiB
Go

package model
import (
"crypto/x509"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/hanzozt/zt/v2/controller/db"
cmap "github.com/orcaman/concurrent-map/v2"
"github.com/stretchr/testify/require"
)
func TestVerifyTokenByInspectionAudience(t *testing.T) {
ca := newRootCa()
leaf := ca.NewLeafWithAKID()
key, err := newKey(leaf.cert, []*x509.Certificate{leaf.cert, ca.cert})
require.NoError(t, err)
resolver, err := newTestJwksResolver()
require.NoError(t, err)
resolver.AddKey(key, leaf.key)
issuer, endpoint, claim := "https://hanzo.id", "https://hanzo.id/v1/iam/.well-known/jwks", "sub"
signer := &db.ExternalJwtSigner{Name: "iam", Issuer: &issuer, JwksEndpoint: &endpoint, IdentityIdClaimsSelector: &claim, Enabled: true}
cache := &TokenIssuerCache{issuers: cmap.New[TokenIssuer]()}
cache.issuers.Set(issuer, &TokenIssuerExtJwt{externalJwtSigner: signer, jwksResolver: resolver, kidToPubKey: map[string]IssuerPublicKey{}})
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.RegisteredClaims{
Issuer: issuer,
Subject: "admin/hanzo-cloud",
Audience: jwt.ClaimStrings{"hanzo-cloud"},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute)),
})
token.Header["kid"] = key.KeyId
signed, err := token.SignedString(leaf.key)
require.NoError(t, err)
t.Run("a signer without an audience accepts any", func(t *testing.T) {
result, err := cache.VerifyTokenByInspection(signed)
require.NoError(t, err)
require.Equal(t, "admin/hanzo-cloud", result.IdClaimValue)
})
t.Run("a signer with an audience requires it", func(t *testing.T) {
audience := "hanzo-cli"
signer.Audience = &audience
_, err := cache.VerifyTokenByInspection(signed)
require.Error(t, err)
})
}