Skip to content

Extensions

Authentication tells you who is calling. Authorization tells you what they may do. In valiss the second question is answered by extensions: named, typed claims carried in a token’s ext field, signed along with the rest of the token and delivered unchanged to the server.

The scheme itself assigns extensions no meaning. It signs and transports each one opaquely; meaning belongs to whoever registered the name. That is what lets the transport layers and your own application both ride the same mechanism without colliding.

Self-naming typed claims

An extension is any struct that names itself. Implement one method and valiss carries the value end to end, handing the same concrete type back on the server with no string plumbing in between:

type QueryFilters struct {
    Regions []string `json:"regions"`
}

func (QueryFilters) ExtensionName() string { return "acme.filters" }

The name is the key under ext, so it must be unique within a token; a producer cannot emit two extensions with the same name. Embed the value into a token at issue time, recover it in the handler by type:

tok, _ := valiss.IssueUser(account, alicePub,
    valiss.WithName("alice"),
    valiss.WithExtension(QueryFilters{Regions: []string{"eu"}}),
    valiss.WithTTL(time.Hour),
)

// In the handler, on the verified identity:
filters, ok, err := valiss.ExtOf[QueryFilters](id.User.Ext)

You can also validate extensions at authentication time rather than in the handler: registering valiss.WithExtensionType[QueryFilters]() on the verifier rejects a request whose token carries a malformed acme.filters claim before your handler runs.

Transport authorization is built on extensions

The HTTP and gRPC integrations are not special-cased in the core; they are extensions like any other, which is the clearest demonstration that the mechanism is enough to build real authorization on.

  • contrib/httpauth defines an extension named http with three independent dimensions, Ext{Hosts, Methods, Paths}. Requests outside the stated bounds are rejected with 403.
  • contrib/grpcauth defines an extension named grpc, Ext{Methods}. Methods outside the stated set are rejected with PermissionDenied.

Each dimension constrains only when populated. A dimension you leave empty imposes no restriction, so Ext{Paths: []string{"/admin/*"}} permits those paths under any method. To scope a read-only admin surface you must name every dimension you mean to bound:

httpauth.Ext{Methods: []string{"GET"}, Paths: []string{"/admin/*"}}

Caution

A half-filled extension over-permits. Because a dimension constrains only when populated, Ext{Paths: []string{"/admin/*"}} bounds the path but leaves every method and host open, so it permits DELETE /admin/* as readily as a read. The zero-value Ext{} grants nothing, but a grant that names some dimensions and not others silently widens to everything it left unset. Name every dimension you mean to bound.

Fail closed, and enforce down the chain

Transport enforcement fails closed. Every token in the chain must carry the transport extension, the zero-value extension grants nothing, and allow-all is the explicit wildcard (Methods: ["*"], Paths: ["*"]), never the default. A deployment that authorizes entirely outside the transport can opt out with AllowMissingExtension(), but that is a deliberate choice, not an accident of an unset field.

Enforcement also compounds down the chain, and it is a verify-time property, not an issue-time one. IssueUser runs no subset check: an account can issue a user token whose extension names broader bounds than the account’s own. What contains it is verification. When both the account and the user token carry the extension, the verifier authorizes a request only if every level permits it (an AND across the chain), so the effective grant is the intersection. A user token that claims more than its account is simply capped at the account’s bounds when verified, and a tenant handed a capped account credential cannot exceed that cap no matter what it issues.

Related

  • Messages: the optional per-message proof of origin a user key mints, carried by the same signed-claim machinery.