Files
jwt/class_test.go
zooqueen 372409fd10 the issuer states the identity class, so a verifier stops inferring it
IAM stamps `type` on a token the client_credentials grant minted and leaves it
off a person's, resolving it from the grant shape — a fact only the issuer holds
and no caller can ask for. This Claims never carried the field, so every
consumer reading these claims had to work the class out for itself.

Each available inference is wrong somewhere. The subject's shape is the tempting
one, since a machine subject reads `owner/name`; but GetUserBySubject resolves
that same form to a PERSON whenever the token names the account rather than its
id, so the inference reports those people as machines. A consumer that files the
two classes in two places — which a custody path must, or one principal's path
becomes another's — then files a person under a machine's name.

Field parity with what IAM emits is this package's whole reason to exist, so the
claim belongs here rather than being read a second time beside it.
2026-08-20 14:15:19 -07:00

81 lines
2.2 KiB
Go

package jwt_test
import (
"encoding/json"
"strings"
"testing"
"time"
"github.com/hanzoai/jwt"
)
// A verifier that files two identity classes in two places needs the issuer to
// say which class it minted. These pin that the answer survives the wire.
func TestAProgramSaysSoOnTheWire(t *testing.T) {
key := rsaKey(t)
raw, err := jwt.Sign(key, &jwt.Claims{
User: &jwt.User{Owner: "hanzo", Name: "hanzo-egress"},
Type: jwt.Program,
Azp: "hanzo-egress",
RegisteredClaims: jwt.RegisteredClaims{
Subject: "admin/hanzo-egress",
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
})
if err != nil {
t.Fatalf("sign: %v", err)
}
got, err := jwt.Parse(raw, key)
if err != nil {
t.Fatalf("parse: %v", err)
}
if got.Type != jwt.Program {
t.Errorf("type = %q, want %q", got.Type, jwt.Program)
}
}
// A person carries no class at all, so the claim must be absent rather than
// present-and-empty: a verifier reading it is asking "is this a program", and an
// empty string answers no only if it is the ONE spelling of no.
func TestAPersonCarriesNoClass(t *testing.T) {
key := rsaKey(t)
raw, err := jwt.Sign(key, &jwt.Claims{
User: &jwt.User{Owner: "hanzo", Name: "z"},
RegisteredClaims: jwt.RegisteredClaims{
Subject: "b474eaa5-e000-474d-b317-c37bbd9a6945",
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
})
if err != nil {
t.Fatalf("sign: %v", err)
}
got, err := jwt.Parse(raw, key)
if err != nil {
t.Fatalf("parse: %v", err)
}
if got.Type != "" {
t.Errorf("a person's token carried type %q", got.Type)
}
body, err := json.Marshal(&jwt.Claims{User: &jwt.User{Owner: "hanzo"}})
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(body), `"type"`) {
t.Errorf("an empty class was emitted anyway: %s", body)
}
}
// The claim reaches this package under the name the issuer writes. A rename on
// either side is a verifier that silently reads every program as a person.
func TestTheClassIsReadUnderTheIssuersName(t *testing.T) {
var c jwt.Claims
if err := json.Unmarshal([]byte(`{"type":"application","owner":"hanzo"}`), &c); err != nil {
t.Fatal(err)
}
if c.Type != jwt.Program {
t.Fatalf("type = %q — the wire name and the field disagree", c.Type)
}
}