Documentation
¶
Overview ¶
Package subjectid implements RFC 9493 Subject Identifiers for Security Event Tokens.
The package provides:
- The sealed SubjectIdentifier interface implemented by every identifier value, defined in this file.
- Per-format concrete types for each format in the IANA "Security Event Identifier Formats" registry, each in its own file (account.go, email.go, …).
- An UnknownFormat carrier that preserves the wire bytes of formats the library does not natively recognize, so forward- compatibility round-trips correctly.
- An opt-in SubjectIdentifier.Validate method per format, returning the package's sentinel errors (per-format FormatErr-built sentinels, ErrRequired for missing members, etc.). Callers branch with errors.Is; every sentinel also matches the umbrella Err.
Index ¶
- Constants
- Variables
- func FormatErr(msg string) error
- func MissingFields(names ...string) error
- func RegisterFormat(name string, ctor Constructor) error
- type AccountID
- type AliasesID
- type Constructor
- type DIDID
- type EmailID
- type ErrRequired
- type IssSubID
- type OpaqueID
- type PhoneNumberID
- type Seal
- type SubjectIdentifier
- type URIID
- type UnknownFormat
Examples ¶
Constants ¶
const SpecVersion = "RFC 9493"
SpecVersion identifies the RFC this package implements. RFCs have no minor or patch numbers; errata to RFC 9493 are absorbed into Go-minor releases of this module without changing the value of this constant.
Variables ¶
var ( // ErrJSON reports that the bytes handed to Parse are not a // JSON object exposing a "format" string member. ErrJSON = fmt.Errorf("%w: input is not a Subject Identifier JSON object", Err) // ErrNestedAliases reports that an aliases identifier contains // another aliases identifier, which RFC 9493 §3.2.8 forbids. ErrNestedAliases = fmt.Errorf("%w: aliases identifier must not contain a nested aliases identifier", Err) // ErrAliasesEmpty reports that an aliases identifier's // "identifiers" array was nil or zero-length. RFC 9493 §3.2.8 // requires at least one element. ErrAliasesEmpty = fmt.Errorf("%w: aliases identifier must contain at least one element", Err) // ErrOpaqueEmpty reports that an OpaqueID's "id" member was // the empty string. RFC 9493 §3.2.4 imposes no internal // structure on opaque identifiers but §3 still forbids empty // values. This sentinel exists alongside ErrRequired so // callers can distinguish "the wire shape was missing the // member" (ErrRequired, raised by Parse before the member is // inspected) from "the member was present but the empty // string" (ErrOpaqueEmpty, raised by OpaqueID.Validate). ErrOpaqueEmpty = fmt.Errorf("%w: opaque identifier is the empty string", Err) // Per-format invalid-value sentinels, one per RFC 9493 format // whose value is structurally checked. Built via FormatErr so // the surface stays uniform and each wraps Err. ErrFormatAccount = FormatErr("invalid acct URI") ErrFormatEmail = FormatErr("invalid email addr-spec") ErrFormatIssSub = FormatErr("invalid iss/sub identifier") ErrFormatPhoneNumber = FormatErr("invalid E.164 phone number") ErrFormatDID = FormatErr("invalid DID URL") ErrFormatURI = FormatErr("invalid absolute URI") // ErrFormatReserved is returned by [RegisterFormat] when a // caller attempts to register a constructor for a format name // that the library has already populated as a built-in. ErrFormatReserved = FormatErr("format name is reserved for a built-in") )
Sentinel errors categorizing the kinds of well-formedness failure a Subject Identifier can have. Each is the errors.Is target a caller uses to branch on the failure category. Each also wraps Err so caller code that only cares whether the error is a subjectid problem can use errors.Is(err, subjectid.Err).
var Err = errors.New("subjectid")
Err is the top-level umbrella sentinel for every error this package returns. Every other sentinel (ErrJSON, ErrFormatDID, ErrFormatReserved, the per-validator format errors built via FormatErr, and the ErrRequired struct type) is detectable as a subjectid.Err via errors.Is.
Use this when handling errors from multiple sources to branch on "did this come from subjectid?" without naming each leaf sentinel:
if errors.Is(err, subjectid.Err) {
// handle as a subjectid validation/codec failure
} else {
// unrelated to subjectid
}
Functions ¶
func FormatErr ¶
FormatErr builds a sentinel error from a descriptive message, wrapping Err so the result matches errors.Is(returned, Err).
Pass the full message the caller wants in the error text — the helper adds no "invalid" prefix or other framing. The result reads "subjectid: <msg>".
Built-in format sentinels (ErrFormatAccount, ErrFormatEmail, etc.) and ErrFormatReserved are constructed via this helper. Consumers that register their own format via RegisterFormat can use FormatErr to mint a matching sentinel so callers can branch with errors.Is in the same idiom that works for the built-ins:
var ErrFormatMyCustom = subjectid.FormatErr("invalid my-custom format")
func (m MyCustomID) Validate() error {
if !myRegex.MatchString(m.Value) {
return ErrFormatMyCustom
}
return nil
}
The returned error is a fresh value; two FormatErr calls with the same message produce distinct sentinels that do NOT compare equal under errors.Is. Store the result in a package-level var if callers need a stable comparison target.
func MissingFields ¶
MissingFields constructs an ErrRequired naming the specific wire- shape member(s) that were missing or empty. Pass one name for the common single-field case, or several for composite formats like iss_sub where multiple members can be missing at once.
func RegisterFormat ¶
func RegisterFormat(name string, ctor Constructor) error
RegisterFormat registers a Constructor for a Subject Identifier format outside the IANA built-in set.
Call once per extension format, typically at consumer init time. Re-registering an already-registered extension format silently replaces the prior constructor — a single consumer init owns each extension format by convention. Concurrent callers are serialized by an internal mutex.
Returns an error wrapping ErrFormatReserved if name matches one of the eight built-in formats: those cannot be overridden; consumers needing different per-built-in behavior should wrap the concrete type rather than re-register the format. Compare with errors.Is.
Example ¶
ExampleRegisterFormat shows the consumer extension path. A custom format name is associated with a constructor at init time; subjectid.Parse then dispatches to it automatically for any payload carrying that format discriminator.
package main
import (
"encoding/json"
"fmt"
"github.com/hstern/go-subjectid"
)
// exampleTenantID is a minimal extension type for the
// ExampleRegisterFormat demo. Production consumers would carry
// the same shape: embed [subjectid.Seal], implement Format,
// Validate, MarshalJSON, UnmarshalJSON, then register a
// constructor at consumer init time.
type exampleTenantID struct {
subjectid.Seal
Tenant string
}
func (exampleTenantID) Format() string { return "org.example.exampletenant" }
func (exampleTenantID) Validate() error { return nil }
func (o exampleTenantID) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Format string `json:"format"`
Tenant string `json:"tenant"`
}{Format: o.Format(), Tenant: o.Tenant})
}
func (o *exampleTenantID) UnmarshalJSON(data []byte) error {
var v struct {
Tenant string `json:"tenant"`
}
if err := json.Unmarshal(data, &v); err != nil {
return err
}
o.Tenant = v.Tenant
return nil
}
func main() {
_ = subjectid.RegisterFormat("org.example.exampletenant",
func() subjectid.SubjectIdentifier { return &exampleTenantID{} })
wire := []byte(`{"format":"org.example.exampletenant","tenant":"acme-prod"}`)
id, err := subjectid.Parse(json.RawMessage(wire))
if err != nil {
panic(err)
}
out, _ := json.Marshal(id)
fmt.Println(string(out))
}
Output: {"format":"org.example.exampletenant","tenant":"acme-prod"}
Example (ReservedName) ¶
ExampleRegisterFormat_reservedName shows the IANA-name guard: re-registering one of the eight built-in format names returns an error wrapping subjectid.ErrFormatReserved, so a consumer cannot accidentally override a built-in.
package main
import (
"encoding/json"
"errors"
"fmt"
"github.com/hstern/go-subjectid"
)
// exampleTenantID is a minimal extension type for the
// ExampleRegisterFormat demo. Production consumers would carry
// the same shape: embed [subjectid.Seal], implement Format,
// Validate, MarshalJSON, UnmarshalJSON, then register a
// constructor at consumer init time.
type exampleTenantID struct {
subjectid.Seal
Tenant string
}
func (exampleTenantID) Format() string { return "org.example.exampletenant" }
func (exampleTenantID) Validate() error { return nil }
func (o exampleTenantID) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Format string `json:"format"`
Tenant string `json:"tenant"`
}{Format: o.Format(), Tenant: o.Tenant})
}
func (o *exampleTenantID) UnmarshalJSON(data []byte) error {
var v struct {
Tenant string `json:"tenant"`
}
if err := json.Unmarshal(data, &v); err != nil {
return err
}
o.Tenant = v.Tenant
return nil
}
func main() {
err := subjectid.RegisterFormat("email",
func() subjectid.SubjectIdentifier { return &exampleTenantID{} })
fmt.Println(errors.Is(err, subjectid.ErrFormatReserved))
}
Output: true
Types ¶
type AccountID ¶
type AccountID struct {
// URI is the acct URI naming the account. It is the value of
// the JSON "uri" member; the scheme is required to be "acct"
// and the scheme-specific part follows the RFC 7565 §3
// "userpart @ host" grammar, with host per RFC 3986 §3.2.2.
URI string
}
AccountID identifies a subject by an "acct" URI per RFC 7565, as defined in RFC 9493 §3.2.1.
Wire shape:
{
"format": "account",
"uri": "acct:example.user@service.example.com"
}
func (AccountID) Format ¶
Format returns "account". See SubjectIdentifier.Format.
func (AccountID) MarshalJSON ¶
MarshalJSON implements json.Marshaler for AccountID. The output is the spec-order JSON object
{"format":"account","uri":"<URI>"}
— format member first, then the format-specific members in the order RFC 9493 §3.2.1 defines them. Output is canonical (no whitespace) and byte-stable for a given input.
func (*AccountID) UnmarshalJSON ¶
UnmarshalJSON implements json.Unmarshaler for AccountID. It decodes the "uri" member; the "format" member and any other extra members are ignored (Postel's law on receive). Use Parse to dispatch on "format" automatically.
func (AccountID) Validate ¶
Validate enforces the RFC 9493 §3.2.1 wire shape: the "uri" member must be non-empty and must match the RFC 7565 + RFC 3986 grammar from account-uri.abnf in its entirety.
Example ¶
ExampleAccountID_Validate shows the per-format validator returning nil for a well-formed acct: URI and a wrapped subjectid.ErrFormatAccount sentinel for a malformed one.
package main
import (
"errors"
"fmt"
"github.com/hstern/go-subjectid"
)
func main() {
ok := subjectid.AccountID{URI: "acct:user@example.com"}
bad := subjectid.AccountID{URI: "not-an-acct-uri"}
fmt.Println(ok.Validate())
fmt.Println(errors.Is(bad.Validate(), subjectid.ErrFormatAccount))
}
Output: <nil> true
type AliasesID ¶
type AliasesID struct {
// Identifiers is the heterogeneous slice of inner Subject
// Identifiers. It is the value of the JSON "identifiers"
// member. The slice MUST be non-empty and MUST NOT contain
// any element whose Format returns "aliases"; both rules
// are enforced in Validate.
Identifiers []SubjectIdentifier
}
AliasesID is a composite identifier representing the same subject under multiple formats, as defined in RFC 9493 §3.2.8.
Wire shape:
{
"format": "aliases",
"identifiers": [
{"format": "email", "email": "user@example.com"},
{"format": "account", "uri": "acct:user@example.com"}
]
}
Per RFC 9493 §3.2.8, an aliases identifier MUST NOT itself contain a nested aliases identifier. The Go type cannot express "[]SubjectIdentifier minus AliasesID" without losing the uniformity that makes the dispatch-and-recurse codec work, so the prohibition is enforced in AliasesID.Validate (a later commit) rather than at the type level.
func (AliasesID) Format ¶
Format returns "aliases". See SubjectIdentifier.Format.
func (AliasesID) MarshalJSON ¶
MarshalJSON implements json.Marshaler for AliasesID. The inner Identifiers slice is marshaled element-by-element; each element's own MarshalJSON is invoked, so a heterogeneous slice round-trips with each element in its own format's spec-order shape.
func (*AliasesID) UnmarshalJSON ¶
UnmarshalJSON implements json.Unmarshaler for AliasesID by reading the "identifiers" array as raw JSON elements and recursively dispatching each through Parse. The element types are arbitrary — heterogeneous slices fall out of the uniform dispatch.
An element whose "format" is not recognized by the registry becomes an UnknownFormat in the resulting slice rather than failing the whole AliasesID. An element that is not valid JSON or is missing the "format" member fails Parse and is returned as the AliasesID-level error — the spec rule that aliases contain non-null subject identifiers leaves no room for a totally unparseable inner element.
func (AliasesID) Validate ¶
Validate enforces the two RFC 9493 §3.2.8 rules on aliases: the identifiers array must be non-empty, and no element may itself be an aliases identifier (no nesting). Inner errors from per-element Validate calls are joined; callers branch via errors.Is against the package sentinels.
Example ¶
ExampleAliasesID_Validate shows the composite rule from RFC 9493 §3.2.8: the identifiers slice must be non-empty AND no element may itself be an aliases identifier. Inner-element errors join into the returned error so errors.Is surfaces any inner-format sentinel.
package main
import (
"fmt"
"github.com/hstern/go-subjectid"
)
func main() {
id := subjectid.AliasesID{
Identifiers: []subjectid.SubjectIdentifier{
subjectid.EmailID{Email: "user@example.com"},
subjectid.AccountID{URI: "acct:user@example.com"},
},
}
fmt.Println(id.Validate())
}
Output: <nil>
Example (NestedRejected) ¶
ExampleAliasesID_Validate_nestedRejected shows the no-nested- aliases rule firing: the outer aliases parses and validates individually-valid inner elements, but a nested AliasesID is rejected with subjectid.ErrNestedAliases.
package main
import (
"errors"
"fmt"
"github.com/hstern/go-subjectid"
)
func main() {
id := subjectid.AliasesID{
Identifiers: []subjectid.SubjectIdentifier{
subjectid.AliasesID{Identifiers: []subjectid.SubjectIdentifier{
subjectid.EmailID{Email: "user@example.com"},
}},
},
}
fmt.Println(errors.Is(id.Validate(), subjectid.ErrNestedAliases))
}
Output: true
type Constructor ¶
type Constructor func() SubjectIdentifier
Constructor is the per-format factory the registry holds. Each Constructor returns a freshly-allocated zero-valued SubjectIdentifier of the matching format type; the codec invokes UnmarshalJSON on the returned value to fill it in.
Constructors registered for non-built-in formats must return a type that embeds Seal so it satisfies the sealed interface.
The returned value is conventionally a pointer so the codec can populate it via UnmarshalJSON, but Parse normalizes the result to its canonical value form before returning it. The extension type must therefore satisfy SubjectIdentifier with value receivers, so the dereferenced value still implements the interface; a type whose methods use pointer receivers would not survive that normalization.
type DIDID ¶
type DIDID struct {
// URL is the DID URL. It is the value of the JSON "url"
// member; the grammar is the W3C DID Core URL —
// did:method:method-specific-id[?query][#fragment]. The
// library validates the top-level shell only in a later
// commit; per-method validation (did:web, did:key, etc.) is
// out of scope.
URL string
}
DIDID identifies a subject by a W3C Decentralized Identifier (DID) URL, as defined in RFC 9493 §3.2.6.
Wire shape:
{
"format": "did",
"url": "did:example:123456"
}
func (DIDID) Format ¶
Format returns "did". See SubjectIdentifier.Format.
func (DIDID) MarshalJSON ¶
MarshalJSON implements json.Marshaler for DIDID. See RFC 9493 §3.2.6 for the wire shape.
func (*DIDID) UnmarshalJSON ¶
UnmarshalJSON implements json.Unmarshaler for DIDID. See AccountID.UnmarshalJSON for the general contract.
func (DIDID) Validate ¶
Validate enforces the RFC 9493 §3.2.6 wire shape: the "url" member must be non-empty and must match the W3C DID Core ABNF from did-url.abnf in its entirety. Per-method validation (did:web, did:key, etc.) is out of scope.
Example ¶
ExampleDIDID_Validate shows the W3C DID Core URL shell check. Per-method validation (did:web, did:key, did:plc, …) is out of scope — the library validates the universal shape only.
package main
import (
"fmt"
"github.com/hstern/go-subjectid"
)
func main() {
fmt.Println(subjectid.DIDID{URL: "did:example:123456"}.Validate())
}
Output: <nil>
type EmailID ¶
type EmailID struct {
// Email is an RFC 5322 §3.4.1 addr-spec. It is the value of
// the JSON "email" member.
//
// Per RFC 9493 §3.2.2, the recipient SHOULD apply its own
// canonicalization (lowercasing the local-part, stripping
// plus-addressing, etc.) — the library performs no
// canonicalization on its own.
Email string
}
EmailID identifies a subject by an email address, as defined in RFC 9493 §3.2.2.
Wire shape:
{
"format": "email",
"email": "user@example.com"
}
func (EmailID) Format ¶
Format returns "email". See SubjectIdentifier.Format.
func (EmailID) MarshalJSON ¶
MarshalJSON implements json.Marshaler for EmailID. See RFC 9493 §3.2.2 for the wire shape.
func (*EmailID) UnmarshalJSON ¶
UnmarshalJSON implements json.Unmarshaler for EmailID. See AccountID.UnmarshalJSON for the general contract.
func (EmailID) Validate ¶
Validate enforces the RFC 9493 §3.2.2 wire shape: the "email" member must be non-empty and must match the RFC 5322 §3.4.1 addr-spec grammar (narrowed per v0.1 policy — see addr-spec.abnf) in its entirety.
Example ¶
ExampleEmailID_Validate shows the addr-spec syntax check firing for malformed input. The empty-string case returns subjectid.ErrRequired (a struct type recoverable via errors.As) rather than the format sentinel — "missing member" is a different rule than "syntax violation".
package main
import (
"errors"
"fmt"
"github.com/hstern/go-subjectid"
)
func main() {
bad := subjectid.EmailID{Email: ""}
err := bad.Validate()
var req subjectid.ErrRequired
if errors.As(err, &req) {
fmt.Println("missing:", req.Fields)
}
}
Output: missing: [email]
type ErrRequired ¶
type ErrRequired struct {
// Fields names the wire-shape members that were missing or
// empty. An empty slice is used when the failing field set
// is not known.
Fields []string
}
ErrRequired reports that one or more required members of the wire shape were missing or empty. RFC 9493 §3 requires every format's defining members to be present and non-empty.
Fields names the specific member(s) that were missing — values like "phone_number", "url", or []string{"iss", "sub"}. Use errors.As to recover them:
var req subjectid.ErrRequired
if errors.As(err, &req) {
log.Printf("missing fields: %v", req.Fields)
}
errors.Is also matches against the zero value, so categorical checks work without naming the fields:
if errors.Is(err, subjectid.ErrRequired{}) { ... }
And against the top-level package umbrella:
if errors.Is(err, subjectid.Err) { ... }
func (ErrRequired) Error ¶
func (e ErrRequired) Error() string
Error implements the error interface.
type IssSubID ¶
type IssSubID struct {
// Iss is the JWT issuer URI. It is the value of the JSON
// "iss" member and a JSON-string-encoded URI.
Iss string
// Sub is the issuer-scoped subject identifier. It is the
// value of the JSON "sub" member; its grammar is the
// issuer's choice.
Sub string
}
IssSubID identifies a subject by a JWT-style (issuer, subject) pair, as defined in RFC 9493 §3.2.3. The pair is also the iss and sub claim pair from RFC 7519.
Wire shape:
{
"format": "iss_sub",
"iss": "https://issuer.example.com/",
"sub": "145234573"
}
func (IssSubID) Format ¶
Format returns "iss_sub". See SubjectIdentifier.Format.
func (IssSubID) MarshalJSON ¶
MarshalJSON implements json.Marshaler for IssSubID. See RFC 9493 §3.2.3 — iss precedes sub, matching the JWT claim order.
func (*IssSubID) UnmarshalJSON ¶
UnmarshalJSON implements json.Unmarshaler for IssSubID. See AccountID.UnmarshalJSON for the general contract.
func (IssSubID) Validate ¶
Validate enforces the RFC 9493 §3.2.3 wire shape: both "iss" and "sub" must be non-empty; "iss" must be an RFC 3986 absolute-URI per the iss.abnf grammar; "sub" may be any printable issuer-chosen identifier per the sub.abnf grammar.
Example ¶
ExampleIssSubID_Validate shows the per-member check on the iss_sub composite: both "iss" and "sub" must be non-empty, and "iss" must be an RFC 3986 absolute URI.
package main
import (
"fmt"
"github.com/hstern/go-subjectid"
)
func main() {
id := subjectid.IssSubID{
Iss: "https://issuer.example.com/",
Sub: "145234573",
}
fmt.Println(id.Validate())
}
Output: <nil>
type OpaqueID ¶
type OpaqueID struct {
// ID is the opaque identifier. It is the value of the JSON
// "id" member and has no syntax constraints beyond being a
// non-empty JSON string. The non-empty rule is enforced by
// Validate and reported as [ErrOpaqueEmpty].
ID string
}
OpaqueID identifies a subject by a case-sensitive opaque string, as defined in RFC 9493 §3.2.4.
Wire shape:
{
"format": "opaque",
"id": "11112222333344445555"
}
func (OpaqueID) Format ¶
Format returns "opaque". See SubjectIdentifier.Format.
func (OpaqueID) MarshalJSON ¶
MarshalJSON implements json.Marshaler for OpaqueID. See RFC 9493 §3.2.4 for the wire shape.
func (*OpaqueID) UnmarshalJSON ¶
UnmarshalJSON implements json.Unmarshaler for OpaqueID. See AccountID.UnmarshalJSON for the general contract.
func (OpaqueID) Validate ¶
Validate enforces the RFC 9493 §3.2.4 non-empty-ID rule. The returned error is ErrOpaqueEmpty rather than ErrRequired so callers can distinguish "id was present but empty" from "id member was missing entirely from the JSON wire shape".
Example ¶
ExampleOpaqueID_Validate shows the only failure mode: subjectid.ErrOpaqueEmpty when ID is the empty string. The opaque format imposes no syntax beyond non-emptiness.
package main
import (
"errors"
"fmt"
"github.com/hstern/go-subjectid"
)
func main() {
fmt.Println(errors.Is(subjectid.OpaqueID{}.Validate(), subjectid.ErrOpaqueEmpty))
}
Output: true
type PhoneNumberID ¶
type PhoneNumberID struct {
// PhoneNumber is the E.164 number. It is the value of the
// JSON "phone_number" member: a leading "+" followed by
// 4 to 15 ASCII digits.
PhoneNumber string
}
PhoneNumberID identifies a subject by an ITU-T E.164 phone number, as defined in RFC 9493 §3.2.5.
The Phone Number Identifier Format identifies a subject using a telephone number. Subject Identifiers in this format MUST contain a "phone_number" member whose value is a string containing the full telephone number of the subject, including an international dialing prefix, formatted according to E.164 [E164]. The "phone_number" member is REQUIRED and MUST NOT be null or empty. The Phone Number Identifier Format is identified by the name "phone_number".
Below is a non-normative example Subject Identifier in the Phone Number Identifier Format:
{
"format": "phone_number",
"phone_number": "+12065550100"
}
Figure 8: Example: Subject Identifier in the Phone Number Identifier Format
func (PhoneNumberID) Format ¶
func (PhoneNumberID) Format() string
Format returns "phone_number". See SubjectIdentifier.Format.
func (PhoneNumberID) MarshalJSON ¶
func (p PhoneNumberID) MarshalJSON() ([]byte, error)
MarshalJSON implements json.Marshaler for PhoneNumberID. See RFC 9493 §3.2.5 for the wire shape.
func (*PhoneNumberID) UnmarshalJSON ¶
func (p *PhoneNumberID) UnmarshalJSON(data []byte) error
UnmarshalJSON implements json.Unmarshaler for PhoneNumberID. See AccountID.UnmarshalJSON for the general contract.
func (PhoneNumberID) Validate ¶
func (p PhoneNumberID) Validate() error
Validate enforces the RFC 9493 §3.2.5 wire shape: "phone_number" must be a non-empty string matching the basic E.164 shell — "+" followed by 4 to 15 ASCII digits with no separators. Returns MissingFields("phone_number") for the empty value and ErrFormatPhoneNumber for shape violations.
Example ¶
ExamplePhoneNumberID_Validate shows the E.164 shell check — "+" followed by 4 to 15 ASCII digits, with no separators. The library does NOT run full per-country plan validation; install a [WithPhoneNumberValidator]-style hook (see godoc) for that.
package main
import (
"fmt"
"github.com/hstern/go-subjectid"
)
func main() {
fmt.Println(subjectid.PhoneNumberID{PhoneNumber: "+12065550100"}.Validate())
}
Output: <nil>
type Seal ¶
type Seal = sealMarker
Seal is the embeddable marker that lets a type defined outside this package satisfy SubjectIdentifier when registered via RegisterFormat. Embed it as an anonymous field in your custom format type to inherit the unexported sealed marker method.
type OrgTenantID struct {
subjectid.Seal
Tenant string
}
func (OrgTenantID) Format() string { return "org.example.tenant" }
func (OrgTenantID) Validate() error { return nil }
func init() {
_ = subjectid.RegisterFormat("org.example.tenant", func() subjectid.SubjectIdentifier {
return &OrgTenantID{}
})
}
The unexported sealed method is promoted to the embedding type, so it satisfies the interface's seal without allowing arbitrary external implementations — any extension type must opt in by embedding Seal.
The built-in format types do not use Seal; they implement sealed directly because they live inside this package.
type SubjectIdentifier ¶
type SubjectIdentifier interface {
// Format returns the IANA "format" discriminator for this
// identifier — "email", "iss_sub", "aliases", and so on. The
// returned value is also the value of the "format" member in
// the JSON wire shape.
Format() string
// Validate reports whether the identifier satisfies the
// RFC 9493 well-formedness rules for its format. A nil return
// means valid; a non-nil return is one of the package's
// sentinel errors — the per-format [FormatErr]-built sentinel
// (e.g. [ErrFormatPhoneNumber]), [ErrRequired] (recoverable
// via [errors.As] to learn which field was missing), or a
// format-specific sentinel like [ErrOpaqueEmpty] or
// [ErrNestedAliases]. Every return value matches the umbrella
// [Err] under [errors.Is].
Validate() error
// contains filtered or unexported methods
}
SubjectIdentifier is the sealed interface implemented by every RFC 9493 Subject Identifier value the package exposes.
Implementations are confined to this package by the unexported sealed marker method. Downstream code that needs a format outside the built-in set calls RegisterFormat (defined in a later commit) rather than implementing the interface directly: registration feeds the codec dispatch table, whereas a direct implementation would be invisible to it.
Canonical dynamic form: every value this package produces is the concrete value type, never a pointer to it. Parse returns IssSubID, not *IssSubID, so the dynamic type read back from Parse is identical to the struct literal a caller writes by hand. Consumers branching with a type switch or assertion should match the value forms (case IssSubID, case AliasesID, …); the pointer forms never occur on a value obtained from this package's API.
func Parse ¶
func Parse(raw json.RawMessage) (SubjectIdentifier, error)
Parse decodes a Subject Identifier from raw JSON, dispatching on the value of the "format" member to the appropriate concrete type. The returned identifier is always in value form (e.g. IssSubID, never *IssSubID) — the canonical dynamic form for every value this package produces, matching what a caller constructs as a struct literal. See SubjectIdentifier.
- For a built-in format (account, email, iss_sub, opaque, phone_number, did, uri, aliases), the matching per-format type is returned with its members populated from the wire.
- For an extension format registered via RegisterFormat, the registered constructor is invoked and json.Unmarshal delegates to its UnmarshalJSON.
- For a format string the registry does not know, an UnknownFormat is returned carrying the original bytes verbatim. The library never errors on an unrecognized format — that is the forward-compatibility contract.
Errors are reserved for two conditions only:
- The bytes are not valid JSON, or the "format" member is not present or not a string. In the first case the error wraps ErrJSON; in the second it is ErrRequired with "format" in its Fields. Both match Err under errors.Is.
- A registered constructor's UnmarshalJSON returns an error. The error is returned verbatim.
Extra members in the JSON object that are not defined for the format are silently dropped, per RFC 9493's "be liberal in what you accept" reading of the wire shape. Strict checking happens at marshal time, not at unmarshal.
Example ¶
ExampleParse shows the discriminator-driven decode path: subjectid.Parse peeks at the wire's "format" member and dispatches to the matching concrete type, returning a value that satisfies subjectid.SubjectIdentifier.
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)
}
email := id.(subjectid.EmailID)
fmt.Println(id.Format(), email.Email)
}
Output: email user@example.com
Example (UnknownFormat) ¶
ExampleParse_unknownFormat shows the forward-compatibility path: a format the library does not recognize parses into subjectid.UnknownFormat carrying the original bytes verbatim, so the payload still round-trips.
package main
import (
"encoding/json"
"fmt"
"github.com/hstern/go-subjectid"
)
func main() {
wire := []byte(`{"format":"org.example.future","widget_id":42}`)
id, err := subjectid.Parse(json.RawMessage(wire))
if err != nil {
panic(err)
}
unk := id.(subjectid.UnknownFormat)
out, _ := json.Marshal(unk)
fmt.Println(unk.FormatName)
fmt.Println(string(out))
}
Output: org.example.future {"format":"org.example.future","widget_id":42}
type URIID ¶
type URIID struct {
// URI is the absolute RFC 3986 URI naming the subject. It is
// the value of the JSON "uri" member; the library validates
// via net/url.Parse plus a strictness wrapper in a later
// commit (absolute URI, non-empty scheme).
URI string
}
URIID identifies a subject by any RFC 3986 URI, as defined in RFC 9493 §3.2.7. It is the "use when nothing more specific applies" fallback within the built-in set.
Wire shape:
{
"format": "uri",
"uri": "urn:oasis:names:tc:saml:2.0:nameid-format:transient"
}
func (URIID) Format ¶
Format returns "uri". See SubjectIdentifier.Format.
func (URIID) MarshalJSON ¶
MarshalJSON implements json.Marshaler for URIID. See RFC 9493 §3.2.7 for the wire shape.
func (*URIID) UnmarshalJSON ¶
UnmarshalJSON implements json.Unmarshaler for URIID. See AccountID.UnmarshalJSON for the general contract.
func (URIID) Validate ¶
Validate enforces the RFC 9493 §3.2.7 wire shape: the "uri" member must be non-empty and must match the RFC 3986 §4.3 absolute-URI grammar in its entirety.
Example ¶
ExampleURIID_Validate shows the RFC 3986 absolute-URI check. "uri" is the fallback format for identifiers without a more specific format in the IANA registry.
package main
import (
"fmt"
"github.com/hstern/go-subjectid"
)
func main() {
id := subjectid.URIID{URI: "urn:oasis:names:tc:saml:2.0:nameid-format:transient"}
fmt.Println(id.Validate())
}
Output: <nil>
type UnknownFormat ¶
type UnknownFormat struct {
// FormatName is the value of the JSON "format" member as
// decoded — the discriminator the library did not recognize.
FormatName string
// Raw is the entire JSON object that was decoded, byte-for-
// byte as it appeared on the wire. Re-encoding returns these
// bytes unchanged.
Raw json.RawMessage
}
UnknownFormat carries a Subject Identifier whose format is not in this build's built-in set and was not registered via RegisterFormat. It is the package's forward-compatibility carrier: a library compiled today, decoding a payload that uses a format the IANA registry adds in 2027, returns an UnknownFormat rather than an error so the value can still round-trip and the caller can branch on whatever subset of formats it actually understands.
The wire bytes are preserved verbatim in Raw so that re-encoding produces the original payload byte-for-byte (modulo JSON whitespace canonicalization). Raw is intentionally a json.RawMessage rather than a map[string]any: interop scenarios often pin exact JSON bytes, and a map reorders its keys on every encode.
func (UnknownFormat) Format ¶
func (u UnknownFormat) Format() string
Format returns the unrecognized format discriminator. The returned value is the FormatName field, not a fixed string — UnknownFormat is the one type whose Format method varies per value. See SubjectIdentifier.Format.
func (UnknownFormat) MarshalJSON ¶
func (u UnknownFormat) MarshalJSON() ([]byte, error)
MarshalJSON implements json.Marshaler for UnknownFormat by returning the Raw bytes verbatim when populated, so the wire payload round-trips through the codec. Note that encoding/json.Marshal runs json.Compact over a json.Marshaler's output, so insignificant whitespace inside Raw is normalized on the way out — the surrounding payload sees canonical JSON. Round-trip byte stability therefore holds modulo json.Compact, matching the conformance test suite's expectation.
Two edge cases:
- Raw is non-empty but not a valid JSON object: an error is returned. UnknownFormat's contract is "round-trip valid JSON the library does not recognize"; emitting invalid JSON would silently corrupt the surrounding payload.
- Raw is empty: a minimal {"format":"<FormatName>"} object is emitted. If FormatName is also empty an error is returned — there is no Subject Identifier to write.
Mutating FormatName after a Parse does not change the emitted bytes (Raw is authoritative when present); to change the wire format string, set Raw to nil before marshaling.
func (*UnknownFormat) UnmarshalJSON ¶
func (u *UnknownFormat) UnmarshalJSON(data []byte) error
UnmarshalJSON implements json.Unmarshaler for UnknownFormat. Stores the "format" discriminator in FormatName and the entire input bytes verbatim in Raw so MarshalJSON (a later commit) can re-emit byte-for-byte.
UnknownFormat is normally produced by Parse for unrecognized formats; this method is provided so an external caller can also json.Unmarshal directly into an UnknownFormat target when its purpose is to round-trip the bytes whole.
func (UnknownFormat) Validate ¶
func (UnknownFormat) Validate() error
Validate is a no-op. UnknownFormat exists precisely to round- trip values whose well-formedness rules the library does not know; performing validation on it would defeat the purpose of the carrier.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
specfixtures
Package specfixtures embeds the eight illustrative Subject Identifier payloads from RFC 9493 §3 as canonical (compact) JSON so the parent package's conformance tests can exercise every built-in format against the actual spec wording.
|
Package specfixtures embeds the eight illustrative Subject Identifier payloads from RFC 9493 §3 as canonical (compact) JSON so the parent package's conformance tests can exercise every built-in format against the actual spec wording. |