SSFgo is a lightweight Go implementation of the OpenID Shared Signals Framework, CAEP and RISC, providing embeddable Transmitter and Receiver capabilities with a focus on standards compliance and interoperability.
Status: API frozen for v1.0, not yet released. Both roles are implemented; once
v1.0.0is tagged the API is covered by COMPATIBILITY.md. The Transmitter passes every module of the OIDF CAEP Interoperability Profile Transmitter plan; the Receiver passes every module of the Receiver plan except one blocked by a conformance-suite defect — see conformance/README.md. The full matrix runs daily in CI. OIDF has not yet opened SSF certification. See ROADMAP.md.
| Specification | Status in SSFgo |
|---|---|
| OpenID Shared Signals Framework 1.0 | Transmitter and Receiver |
| OpenID CAEP 1.0 | all 8 event types |
| OpenID RISC 1.0 | all 14 event types |
| CAEP Interoperability Profile 1.0 | both roles pass the OIDF plans; caep/interop enforces the profile for each |
| RFC 8417 Security Event Token | done |
| RFC 9493 Subject Identifiers | done |
| RFC 8935 Push delivery / RFC 8936 Poll delivery | both sides |
The module has no third-party dependencies. Durable storage for
PostgreSQL and SQLite is a separate module,
storage/sqlstore, which imports no database driver
itself.
A Transmitter — for example inside an identity provider — serves the SSF endpoints and emits events:
tx, err := transmitter.New(transmitter.Config{
Issuer: "https://idp.example.com/ssf",
SigningKeys: []transmitter.SigningKey{{Signer: key, Algorithm: ssf.RS256, KeyID: "2026-09"}},
EventsSupported: interop.EventTypes(),
DeliveryMethods: []ssf.DeliveryMethod{ssf.DeliveryPush, ssf.DeliveryPoll},
DefaultSubjects: ssf.DefaultSubjectsAll,
Store: memstore.NewStreamStore(),
Authorize: authorizeAccessToken, // your OAuth resource-server check
})
go tx.Run(ctx) // push delivery
http.ListenAndServeTLS(":443", cert, key, tx.Handler())
tx.Emit(ctx, ssf.IssSubSubject{Issuer: iss, Subject: "alice"}, caep.SessionRevoked{
Common: caep.Common{ReasonAdmin: ssf.LocalizedText{"en": "Suspicious activity"}},
})A Receiver — for example inside a relying party — creates a stream and handles typed events:
rx, err := receiver.New(ctx, receiver.Config{
Issuer: "https://idp.example.com/ssf",
Audience: "https://rp.example.com",
Registry: registry, // ssf.NewRegistry() + caep.Register
Algorithms: []ssf.SignatureAlgorithm{ssf.RS256},
TokenSource: &receiver.ClientCredentials{TokenURL: tokenURL, ClientID: id, ClientSecret: secret, AuthMethod: receiver.ClientSecretBasic},
ReplayStore: memstore.NewReplayStore(),
})
// Optional: interop.ApplyReceiver(&cfg) before receiver.New holds the
// Transmitter to the CAEP Interoperability Profile.
receiver.On(rx, func(ctx context.Context, set ssf.SET, e caep.SessionRevoked) error {
return sessions.RevokeAll(ctx, set.Subject)
})
http.Handle("/ssf/events", rx.PushHandler(receiver.PushOptions{AuthorizationHeader: pushSecret}))
stream, err := rx.CreateStream(ctx, receiver.StreamRequest{Delivery: &ssf.Delivery{
Method: ssf.DeliveryPush, EndpointURL: "https://rp.example.com/ssf/events", AuthorizationHeader: pushSecret,
}})memstore keeps everything in memory. To survive restarts, or to run
several Transmitter instances on one database, use storage/sqlstore:
// go get github.com/idfoundry/ssfgo/storage/sqlstore
db, err := sql.Open("pgx", dsn) // any database/sql driver for PostgreSQL or SQLite
err = sqlstore.CreateSchema(ctx, db, sqlstore.Postgres)
store, err := sqlstore.NewStreamStore(db, sqlstore.Postgres) // transmitter.Config.Store
replay, err := sqlstore.NewReplayStore(db, sqlstore.Postgres) // receiver.Config.ReplayStoreexamples/session-revocation runs both
sides in one process: go run ./examples/session-revocation.
See ARCHITECTURE.md, and SECURITY.md for the security model and how to report a vulnerability.
See CONTRIBUTING.md. Changes are listed in CHANGELOG.md.
MIT — see LICENSE.