Repository navigation
Expand file tree
/
Copy pathCHANGELOG.json
More file actions
4615 lines (4615 loc) · 234 KB
/
Copy pathCHANGELOG.json
File metadata and controls
4615 lines (4615 loc) · 234 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
{
"irVersion": "1.0",
"project": "systemforge",
"repository": "https://github.com/plexusone/systemforge",
"versioning": "semver",
"commitConvention": "conventional",
"maintainers": [
"grokify"
],
"releases": [
{
"version": "v0.30.0",
"date": "2026-10-29",
"highlights": [
{
"description": "`contract/audit/ocsfaudit` writes SystemForge's security events as OCSF 1.10 events, one JSON object per line, to an `io.Writer` the host names, for a security data lake or SIEM that ingests OCSF"
},
{
"description": "`audit.NewMultiEmitter` hands every security event to several emitters, such as the log emitter and the OCSF emitter"
}
],
"security": [
{
"description": "OCSF events identify people only by principal ID and client address: names, email addresses, session tokens, tokens, token and code prefixes, codes and free-text reasons are left out of every field (a revoked managed-principal session is named by its record UUID), and event details reach the output only through a per-event-type allowlist of codes, UUIDs, booleans, integers and times",
"commit": "f8f5d44"
},
{
"description": "The client sends the `User-Agent`, so it can carry an email address or a token; both the log emitter and the OCSF emitter now record it only through `audit.SanitizeUserAgent`, which removes control characters and then leaves out a value that looks like it holds an email address or a credential (a heuristic for the common shapes)",
"commit": "8eaa650"
},
{
"description": "Token and authorization code replay events are High severity with `is_alert`, a platform-admin grant is High, and a production waiver at startup is Medium",
"commit": "f8f5d44"
}
],
"added": [
{
"description": "`ocsfaudit.New(w, Options)`: an `audit.Emitter` that maps each event to an OCSF class (Authentication, User Management, Group Management, Entity Management, Application Lifecycle, or Base Event for an unknown type), entities OCSF has no type for as `type_id` Other with the type named,, with product, vendor and deployment metadata (`metadata.reporter.name`), the organization in `metadata.tenant_uid` and the request ID in `metadata.correlation_uid`",
"commit": "f8f5d44"
},
{
"description": "`Emit` never blocks, logs or fails the request: it queues each line in a bounded buffer written by one goroutine, and drops and counts events when the buffer is full (`Dropped`) or cannot encode them (`EncodeFailures`); a reporter goroutine logs the counts once a minute and on `Close`, write failures and short writes are counted (`WriteFailures`) and logged at most once a minute, a line torn by a failed write is the only one lost, and `Close` drains, flushes and syncs",
"commit": "423324d"
},
{
"description": "`audit.MultiEmitter` and `audit.NewMultiEmitter`",
"commit": "76693ba"
},
{
"description": "Dependency `github.com/grokify/ocsf-schema-go` v0.2.0",
"commit": "d9c1761"
},
{
"description": "`audit.SanitizeUserAgent`: removes control characters and invalid UTF-8, then leaves out a `User-Agent` that looks like it holds an email address or a credential, and cuts any other to 512 bytes",
"commit": "8eaa650"
}
],
"changed": [
{
"description": "`audit.LogEmitter` leaves out a `User-Agent` that holds an email address or a credential-like value",
"commit": "8eaa650"
}
],
"tests": [
{
"description": "The OCSF output of every security event type, with success and failure variants, validates against the schema embedded in `ocsf-schema-go`",
"commit": "7b8d38e"
},
{
"description": "The tests proving no token or secret reaches a log or event also run the OCSF emitter, and email verification and change events carry no address",
"commit": "b0e01a3"
},
{
"description": "Every event, Base Event included, validates through `ValidateEvent` with no skipped issues; torn lines, short writes, Emit with a stuck logger, periodic drop reports, unsyncable files and User-Agent sanitizing are tested",
"commit": "423324d"
},
{
"description": "A torn line stays marked through a failed Write that wrote nothing, and control characters cannot hide an email address from the User-Agent check",
"commit": "0300d41"
}
],
"documentation": [
{
"description": "`docs/security/ocsf-events.md`: wiring with an append-mode file, the event mapping, fields, privacy rules and buffering",
"commit": "66895d1"
},
{
"description": "The README and the documentation home feature security events and OCSF output",
"commit": "bbece5fa"
},
{
"description": "The OCSF page states the schema version `1.10.0-dev`, recommends rotating by renaming over `copytruncate`, and describes delivery, session IDs and the `User-Agent` exactly",
"commit": "cabf495"
},
{
"description": "The README's privacy claim covers the OCSF emitter and says what the log emitter records; rotation by renaming waits for the reopen and uses `delaycompress` with compression",
"commit": "23c5e87"
}
]
},
{
"version": "v0.29.0",
"date": "2026-10-29",
"highlights": [
{
"description": "SystemAuth asks for consent by the client's party: clients get `party` (`first_party`, `second_party` or `third_party`) and an optional `logo_url`, only first-party clients skip the consent screen, and `social_login.skip_consent` no longer auto-consents every client"
},
{
"description": "The authorization endpoint honours the OIDC `prompt` parameter, so a first-party application can sign a user in silently, and the login chooser names the requesting client and shows its logo"
},
{
"description": "Verified email: with a new `mail` section, SystemAuth verifies an unverified provider email at sign-in, lets a signed-in person change their email at `/account/email`, and carries `email_verified_at` in ID tokens and UserInfo"
},
{
"description": "Configuration secrets resolve through an omnivault vault named in `secrets.vault`, with the environment as the default, and no cloud SDK in the library packages"
},
{
"description": "`systemauth org member add|remove|list`, organization slug resolution for relying parties, and read-only application vocabularies in the product contract API"
}
],
"security": [
{
"description": "`social_login.skip_consent` granted consent for every client to users signed in through social login, and `DefaultSessionProvider`'s `WithSkipConsent` did the same, so once a client the operator does not own was registered its users were never asked; the authorization endpoint now decides by the client's party, and a second- or third-party client needs the user's recorded consent to every requested scope",
"commit": "6f0803a"
},
{
"description": "A client from a custom `Storage` that does not report its party is treated as third-party, and a client registered at runtime without a party is `third_party`",
"commit": "5ad47a0"
},
{
"description": "Email verification links are single-use, purpose-scoped and stored as SHA-256 hashes; a `verify_email` link completes only in the browser that started the sign-in and an `email_change` link only from a session of the principal that asked, each behind an origin-checked CSRF form, so a forwarded or scanned link cannot complete it",
"commit": "54303df"
},
{
"description": "Verification mail is limited per recipient address and per client address on the shared rate limit storage, and requests answer the same whether or not a message was sent or the address belongs to an account",
"commit": "17fee3f"
},
{
"description": "Production mode refuses `mail.provider: log`, with no waiver, because it writes verification links to the server log",
"commit": "17fee3f"
},
{
"description": "The client logo on the login chooser must be an https URL, is rendered by `html/template`, and only that page allows images from its origin in the content security policy",
"commit": "aa33316"
}
],
"breaking": [
{
"description": "Run `migrations/0012_oauth_app_party.sql` on an existing database without startup migration; it adds `sf_oauth_apps.party` and fills it from `first_party`. Without the column, reading or writing an OAuth application fails",
"commit": "7004ac7"
},
{
"description": "`social_login.skip_consent` and `WithSkipConsent` no longer grant consent; users of a second- or third-party client, including any client registered at runtime without a party, are asked for consent",
"commit": "6f0803a"
},
{
"description": "`DefaultSessionProvider` records no consent and now reports none; a deployment using it with second- or third-party clients needs a `SessionProvider` that records consent",
"commit": "6f0803a"
},
{
"description": "`rate_limit.redis_url` is now expanded like the other secret fields, so a literal `$` in it is expanded",
"commit": "4a4470a"
}
],
"upgradeGuide": [
{
"description": "Run `systemauth migrate` (or start with `--migrate`), or apply `migrations/0012_oauth_app_party.sql`; the schema migration also creates `sf_verification_tokens`"
},
{
"description": "Set `party: first_party` or `second_party` on runtime-registered clients the operator or a customer owns; otherwise their users are now asked for consent. Clients in the configuration file are rewritten as `first_party` at startup unless they set `party`"
},
{
"description": "Remove `social_login.skip_consent` and `WithSkipConsent`; they are ignored, and SystemAuth logs a warning at startup when `skip_consent` is set"
},
{
"description": "To verify unverified provider emails, add a `mail` section; `ses` needs a sender (`systemauthsvc.Options.MailSender` or `systemauth.WithMailSender`; the `systemauth` binary builds it from `mail.ses`), and production refuses `log`. Without `mail`, sign-ins with an unverified provider email are refused as before"
},
{
"description": "To read secret references from a vault other than the environment, set `secrets.vault` to an omnivault URI, register cloud schemes with `omnivault.RegisterProvider` in your binary, or pass a resolver to `LoadConfigWithResolver`"
},
{
"description": "Escape any literal `$` in `rate_limit.redis_url`"
}
],
"added": [
{
"description": "Client `party` (`first_party`, `second_party`, `third_party`) and `logo_url` on `Client`, static clients, `OAuthClient` and Ent storage (`sf_oauth_apps.party`, `sf_oauth_apps.logo_url`); unknown parties are refused by `Config.Validate`, `NewClientFromConfig`, `RegisterClient` and both storages",
"commit": "5ad47a0"
},
{
"description": "`sf_oauth_apps.party` in the identity schema and `migrations/0012_oauth_app_party.sql`",
"commit": "7004ac7"
},
{
"description": "OIDC `prompt` at the authorization endpoint: `none` answers `login_required` or `consent_required` instead of showing a page, `login` re-authenticates, `consent` always shows the consent page, `select_account` is accepted with no effect, and `none` with another value or an unknown value is `invalid_request`",
"commit": "fdbd39e"
},
{
"description": "The login chooser names the requesting client and shows its `logo_url`",
"commit": "aa33316"
},
{
"description": "`VerificationTokenStore` with memory and Ent implementations (`sf_verification_tokens`) and the `storetest.VerificationTokenStore` conformance suite",
"commit": "3df8231"
},
{
"description": "Ent schema `VerificationToken` (`sf_verification_tokens`)",
"commit": "e870e8a"
},
{
"description": "SystemAuth configuration `mail` (`provider` `ses`, `smtp` or `log`, `from`, `reply_to`, `product_name`, `support_url`, `token_lifetime` from 5m to 24h with a default of 30m, `per_address` default 5 an hour, `per_client` default 20 an hour, `smtp.*`, `ses.region`, `ses.configuration_set`); `systemauth.WithMailSender` and `systemauthsvc.Options.MailSender`; the `systemauth` binary builds the Amazon SES sender",
"commit": "17fee3f"
},
{
"description": "A verify-email step at social sign-in for an unverified provider email, with a rate-limited resend; a confirmed address counts as proven (`ExternalLogin.EmailProven`)",
"commit": "54303df"
},
{
"description": "Email change at `/account/email` with a notice to the current address; `PrincipalEmailChanger`, implemented by the Ent and memory directories",
"commit": "67aa733"
},
{
"description": "Security events `systemauth.email_verification.sent`, `systemauth.email.verified`, `systemauth.email_change.requested`, `systemauth.email_change.cancelled` and `systemauth.email.changed`, and the sign-in refusal reason `verification_invalid`",
"commit": "54303df"
},
{
"description": "`email_verified_at` claim (a NumericDate, with the `email` scope) in ID tokens, UserInfo and discovery's `claims_supported`; `HumanPrincipal.EmailVerifiedAt`, and `relyingparty.IDTokenClaims.EmailVerifiedAt`, `UserInfo.EmailVerifiedAt` (`*jwt.NumericDate`, accepting integer and floating-point values) and `VerifiedIdentity.EmailVerifiedAt`",
"commit": "396032e"
},
{
"description": "`secrets` package: `Resolver`, `New` and `Env`; SystemAuth configuration `secrets.vault`; `systemauth.LoadConfigWithResolver`, `ParseConfigWithResolver`, `DecodeConfigWithResolver` and `DecodeConfigFileWithResolver`; `systemauthsvc.LoadConfigWithResolver` and `LoadConfigBytesWithResolver`",
"commit": "4a4470a"
},
{
"description": "`systemauth org member add`, `remove` and `list` (`identity/organization/membercmd`) for managing members and application roles without the HTTP API",
"commit": "66a30b1"
},
{
"description": "`GET /oauth/userinfo/memberships/resolve?slug=`, the optional `MembershipResolver` interface implemented by `EntOrgsSource`, `relyingparty.Client.ResolveOrganization` and `relyingparty.ErrOrganizationNotResolved`",
"commit": "2b666ca"
},
{
"description": "`GET {base}/policy/vocabularies` in the product contract API and `contract.WithVocabularies`",
"commit": "08a28f4"
},
{
"description": "Dependencies `github.com/plexusone/omnimail` v0.1.0 and `github.com/plexusone/omni-aws` v0.11.0",
"commit": "17fee3f"
}
],
"changed": [
{
"description": "Clients declared in the configuration file default to `first_party` and are rewritten from it at startup; clients registered at runtime default to `third_party`",
"commit": "5ad47a0"
},
{
"description": "With mail configured, a social sign-in whose provider email is not verified is held for verification instead of refused",
"commit": "54303df"
},
{
"description": "`rate_limit.redis_url` is expanded like the other secret fields; every secret field is resolved through the `secrets` resolver",
"commit": "4a4470a"
},
{
"description": "The example configuration and the CLI help no longer mention `skip_consent`",
"commit": "9496517"
}
],
"deprecated": [
{
"description": "`social_login.skip_consent`: ignored, with a warning at startup",
"commit": "6f0803a"
},
{
"description": "`DefaultSessionProvider` option `WithSkipConsent`: a no-op",
"commit": "6f0803a"
},
{
"description": "`sf_oauth_apps.first_party`: use `party`; it is kept equal to `party == first_party`",
"commit": "7004ac7"
}
],
"tests": [
{
"description": "Product entitlements in the `orgs` claim across the ID token, JWT access tokens, UserInfo and the memberships endpoint",
"commit": "2c6605a"
},
{
"description": "A managed principal created through `appauth` `Transport` against a packaged SystemAuth, and refused without DPoP proofs",
"commit": "bbf8362"
},
{
"description": "Configuration clients stored before parties existed stay first-party after an upgrade",
"commit": "2b5d5b9"
},
{
"description": "The legacy-name gate blocks Core-era names, the `cf_` prefix and upstream GitHub and Google OAuth wiring outside `identity/oauthclient`, and skips `.claude`",
"commit": "49f8029"
},
{
"description": "Verification token store conformance, verify-email and email-change flows, mail limits and the `prompt` values"
}
],
"documentation": [
{
"description": "Verified email guide: configuration, messages, abuse controls, the sign-in step, email change and what relying parties receive",
"commit": "396032e"
},
{
"description": "Sending Mail with Amazon SES: domain identity, Easy DKIM, SPF through a custom MAIL FROM domain, DMARC, a configuration set, leaving the sandbox and a least-privilege policy",
"commit": "396032e"
},
{
"description": "Client parties, consent, `prompt`, the client name and logo, and the `sf_oauth_apps.party` upgrade",
"commit": "9496517"
},
{
"description": "Secrets and `secrets.vault` in Deploying SystemAuth",
"commit": "4a4470a"
},
{
"description": "Organization slug resolution for relying parties, application vocabularies, entitlements in the `orgs` claim and `systemauth org member`"
},
{
"description": "v0.29.0 release notes"
}
]
},
{
"version": "v0.28.0",
"date": "2026-10-29",
"highlights": [
{
"description": "Layered rate limits are on by default in SystemAuth: a limit per client address on every route, route policies for the token, login, sign-up and BFF sign-in paths, limits on failed client authentication, the JWT-bearer grant and social login, and a signed-in limit, all on one shared storage and answered with IETF `RateLimit-Policy` and `RateLimit` headers"
},
{
"description": "One client-address rule (`session/clientaddr`), configured with trusted proxy networks and the header they write, used by SystemAuth, the BFF, `appauth`, rate limits and security events"
},
{
"description": "Structured security events through `contract/audit`, logs without stack traces or secrets, and no internal error text in about 60 API responses"
},
{
"description": "Static API key authentication is removed from relying parties; a threat model documents the trust boundaries and their mitigations"
}
],
"security": [
{
"description": "SystemAuth had no limit on most routes, so credential endpoints could be guessed at and any route flooded; layered rate limits now apply by default: 300 requests a minute per client address on every route, route policies for `/oauth/token` (per address and per OAuth client), `/login*`, `/signup*` and `/bff/auth/*`, and exemptions for health checks, static assets and the billing webhook",
"commit": "50704e6"
},
{
"description": "Handler limits cover failed client authentication at the token endpoint (per address and client, then per address), tokens issued per service account by the JWT-bearer grant, and social login start and callback; the login-token, waitlist, sign-up code and managed-principal code limits move onto the shared storage",
"commit": "50704e6"
},
{
"description": "On a storage outage the default layer and the signed-in limit let requests through and log the failure, while the route policies and credential handler limits refuse with `503`; a full `MemoryStorage` is always a `429`, so a flood of new addresses cannot switch limits off",
"commit": "395e6ee"
},
{
"description": "Client addresses were found in several ways, some of which believed forwarding headers from any peer; `session/clientaddr` believes `X-Forwarded-For` (read from the right) or a single-value header only from trusted proxy networks and falls back to the direct peer",
"commit": "2687031"
},
{
"description": "Token, authorize and access-token log records carried the error value, which slog's text handler formats with its stack trace; they now carry the RFC 6749 error code, description, hint and status, adding the cause by message only at debug level or for server errors",
"commit": "f82ee42"
},
{
"description": "Tests run sign-in, the authorization code flow with PKCE, refresh, code and refresh-token replay, client credentials, JWT bearer, revocation, logout and BFF sign-in and sign-out at debug level and fail if any token, code, verifier, secret, DPoP proof, assertion, key or session cookie appears in a log record or security event",
"commit": "72be528"
},
{
"description": "SystemAuth, organizations, platform administration and service accounts record security events through an `audit.Emitter`, each with the request ID and the visitor's address, and never a token, code or secret",
"commit": "b764542"
},
{
"description": "Security events recorded the direct peer as the client address, so behind a reverse proxy every event named the proxy; they now use the client-address rule",
"commit": "c21a796"
},
{
"description": "About 60 API responses across organization, marketplace, contract, invite, connections, `session/middleware`, managed principals, SystemAuth identity sync and the sign-up API copied internal error text, such as database errors, into the response body; known errors now map to fixed messages and anything else is logged and answered with a generic `500`",
"commit": "511701e"
},
{
"description": "`bff.CookieConfig` accepted `SameSite=None`, which lets the browser send the session cookie on cross-site subrequests that the origin check relies on it withholding; `Validate` and `NewHandler` now refuse it",
"commit": "107bf57"
},
{
"description": "`EmbeddedOAuthProvider.Revoke` passed the raw token where storage expects a request ID, so it reported success without revoking anything; it now revokes the access and refresh tokens of the token's family",
"commit": "3c9390d"
},
{
"description": "The `multiapp` server and the `publicclient` loopback callback server set no `ReadHeaderTimeout`, so a client could hold a connection open by sending headers slowly; every server systemforge builds now sets it",
"commit": "b390161"
},
{
"description": "Static API keys were a second, weaker way into a relying party: long-lived bearer secrets outside the short-lived, revocable access tokens; they are removed",
"commit": "bc9558e"
}
],
"breaking": [
{
"description": "`appauth.Options.APIKeys`, `relyingparty.BearerConfig.APIKeys`, `relyingparty.APIKeyValidator`, `relyingparty.AuthMethodAPIKey` and `relyingparty.AuthenticatedPrincipal.APIKeyID` are removed; a non-JWT `Bearer` value is answered `401` with `invalid_token`, and `X-API-Key` is ignored",
"commit": "bc9558e"
},
{
"description": "Rate limits are on by default in SystemAuth; set `rate_limit.disabled: true` to turn off the default layer and route policies when the host applies its own (handler limits stay on)",
"commit": "50704e6"
},
{
"description": "Organization API status codes: the personal-organization conflict is `409` on every path, a storage error while reading a member is `500` instead of `404`, and an unexpected error on create or remove is `500` instead of `400`",
"commit": "511701e"
},
{
"description": "Error responses of the organization, marketplace, contract, invite, connections, managed-principal, identity sync and sign-up APIs and the bearer and DPoP middlewares carry fixed messages instead of the underlying error text",
"commit": "511701e"
},
{
"description": "`contract.ToContractError` no longer copies a non-contract error's text into its message",
"commit": "13364ef"
},
{
"description": "`ratelimit.Limiter.Middleware` answers `429` instead of `503` when a `MemoryStorage` is full",
"commit": "395e6ee"
},
{
"description": "`bff.CookieConfig.Validate` rejects `SameSite=None`",
"commit": "107bf57"
},
{
"description": "`EmbeddedOAuthProvider.Revoke` returns an error wrapping `ErrInvalidToken` for an unknown, expired or already revoked token",
"commit": "3c9390d"
}
],
"upgradeGuide": [
{
"description": "Behind a reverse proxy, set `client_address` (for example `trusted_proxies: [\"127.0.0.1/32\", \"::1/128\"]` with `header: X-Forwarded-For`); otherwise every visitor has the proxy's address and the per-address limits apply to all of them together"
},
{
"description": "With several SystemAuth instances, set `rate_limit.redis_url`; with one, set `rate_limit.single_instance: true`"
},
{
"description": "A relying party that calls `/oauth/token` from one server address should add that network to `rate_limit.exempt_networks`, or raise the `token` route's limits under `rate_limit.routes`"
},
{
"description": "Move API key callers to `identity/publicclient` sign-in (command-line tools) or service accounts (automation), and remove `APIKeys` from `appauth.Options` and `relyingparty.BearerConfig`"
},
{
"description": "Wire an audit emitter (`systemauthsvc.Options.AuditEmitter`, or `systemauth.WithAuditEmitter`, `organization.WithAuditEmitter`, `principal.WithAuditEmitter` and `serviceaccount.Config.Audit`) and ship the events off the host"
},
{
"description": "Expect `429` responses with `Retry-After` and `RateLimit` headers, and check clients that rely on the old organization status codes or on error text in responses"
}
],
"added": [
{
"description": "`session/clientaddr`: `Resolver`, `Config` (`TrustedProxies`, `Header`), `New`, `MustNew`, `LoopbackProxy`, `Peer`, `PeerAddr`, `PeerIP` and `ParseNetworks`",
"commit": "2687031"
},
{
"description": "SystemAuth configuration `client_address.trusted_proxies` and `client_address.header`, and `systemauth.WithClientAddressResolver`; `scimtoken.WithClientAddress`, `bff.ClientIPConfig.Resolver`, `appauth.Options.ClientAddress` and `Authenticator.ClientAddress`",
"commit": "2687031"
},
{
"description": "`ratelimit.Policy`, `PolicyConfig`, `NewPolicy`, `Route`, `RouteLimit`, `DefaultRoutes`, `MergeRoutes`, the route names (`RouteToken`, `RouteLogin`, `RouteSignup`, `RouteBFFAuth`, `RouteHealth`, `RouteStatic`, `RoutePaymentWebhook`), `KeyAddress`, `KeyClient` and `AddressKey`",
"commit": "395e6ee"
},
{
"description": "`ratelimit.NewSignedInLimiter`, `SignedInKey`, `Check`, `Exhausted`, `Record`, `WriteHeaders`, `WriteTooManyRequests`, `RetryAfterSeconds`, `WithName`, `WithSkip`, and `Peek` on `MemoryStorage` and `RedisStorage`",
"commit": "395e6ee"
},
{
"description": "SystemAuth configuration `rate_limit.disabled`, `redis_url`, `single_instance`, `default`, `signed_in`, `exempt_networks` and `routes` (`name`, `paths`, `methods`, `per_address`, `per_client`, `exempt`, `fail_open`)",
"commit": "50704e6"
},
{
"description": "`systemauth.WithRateLimitStorage`, `Server.RateLimitStorage`, `Server.RateLimitPolicy`, `Server.SignedInRateLimit` and `Server.ClientAddress`; `systemauthsvc.Options.RateLimitStorage`; `appauth.Options.SignedInRateLimit`",
"commit": "50704e6"
},
{
"description": "`audit.Context.RequestID`, `audit.RequestID`, `audit.NopEmitter`, `audit.NewLogEmitter` and `requestprincipal.AuditActor`",
"commit": "8f4422e"
},
{
"description": "`systemauth.WithAuditEmitter`: sign-ins and their refusals, sign-outs, refused token requests, refresh-token reuse, authorization-code replay, token and token-family revocations, and the signing key in use at startup",
"commit": "b764542"
},
{
"description": "`organization.WithAuditEmitter`, `principal.WithAuditEmitter` and `serviceaccount.Config.Audit`: membership, ownership, platform administration and service account changes",
"commit": "0c33a0b"
},
{
"description": "`systemauthsvc.Options.AuditEmitter` reaches SystemAuth and the organization and principal services, and each production check waived at startup is recorded as a `systemauth.production.waiver` event; the `systemauth` command writes its security events to its log",
"commit": "a82d77e"
},
{
"description": "Sentinel errors `organization.ErrOwnerRequired`, `ErrAlreadyMember`, `ErrNewOwnerNotMember` and `ErrInvalidOwnerRole`",
"commit": "511701e"
},
{
"description": "Sentinel errors `marketplace.ErrInvalidListing` and `ErrListingNotDeletable`",
"commit": "1d36333"
},
{
"description": "Sentinel errors `invite.ErrInviteNotFound`, `ErrPendingInviteExists`, `ErrAlreadyMember`, `ErrAppRolesNotConfigured` and `ErrCollaboratorAppRoles`",
"commit": "f5df37b"
}
],
"changed": [
{
"description": "Responses carry the IETF `RateLimit-Policy` and `RateLimit` headers beside `X-RateLimit-*`, and `Retry-After` is rounded up to at least one second",
"commit": "395e6ee"
},
{
"description": "`systemauthsvc` warns in production when rate limit counters are per instance and no single instance is declared",
"commit": "50704e6"
},
{
"description": "The bearer and DPoP middlewares answer the matching sentinel's text, or `authentication failed`, instead of the full error chain",
"commit": "f2ce11d"
}
],
"deprecated": [
{
"description": "`ratelimit.ForwardedClientIP` and `ratelimit.ForwardedIPKey`; use a `clientaddr.Resolver` with `ratelimit.AddressKey`",
"commit": "2687031"
},
{
"description": "`systemauth.WithLoginTokenClientIP`; configure `client_address` or use `WithClientAddressResolver`",
"commit": "2687031"
},
{
"description": "`bff.CloudflareClientIPConfig` and the `ClientIPConfig` fields `TrustCloudflare`, `TrustProxy`, `TrustedProxies` and `CloudflareIPRanges`; set `ClientIPConfig.Resolver`",
"commit": "2687031"
}
],
"removed": [
{
"description": "`appauth.Options.APIKeys`, `relyingparty.BearerConfig.APIKeys`, `relyingparty.APIKeyValidator`, `relyingparty.AuthMethodAPIKey` and `relyingparty.AuthenticatedPrincipal.APIKeyID`",
"commit": "bc9558e"
}
],
"fixed": [
{
"description": "Security events behind a reverse proxy recorded the proxy's address instead of the visitor's",
"commit": "c21a796"
},
{
"description": "`EmbeddedOAuthProvider.Revoke` reported success without revoking the token",
"commit": "3c9390d"
}
],
"tests": [
{
"description": "Client addresses: spoofed headers, proxy chains, IPv6 and ports",
"commit": "59e4812"
},
{
"description": "Rate limits: each layer, headers, storage outages and exemptions",
"commit": "33fa3c0"
},
{
"description": "Logs and security events carry no tokens or secrets, and each security event is recorded",
"commit": "72be528"
},
{
"description": "No internal error text in the responses of each API changed"
}
],
"documentation": [
{
"description": "Threat model: scope and assumptions, assets, actors, trust boundaries, threats and mitigations per boundary, and residual risks",
"commit": "5cee54e"
},
{
"description": "Security events: how to record them, their common fields and each event type",
"commit": "0af03b0"
},
{
"description": "Client addresses and rate limiting guide, with the `client_address` and `rate_limit` settings in Deploying SystemAuth and the production checklist",
"commit": "b202382"
},
{
"description": "Upgrade section for the removal of static API keys in the application authentication guide",
"commit": "1c4e6db"
},
{
"description": "Rate limiting described as shipped in the threat model",
"commit": "3bde6b3"
},
{
"description": "v0.28.0 release notes"
}
]
},
{
"version": "v0.27.0",
"date": "2026-10-28",
"highlights": [
{
"description": "Production hardening for SystemAuth: authorization codes and refresh tokens are used once under concurrency, social sign-in links to an existing account by email only when the provider is authoritative for the address, production startup fails closed on the token HMAC secret, bearer-token clients and a per-instance DPoP replay cache, and every response carries security headers"
},
{
"description": "Payments move onto payforge in the new `billing` package: a verified, idempotent webhook handler, plans, sales, refunds, disputes and sellers applied to the database and the authorization graph, and checkout and customer-portal handlers with configured redirects; `marketplace/stripe` is removed"
},
{
"description": "A security policy (`SECURITY.md`) and a production checklist for operators"
}
],
"security": [
{
"description": "An authorization code could be exchanged, and a refresh token rotated, twice by concurrent requests, because Fosite reads before it writes and the storage did not run the two in one transaction; invalidating a code now matches only an unused code and rotation retires the presented refresh token only while it is active, so the losing request gets no tokens",
"commit": "d94d342"
},
{
"description": "The memory store returned no request for a used authorization code, so a replayed code answered `server_error` and left the tokens it had issued active; it now returns the request and the tokens are revoked",
"commit": "d94d342"
},
{
"description": "A new GitHub or Google identity was linked to any existing account with the same verified email, so whoever kept a stale verification of an address (such as a former work address) could sign in to the account of its current owner; an identity now links by email only when the provider is authoritative for the address (Google for gmail.com and googlemail.com, and for a Workspace account's own `hd` domain), and GitHub sign-in uses the primary email only",
"commit": "f5ba501"
},
{
"description": "SystemAuth started in production without a token HMAC secret, with statically configured `allow_bearer_tokens` clients, and with a per-instance DPoP replay cache; these now fail closed unless waived in `production.allow` or, for the replay cache, `dpop.single_instance` is set, and the token endpoint refuses `client_credentials` with a shared secret and `allow_bearer_tokens` clients with `unauthorized_client`",
"commit": "74f8f52"
},
{
"description": "SystemAuth pages set no security headers; every response now carries a content security policy with `frame-ancestors 'none'`, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy: same-origin` and, on an https issuer, HSTS",
"commit": "d49cd53"
},
{
"description": "The `marketplace/stripe` payment path had no webhook handler, failed on Stripe API-version mismatches, never retried a failed authorization sync, had no replay or ordering protection, redirected to request-supplied `success_url` and `cancel_url` (an open redirect), returned raw errors to clients, and would retry another application's events forever; `billing` on payforge replaces it",
"commit": "d5e6d5b"
}
],
"breaking": [
{
"description": "`marketplace/stripe`, `marketplace.CheckoutService` and its request and session types, `EntService.WithCheckout`, `Service.Checkout`, the marketplace API's `POST /checkout` and `WithPurchaseAuthorizer` are removed; use `billing.Checkout` and its endpoints, passing the `PurchaseAuthorizer` as `CheckoutConfig.Purchases`",
"commit": "d5e6d5b"
},
{
"description": "Outside development mode SystemAuth refuses to start without `tokens.hmac_secret`, or without `dpop.replay_cache_redis_url`, `Options.DPoPReplayCache` or `dpop.single_instance`, or with a statically configured client that sets `allow_bearer_tokens`, unless the check is waived in `production.allow`",
"commit": "74f8f52"
},
{
"description": "Outside development mode the token endpoint refuses the `client_credentials` grant from a client authenticating with a shared secret, and every request from an `allow_bearer_tokens` client, with `unauthorized_client`, unless waived",
"commit": "852ae8f"
},
{
"description": "A social sign-in links to an existing account by email only when the provider is authoritative for the address; otherwise an email that belongs to an account is refused with `ErrEmailConflict`, and GitHub sign-in uses the primary email only",
"commit": "f5ba501"
},
{
"description": "SystemAuth pages send `Referrer-Policy: same-origin` instead of `no-referrer`",
"commit": "d49cd53"
},
{
"description": "`marketplace.Subscription.IsActive` is true for a past-due subscription",
"commit": "03abe3b"
}
],
"added": [
{
"description": "`billing`: `Store`, payforge's store on the Ent client for processed event IDs (`sf_billing_events`) and the customer-to-organization mapping (`sf_billing_customers`)",
"commit": "4f4b6a5"
},
{
"description": "`billing/pglock`: payforge's `Locker` as a PostgreSQL transaction-scoped advisory lock held on its own connection, shared by every instance",
"commit": "4f4b6a5"
},
{
"description": "`billing.NewWebhookHandler`: verifies with payforge's reader under its body limit, applies the event under the organization's lock, answers fixed words, and acknowledges duplicate, ignored, other-mode and foreign events",
"commit": "47e2546"
},
{
"description": "`billing.NewSink`: payforge's entitlement, sale, seller and dispute sinks and the webhook's checkout completer, applying plans, licenses from recorded orders (`sf_billing_orders`), refunds (`sf_billing_refunds`), disputes (`sf_billing_disputes`) and seller accounts (`sf_billing_sellers`) in one transaction and re-writing the authorization graph on every apply",
"commit": "5fc3fbc"
},
{
"description": "`billing.NewSellerGate`, `marketplace.PublishGate`, `marketplace.ErrSellerNotReady` and `EntListingService.WithPublishGate`: a paid listing is published only when its seller can take charges",
"commit": "5fc3fbc"
},
{
"description": "`billing.NewCheckout`: plan checkout, one-time listing checkout and the customer portal for the caller's organization, with configured https redirects on allowed hosts and fixed JSON errors; `billing.NewStripe` builds payforge's Stripe client and webhook reader for production",
"commit": "67e08b6"
},
{
"description": "`appauth.Options.SingleInstance`; `New` warns when DPoP proofs use a per-instance replay cache on a public URL",
"commit": "3b1b26d"
},
{
"description": "`production.allow` (`ephemeral_hmac_secret`, `client_secret_client_credentials`, `bearer_token_clients`), `dpop.single_instance`, and `systemauth.WithProductionPolicy` with `ProductionPolicy`",
"commit": "852ae8f"
},
{
"description": "`oauthclient.User.HostedDomain`, read from Google's `hd` claim, and `systemauth.AuthoritativeEmail` with `ExternalLogin.EmailAuthoritative`",
"commit": "f5ba501"
},
{
"description": "Ent tables `sf_billing_events`, `sf_billing_customers`, `sf_billing_orders`, `sf_billing_refunds`, `sf_billing_disputes` and `sf_billing_sellers`, created by the schema migration",
"commit": "4f4b6a5"
},
{
"description": "`relyingparty.BFFConfig.LoginErrorPath` and `appauth.Options.LoginErrorPath`: a browser whose sign-in fails at the BFF's login or callback routes is redirected to the app's sign-in page with `?error=<CODE>` instead of shown a JSON error; requests that ask for JSON still get JSON",
"commit": "69ce7a4"
},
{
"description": "`relyingparty.BFF.SignOut` ends a browser session (deletes it, revokes its SystemAuth refresh token, clears the cookie) without writing a response, for handlers that serve their own sign-out; `/bff/auth/logout` calls it after its cross-origin check",
"commit": "fb08960"
}
],
"changed": [
{
"description": "Discovery stops listing `client_credentials` when the production policy refuses it",
"commit": "852ae8f"
}
],
"fixed": [
{
"description": "A past-due subscription keeps its plan while the provider retries the payment, so the organization's entitlements no longer drop the paid tier during retries",
"commit": "03abe3b"
},
{
"description": "Cookie-authenticated writes required a JSON body even with an allowed `Origin`, so signed-in browsers could not upload files with `multipart/form-data` or send bodyless writes; with an allowed `Origin` any body is now accepted, and without one the JSON body and an allowed `Referer` are still required",
"commit": "f59dd02"
},
{
"description": "`billing.NewCheckout` accepted loopback redirect URLs with any Stripe key; they now pass only with a test-mode key, and a provider that reports no mode counts as live",
"commit": "b906d66"
}
],
"removed": [
{
"description": "`marketplace/stripe`, `marketplace.CheckoutService`, `EntService.WithCheckout`, `Service.Checkout`, `POST /checkout` and `WithPurchaseAuthorizer`",
"commit": "d5e6d5b"
}
],
"tests": [
{
"description": "Production checks, waivers and the replay cache warning",
"commit": "67feabf"
},
{
"description": "Security headers on every SystemAuth page",
"commit": "9a508ff"
},
{
"description": "SCIM-provisioned users link only on authoritative sign-ins: a Workspace sign-in links, a GitHub sign-in with the same verified address is refused",
"commit": "ee8a640"
}
],
"documentation": [
{
"description": "Security policy (`SECURITY.md`): reporting through GitHub private vulnerability reporting, response targets, supported versions and disclosure",
"commit": "ad689bc"
},
{
"description": "Production checklist for operators",
"commit": "f439c79"
},
{
"description": "Production checks, waivers and `dpop.single_instance` in Deploying SystemAuth and the DPoP pages",
"commit": "0314b62"
},
{
"description": "Security headers in Deploying SystemAuth",
"commit": "0631cb0"
},
{
"description": "The production checklist names each check's key and waiver",
"commit": "32b5d63"
},
{
"description": "Billing guide, with the upgrade from `marketplace/stripe`",
"commit": "d5e6d5b"
},
{
"description": "Production hardening and billing roadmap",
"commit": "8cd6533"
},
{
"description": "v0.27.0 release notes"
}
],
"dependencies": [
{
"description": "Add `github.com/plexusone/payforge` v0.1.0",
"commit": "4f4b6a5"
},
{
"description": "Add `github.com/plexusone/systemspec-pricingpackaging` v0.1.0",
"commit": "4f4b6a5"
},
{
"description": "Remove `github.com/stripe/stripe-go/v84`; payforge brings `stripe-go/v87` indirectly",
"commit": "d5e6d5b"
}
],
"upgradeGuide": [
{
"description": "Set `tokens.hmac_secret` (at least 32 bytes, stable across deploys, for example `${SYSTEMAUTH_TOKEN_HMAC_SECRET}`); people sign in once more after the first deploy with it"
},
{
"description": "Set `dpop.single_instance: true` when one instance serves SystemAuth, or `dpop.replay_cache_redis_url` when several do; set `appauth.Options.SingleInstance` for an application served by one instance"
},
{
"description": "Move `client_credentials` clients to service accounts, or list `client_secret_client_credentials` in `production.allow`; likewise `bearer_token_clients` for `allow_bearer_tokens` clients"
},
{
"description": "Migrate from `marketplace/stripe` to `billing` following the billing guide, and run the schema migration for the `sf_billing_*` tables"
},
{
"description": "A person who signed up with one provider and now uses another whose email is not authoritative for the address signs in the original way"
},
{
"description": "A reverse proxy must not overwrite `Referrer-Policy`: `same-origin` keeps the `Origin` header on same-origin form posts"
}
]
},
{
"version": "v0.26.0",
"date": "2026-10-28",
"highlights": [
{
"description": "`identity/appauth` assembles the relying party for an application in a few lines, with the same defaults everywhere: a required DPoP key, labelled configuration errors, memberships filtered to the application's roles, one request-authentication chain, and calls to SystemAuth for the signed-in person"
},
{
"description": "Service accounts: automation that belongs to an organization, holds application roles there, and calls APIs with short-lived DPoP-bound tokens it gets by signing an assertion with a private key (the RFC 7523 JWT-bearer grant), managed with the `service-account` command and used from Go with `identity/serviceclient`"
},
{
"description": "Refresh tokens survive a SystemAuth restart and work on every instance with `tokens.hmac_secret`, which rotates with `tokens.hmac_verification_secret`"
}
],
"added": [
{
"description": "`identity/appauth`: `Config` with `Validate` and labels a host can relabel, `ConfigFromEnv`, `ParseDPoPKey`, `ReadDPoPKeyFile` and `DPoPKeyCommand`",
"commit": "d4f1f61"
},
{
"description": "`appauth.New`: the relying-party client from metadata or discovery, the `/bff/*` handler, an application membership filter, `Middleware` and `Authenticate` returning a `requestprincipal.RequestPrincipal`, extra authenticators, `SessionToken` and `Transport` for calls to SystemAuth, and RFC 9728 resource metadata",
"commit": "abd85d8"
},
{
"description": "`pgstore.PrincipalStore` on a new `sf_rp_principals` table created by `EnsureSchema`, with `SetActive` and `Delete`; `NewStores` returns it",
"commit": "6708140"
},
{
"description": "`storetest.PrincipalStore`, a conformance suite for principal stores",
"commit": "e79798a"
},
{
"description": "`identity/serviceaccount`: service accounts in an organization with a `service_account` membership, keys generated as P-256 or supplied as ES256, RS256 or EdDSA, application roles, and disable, enable, delete and key revocation recorded as access changes; revoking a usable key ends the account's current tokens",
"commit": "3f236a1"
},
{
"description": "The JWT-bearer grant behind `oauth.jwt_bearer` (enabled, maximum assertion lifetime, token lifetime): self-issued assertions verified against a service account's keys, a DPoP proof required, replayed `jti` refused, tokens with the account as subject, the `orgs` and `principal_type` claims and no refresh token, under the reserved client `systemauth-jwt-bearer`",
"commit": "78fc8ac"
},
{
"description": "`identity/serviceclient`: signs assertions, requests DPoP-bound tokens with nonce retries, renews them before expiry, and adds proofs to API requests",
"commit": "425235d"
},
{
"description": "`relyingparty.BearerConfig.ServicePrincipals` and `VerifiedIdentity.Service`: an application creates a service account's local principal on its first request, and refuses a service-account token whose local principal is not of type service; `appauth.Options.ServiceAccounts` turns it on and accepts `relyingparty.ServiceAccountClientID`",
"commit": "6a466d9"
},
{
"description": "`serviceaccountcmd`, the `service-account` command (`create`, `list`, `show`, `disable`, `enable`, `delete`, `key add|list|revoke`, `role grant|remove`) that `systemauth` registers and a host mounts",
"commit": "6168a12"
},
{
"description": "`principal.Service.CreateServiceInTx` creates a service principal inside a caller's transaction",
"commit": "aca6b61"
},
{
"description": "`MemoryPrincipalStore.SetActive`",
"commit": "6a466d9"
},
{
"description": "`tokens.hmac_secret` and `tokens.hmac_verification_secret` (`TokenConfig.HMACSecret`, `HMACVerificationSecret`, `MinHMACSecretLength`): the secret signing authorization codes, refresh tokens and opaque access tokens, at least 32 bytes and expanded from the environment, and a second secret accepted but not signed with during a rotation",
"commit": "54f81f9"
}
],
"changed": [
{
"description": "`approle` accepts `service_account` memberships, so service accounts hold application roles",
"commit": "7e1535f"
},
{
"description": "Token introspection includes the `orgs`, `orgs_truncated` and `principal_type` values the access token was issued with, for every principal",
"commit": "7b154a2"
}
],
"deprecated": [
{
"description": "`appauth.Options.APIKeys`: command-line tools sign in with `identity/publicclient`, and automation uses service accounts (ADR-016)"
}
],
"fixed": [
{
"description": "`relyingparty.RequestAuthenticator` answers a cookie-authenticated cross-origin write with `403 CSRF_REJECTED` and no challenge, instead of a `401` that tells the client to sign in again",
"commit": "662ffaa"
},
{
"description": "Refresh tokens and authorization codes no longer stop working at every SystemAuth restart and on other instances when `tokens.hmac_secret` is set; without it SystemAuth warns that they will",
"commit": "54f81f9"
}
],
"tests": [
{
"description": "`appauth` against an embedded SystemAuth requiring DPoP nonces: browser sign-in, cross-origin writes, a restart with the same key and stores, command-line tokens, another client's tokens, a bound token refused as Bearer, and calls to SystemAuth with `Transport`",
"commit": "51647ea"
},
{
"description": "The JWT-bearer grant end to end with P-256 and RSA keys: issuance, introspection, DPoP binding, and refusals for replay, expiry, overlong lifetime, missing `jti`, wrong audience, missing proof, revoked keys and disabled accounts",
"commit": "9df9951"
},
{
"description": "From command to application: an account created, granted a role and given a key calls an `appauth` API through `serviceclient`, loses the role's organization after the role is removed, gets no token once disabled, and is deleted with its issued tokens in place",
"commit": "633812d"
},
{
"description": "Codes and refresh tokens across a restart with and without `tokens.hmac_secret`, and a secret rotation through pre-staging, promotion, rollback and retirement",
"commit": "8f67e3d"
}
],
"documentation": [
{
"description": "Authenticating an Application guide, including moving an existing principals table into `sf_rp_principals`",
"commit": "86cfadf"
},
{
"description": "Service Accounts walkthrough, and ADR-016 records the service account's JWT-bearer grant",
"commit": "4aa14ce"
},
{
"description": "The relying-party setup example sets the DPoP key; the `session/bff` and session pages point to `identity/appauth`",
"commit": "86cfadf"
},
{
"description": "Specs for application authentication and service accounts",
"commit": "8fe4bc5"
},
{
"description": "The API keys page shows a truncated sample key, and the API key, marketplace and Ent store examples use the `sf` prefix",
"commit": "60e437f"
},
{
"description": "The token HMAC secret and its rotation in Deploying SystemAuth and Rotating Keys and Secrets",
"commit": "ea624df"
},
{
"description": "v0.26.0 release notes"
}
]
},
{
"version": "v0.25.0",
"date": "2026-10-27",
"highlights": [
{
"description": "`relyingparty.Client.Transport` lets relying parties call SystemAuth's APIs, such as the managed-principal API with `mpclient`, with their people's DPoP-bound session tokens, which SystemAuth refuses as `Bearer`"
}
],
"added": [
{
"description": "`relyingparty.Client.Transport`: an `http.RoundTripper` that presents the token the caller set in `Authorization` with the `DPoP` scheme and a new proof when it is bound to the client's key (or opaque), as `Bearer` when unbound, refuses a JWT bound to another key with `ErrForeignDPoPToken` (SystemAuth refuses an opaque one), and retries once with SystemAuth's nonce when the request body can be sent again",
"commit": "5538912"
}
],
"changed": [
{
"description": "The relying party's calls to userinfo, memberships and access changes go through `Transport`; a JWT access token is sent only once its signature verifies against SystemAuth's keys",
"commit": "5538912"
}
],
"tests": [
{
"description": "Cover the transport for each kind of token, another key's token, and nonce retries with and without a resendable body; against SystemAuth, `mpclient`'s bound token is refused as `Bearer` and works through the transport, with and without nonces",
"commit": "d5c5e4f"
}
],
"documentation": [
{
"description": "`mpclient`, Managed Principals, Relying Party and Calling APIs with DPoP: sending DPoP-bound session tokens to SystemAuth's APIs",
"commit": "8940e2b"
},
{
"description": "v0.25.0 release notes"
}
],
"security": [
{
"description": "`relyingparty.Client.Transport` reads a JWT's `cnf` binding only after its signature verifies against SystemAuth's keys and its issuer matches, so it makes no DPoP proof for a token SystemAuth did not issue; a forged token is refused with `ErrInvalidToken`"
}
]
},
{
"version": "v0.24.0",
"date": "2026-10-27",
"highlights": [