A Go library implementing RFC 9493 — Subject Identifiers for Security Event Tokens.
Status: released,
v0.x. Latest tagv0.2.0. The API is tagged and usable, achieving byte-stable round-trip on every RFC 9493 §3 example. While the library is in thev0.xseries a minor release may still make breaking changes (per SemVer's0.xconvention) — seeCHANGELOG.mdbefore upgrading. The module pathgithub.com/hstern/go-subjectidhas been stable since the first commit. Av1.0.0API-stability commitment will follow once the surface settles.
go-subjectid is the Go ecosystem's reference implementation of the
RFC 9493 Subject Identifier types — the discriminated-union JSON
values used by SET / SSF / CAEP / RISC to identify the subject of a
security event.
The library handles:
- The eight built-in formats from the IANA "Security Event
Identifier Formats" registry:
account,email,iss_sub,opaque,phone_number,did,uri,aliases. - JSON codec — discriminator-driven
Unmarshaldispatch plus spec-order, byte-stableMarshaloutput. - Validation — opt-in
Validate()per format, returning sentinel errors (ErrFormatXxx,ErrRequired,ErrOpaqueEmpty,ErrNestedAliases, …). Callers branch witherrors.Is; every sentinel also matches the umbrellasubjectid.Err.ErrRequiredis a struct type — useerrors.Asto recover the missing field names. - Forward compatibility — unknown formats parse into an
UnknownFormatcarrier that preserves the wire bytes verbatim, so the identifier round-trips even when the library can't fully parse it. - Extension —
RegisterFormatlets downstream code add a new format constructor for non-registry types.
go get github.com/hstern/go-subjectid@latestRequires Go 1.26 or newer.
Per-format validation regexes are generated from the relevant RFCs'
ABNF grammars in grammar/<rfc>/*.abnf and committed to the tree as
*.rex files alongside, so the runtime tree depends only on the Go
standard library. Regenerating those files needs the
pandatix/go-abnf pap CLI.
go install does not work because the cmd/pap go.mod uses a local
replace directive; build from source:
git clone --depth=1 https://github.com/pandatix/go-abnf.git /tmp/go-abnf
go build -C /tmp/go-abnf/cmd/pap -o "$(go env GOPATH)/bin/pap"Then regenerate with make generate (or make -B generate to force a
full rebuild). CI runs the same incantation in the pap job below and
fails the build on uncommitted drift, so always commit the .rex
files alongside any .abnf change.
Decode a Subject Identifier from JSON, validate it, and act on the result:
package main
import (
"encoding/json"
"fmt"
"github.com/hstern/go-subjectid"
)
func main() {
wire := []byte(`{"format":"email","email":"user@example.com"}`)
id, err := subjectid.Parse(json.RawMessage(wire))
if err != nil {
panic(err)
}
if err := id.Validate(); err != nil {
panic(err)
}
fmt.Printf("%s identifier: %#v\n", id.Format(), id)
}Build a Subject Identifier value in Go and emit canonical wire bytes:
package main
import (
"encoding/json"
"fmt"
"github.com/hstern/go-subjectid"
)
func main() {
id := subjectid.IssSubID{
Iss: "https://issuer.example.com/",
Sub: "145234573",
}
if err := id.Validate(); err != nil {
panic(err)
}
out, err := json.Marshal(id)
if err != nil {
panic(err)
}
fmt.Println(string(out))
// {"format":"iss_sub","iss":"https://issuer.example.com/","sub":"145234573"}
}The codec is byte-stable: format is emitted first, then the
format-specific members in the order RFC 9493 §3 defines them.
Round-tripping a spec-example payload produces output identical to
the input.
Each format in the IANA "Security Event Identifier Formats"
registry has its own typed Go struct. All eight implement the
sealed SubjectIdentifier interface:
| Format | Go type | Wire member(s) | Spec section |
|---|---|---|---|
account |
AccountID |
uri (acct: URI per RFC 7565) |
§3.2.1 |
email |
EmailID |
email (addr-spec per RFC 5322 §3.4.1) |
§3.2.2 |
iss_sub |
IssSubID |
iss (URI), sub |
§3.2.3 |
opaque |
OpaqueID |
id (non-empty string) |
§3.2.4 |
phone_number |
PhoneNumberID |
phone_number (E.164) |
§3.2.5 |
did |
DIDID |
url (W3C DID Core URL) |
§3.2.6 |
uri |
URIID |
uri (RFC 3986 absolute-URI) |
§3.2.7 |
aliases |
AliasesID |
identifiers (slice; no nested aliases) |
§3.2.8 |
Use the typed constructor directly when building a value; use
Parse (or json.Unmarshal into a known type) when
decoding from the wire. See the godoc Examples on each format
type for runnable usage.
For format names outside the IANA built-in set, define a Go type
embedding subjectid.Seal and register a constructor at consumer
init time:
type OrgTenantID struct {
subjectid.Seal
Tenant string
}
func (OrgTenantID) Format() string { return "org.example.tenant" }
func (OrgTenantID) Validate() error { /* … */ return nil }
func (o OrgTenantID) MarshalJSON() ([]byte, error) { /* spec-order */ }
func (o *OrgTenantID) UnmarshalJSON(b []byte) error { /* … */ }
func init() {
_ = subjectid.RegisterFormat("org.example.tenant",
func() subjectid.SubjectIdentifier { return &OrgTenantID{} })
}Re-registering an IANA built-in name returns an error wrapping
subjectid.ErrFormatReserved. Unknown formats encountered on
unmarshal — neither built-in nor registered — parse into an
UnknownFormat carrier that preserves the wire bytes verbatim,
so payloads always round-trip even when the library can't
recognize every entry.
Subject Identifiers are a building block, not a complete protocol. The RFC 9493 wire shape is referenced by, but does not stand alone in:
- RFC 8417 — Security Event Token (SET). JWT-shaped events
whose
eventsclaim values carry asubjectmember that is an RFC 9493 Subject Identifier.go-subjectidis the wire-layer dependency for any Go SET library; the SET envelope itself (JWT signing, audience routing,txncorrelation) is the consumer's responsibility. - OpenID Shared Signals Framework (SSF). The transmitter
- receiver protocols for streaming SETs. Stream Configuration
uses
subjectin the same shape; SSF receivers consuming CAEP / RISC streams need a Subject Identifier parser at the edge.
- receiver protocols for streaming SETs. Stream Configuration
uses
- OpenID Continuous Access Evaluation Protocol (CAEP) and the
RISC event family. CAEP and RISC define event schemas that
embed a
subjectmember;go-subjectiddecodes that member.
A future sibling library (go-ssf-shaped — name TBD) will depend
on this one. Until then, consumers wiring SET / SSF stacks in Go
can use this library directly at the subject boundary and
supply their own JWT and HTTP layers.
v0.x is pre-release. The public API surface is expected to
remain stable through v0.x minor bumps once v0.1.0 is tagged,
but breaking changes may land between minor versions if a
wire-fidelity issue is found. After v1.0.0, Go module SemVer
applies: breaking changes require a v2 branch with /v2
import-path suffix per the standard major-version handling.
The library's wire-fidelity claim — every RFC 9493 §3 example
round-trips byte-stably through the codec — is regression-tested
in CI via the embedded spec fixtures in internal/specfixtures/.
The library's design rationale, especially the wire-fidelity choices
that bite every RFC 9493 implementer once, will be summarized in
design.md ahead of the v0.1.0 tag. The headline decisions:
- Sealed
SubjectIdentifierinterface with per-format concrete types — nomap[string]any, no zero-valued union-fields struct. encoding/jsonstdlib with customUnmarshalJSONdispatch on theformatdiscriminator.- Lenient on unmarshal, strict on marshal. Postel's law: decode whatever the wire gave us, validate at the marshal boundary.
- Byte-stable output:
formatfirst, format-specific members in the order RFC 9493 §3 defines them. - Open-extension fields are
json.RawMessage, notmap[string]any— interop scenarios pin exact JSON bytes, andmapreorders keys.
- Spec version:
const SpecVersion = "RFC 9493". RFCs don't have minor/patch numbers; errata are absorbed into Go-minor releases without changing the constant. - Go version: 1.26+.
- Dependencies: standard library only, at runtime. Test
dependencies, if any, are listed in
go.mod. - Library SemVer is independent of the spec version. Major-
version handling follows the
go-josebranch pattern (no versioned subdirectories —vNlives ingo.modon avNbranch).
Contributor conventions are in AGENTS.md: commit
message style, code review expectations, the per-file SPDX header,
and the local pre-PR checks the CI also runs.
Bugs and feature ideas are welcome via the project's issue tracker.
Apache-2.0. See LICENSE for the full text.
Every source file carries an SPDX identifier:
// Copyright 2026 The go-subjectid Authors
// SPDX-License-Identifier: Apache-2.0