Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.2.0] - 2026-06-21

### Changed

- **Breaking:** Upgraded [`go-subjectid`](https://github.com/hstern/go-subjectid)
to v0.2.0, whose `Parse` now returns Subject Identifiers in their value form
(for example `subjectid.IssSubID`) rather than the pointer form. A parsed
`SET` now holds the same value form a `SET` built in Go holds, so a type
switch on `SET.Subject` no longer has to special-case the parsed shape — but
code that asserted the parsed subject as `*subjectid.IssSubID` must now assert
the value form. The `SubjectAs[T]` and `SET.IssSub` accessors are unchanged:
they still return the value form and still absorb a hand-built pointer-form
subject, so consumers already using them need no changes. The `SET.Subject`
documentation now describes the value form as the parsed type.

## [0.1.1] - 2026-06-21

### Added
Expand Down Expand Up @@ -42,6 +57,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
(such as OpenID CAEP and RISC) plug in typed decoders while unknown event
types stay raw.

[Unreleased]: https://github.com/hstern/go-secevent/compare/v0.1.1...HEAD
[Unreleased]: https://github.com/hstern/go-secevent/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/hstern/go-secevent/compare/v0.1.1...v0.2.0
[0.1.1]: https://github.com/hstern/go-secevent/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/hstern/go-secevent/releases/tag/v0.1.0
8 changes: 4 additions & 4 deletions conformance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,12 @@ func assertSubject(t *testing.T, set *SET, tc conformanceCase) {
return
}

got, ok := set.Subject.(*subjectid.IssSubID)
got, ok := set.Subject.(subjectid.IssSubID)
if !ok {
t.Fatalf("Subject is %T, want *subjectid.IssSubID", set.Subject)
t.Fatalf("Subject is %T, want subjectid.IssSubID", set.Subject)
}
if *got != tc.wantSubject {
t.Errorf("Subject = %+v, want %+v", *got, tc.wantSubject)
if got != tc.wantSubject {
t.Errorf("Subject = %+v, want %+v", got, tc.wantSubject)
}
}

Expand Down
7 changes: 3 additions & 4 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,9 @@ func Example_encode() {
// {"iss":"https://idp.example.com/","iat":1615305600,"jti":"set-0002","aud":"https://receiver.example.com/","sub_id":{"format":"iss_sub","iss":"https://idp.example.com/","sub":"user-7f3e2a"},"events":{"https://schemas.openid.net/secevent/caep/event-type/session-revoked":{"initiating_entity":"policy"}}}
}

// ExampleSET_IssSub reads the typed sub_id of a parsed SET. Parse hands back
// the pointer form of the Subject Identifier (a *subjectid.IssSubID), but
// IssSub returns the iss_sub value directly, so a consumer never has to handle
// the pointer/value distinction itself.
// ExampleSET_IssSub reads the typed sub_id of a parsed SET. IssSub returns the
// iss_sub value directly regardless of whether the held identifier is the value
// or pointer form, so a consumer never has to handle the distinction itself.
func ExampleSET_IssSub() {
// Verified, base64url-decoded claims-set bytes carrying an iss_sub subject.
payload := []byte(`{
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ module github.com/hstern/go-secevent

go 1.26

require github.com/hstern/go-subjectid v0.0.0-20260525222327-b47140763585
require github.com/hstern/go-subjectid v0.2.0
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github.com/hstern/go-subjectid v0.0.0-20260525222327-b47140763585 h1:dDFXd9djpbmlAOSlzIbFunmu6YVnzMcOnxPH+ySINZ0=
github.com/hstern/go-subjectid v0.0.0-20260525222327-b47140763585/go.mod h1:KS0TexS0PZQe7CnntjRRzcufxLdMji1DMVpxiBqCvRM=
github.com/hstern/go-subjectid v0.2.0 h1:kakUgtYVbUjIIVLaJcwWnQ9QKFwIw+AQxCZKOGLiOWU=
github.com/hstern/go-subjectid v0.2.0/go.mod h1:KS0TexS0PZQe7CnntjRRzcufxLdMji1DMVpxiBqCvRM=
16 changes: 8 additions & 8 deletions interop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,12 @@ func TestInteropCAEPSessionRevoked(t *testing.T) {
}

wantSubject := subjectid.IssSubID{Iss: "https://idp.example.com/", Sub: "user-7f3e2a"}
got, isIssSub := set.Subject.(*subjectid.IssSubID)
got, isIssSub := set.Subject.(subjectid.IssSubID)
if !isIssSub {
t.Fatalf("Subject is %T, want *subjectid.IssSubID", set.Subject)
t.Fatalf("Subject is %T, want subjectid.IssSubID", set.Subject)
}
if *got != wantSubject {
t.Errorf("Subject = %+v, want %+v", *got, wantSubject)
if got != wantSubject {
t.Errorf("Subject = %+v, want %+v", got, wantSubject)
}

event, ok, err := set.Events.Typed(caepSessionRevokedURI)
Expand Down Expand Up @@ -189,13 +189,13 @@ func TestInteropRISCAccountDisabled(t *testing.T) {
if err != nil {
t.Fatalf("decode per-event subject: %v", err)
}
issSub, isIssSub := subject.(*subjectid.IssSubID)
issSub, isIssSub := subject.(subjectid.IssSubID)
if !isIssSub {
t.Fatalf("per-event subject is %T, want *subjectid.IssSubID", subject)
t.Fatalf("per-event subject is %T, want subjectid.IssSubID", subject)
}
want := subjectid.IssSubID{Iss: "https://idp.example.com/", Sub: "user-7f3e2a"}
if *issSub != want {
t.Errorf("per-event subject = %+v, want %+v", *issSub, want)
if issSub != want {
t.Errorf("per-event subject = %+v, want %+v", issSub, want)
}
}

Expand Down
4 changes: 2 additions & 2 deletions parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,9 @@ func TestParseSubID(t *testing.T) {
if got, want := set.Subject.Format(), "iss_sub"; got != want {
t.Errorf("Subject.Format() = %q, want %q", got, want)
}
iss, ok := set.Subject.(*subjectid.IssSubID)
iss, ok := set.Subject.(subjectid.IssSubID)
if !ok {
t.Fatalf("Subject has type %T, want *subjectid.IssSubID", set.Subject)
t.Fatalf("Subject has type %T, want subjectid.IssSubID", set.Subject)
}
if iss.Iss != "https://issuer.example.com/" || iss.Sub != "145234573" {
t.Errorf("Subject = {Iss:%q Sub:%q}, want {iss/ 145234573}", iss.Iss, iss.Sub)
Expand Down
11 changes: 5 additions & 6 deletions secevent.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,11 @@ type SET struct {
// field. A nil Subject means the claim is absent. OPTIONAL (RFC 8417 §2.2,
// RFC 9493 §3).
//
// The interface's dynamic type depends on how the SET was built: Parse
// yields the pointer form (for example *subjectid.IssSubID, because
// go-subjectid's registry constructors return pointers), whereas a SET
// built in Go naturally holds the value form (subjectid.IssSubID). Use
// SubjectAs or IssSub to read the concrete subject without handling both
// forms by hand.
// Parse yields the value form of the identifier (for example
// subjectid.IssSubID), the same form a SET built in Go naturally holds; a
// SET may also be hand-built with the pointer form (*subjectid.IssSubID).
// Use SubjectAs or IssSub to read the concrete subject without handling
// both forms by hand.
Subject subjectid.SubjectIdentifier

// TransactionID (txn) optionally correlates the SET with related events or
Expand Down
20 changes: 10 additions & 10 deletions subject.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ import (
// SubjectAs returns the SET's sub_id as the concrete Subject Identifier type T,
// reporting ok=false when the subject is absent or of a different format.
//
// It exists to absorb a pointer/value asymmetry in the Subject field. A SET
// produced by Parse carries the pointer form of its identifier (for example
// *subjectid.IssSubID), because go-subjectid's registry constructors return
// pointers; a SET built in Go naturally holds the value form
// (subjectid.IssSubID), because the concrete types satisfy the interface with
// value receivers. A consumer that type-asserts SET.Subject directly therefore
// has to handle both forms. SubjectAs handles them once: it accepts the value
// type as T (for example SubjectAs[subjectid.IssSubID]) and matches whether the
// held identifier is the value or the pointer form, always returning the value.
// It exists to absorb the value/pointer distinction in the Subject field.
// go-subjectid's Parse returns identifiers in their value form (for example
// subjectid.IssSubID), the same form a SET built in Go naturally holds, so the
// two share a dynamic type. A SET may nonetheless be hand-built with the
// pointer form (*subjectid.IssSubID also satisfies the interface), and a
// consumer that type-asserts SET.Subject directly would have to handle both.
// SubjectAs handles them once: it accepts the value type as T (for example
// SubjectAs[subjectid.IssSubID]) and matches whether the held identifier is the
// value or the pointer form, always returning the value.
//
// if iss, ok := secevent.SubjectAs[subjectid.IssSubID](set); ok {
// // iss is a subjectid.IssSubID regardless of how set was built
Expand All @@ -38,7 +38,7 @@ func SubjectAs[T subjectid.SubjectIdentifier](s *SET) (T, bool) {
return v, true
}

// The pointer form (a SET from Parse) is *T; dereference it. A type
// The pointer form (a hand-built SET) is *T; dereference it. A type
// assertion to *T will not compile for an arbitrary type parameter, so
// reach for reflection to peel exactly one pointer indirection.
rv := reflect.ValueOf(s.Subject)
Expand Down
24 changes: 12 additions & 12 deletions subject_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ func TestSETSubjectRoundTrip(t *testing.T) {
t.Fatalf("subjectid.Parse: %v", err)
}

gotIssSub, ok := got.(*subjectid.IssSubID)
gotIssSub, ok := got.(subjectid.IssSubID)
if !ok {
t.Fatalf("parsed Subject is %T, want *subjectid.IssSubID", got)
t.Fatalf("parsed Subject is %T, want subjectid.IssSubID", got)
}
if *gotIssSub != want {
t.Errorf("Subject round-trip = %+v, want %+v", *gotIssSub, want)
if gotIssSub != want {
t.Errorf("Subject round-trip = %+v, want %+v", gotIssSub, want)
}
if got.Format() != "iss_sub" {
t.Errorf("Format() = %q, want %q", got.Format(), "iss_sub")
Expand Down Expand Up @@ -74,8 +74,8 @@ func TestSubjectAsValueForm(t *testing.T) {
}

// TestSubjectAsPointerForm checks that SubjectAs and IssSub also read the
// pointer form — the shape Parse produces, since go-subjectid's registry
// constructors return pointers. This is the asymmetry the accessor absorbs.
// pointer form — the shape a SET hand-built with *subjectid.IssSubID carries.
// This is the value/pointer distinction the accessor absorbs.
func TestSubjectAsPointerForm(t *testing.T) {
want := subjectid.IssSubID{Iss: "https://issuer.example.com/", Sub: "145234573"}
s := SET{Subject: &want}
Expand All @@ -90,8 +90,8 @@ func TestSubjectAsPointerForm(t *testing.T) {

// TestSubjectAsRoundTrip is the acceptance round-trip: a SET built in Go with
// the value form of the subject, encoded and parsed back, yields an equal
// subject through the accessor — even though Parse hands back the pointer form.
// A consumer never has to write a pointer/value type switch.
// subject through the accessor. A consumer never has to write a pointer/value
// type switch.
func TestSubjectAsRoundTrip(t *testing.T) {
want := subjectid.IssSubID{Iss: "https://issuer.example.com/", Sub: "145234573"}
s := SET{
Expand All @@ -114,10 +114,10 @@ func TestSubjectAsRoundTrip(t *testing.T) {
t.Fatalf("Parse: %v", err)
}

// Sanity: the parsed subject is the pointer form, which is exactly why the
// accessor is needed.
if _, ok := parsed.Subject.(*subjectid.IssSubID); !ok {
t.Fatalf("parsed Subject is %T, want *subjectid.IssSubID", parsed.Subject)
// Sanity: the parsed subject is the value form go-subjectid's Parse returns,
// the same form fed in — the accessor still spares consumers the type switch.
if _, ok := parsed.Subject.(subjectid.IssSubID); !ok {
t.Fatalf("parsed Subject is %T, want subjectid.IssSubID", parsed.Subject)
}

got, ok := parsed.IssSub()
Expand Down
Loading