Skip to content
IDFoundryPublic

About

SSF CAEP

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

135 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SSFgo

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.0 is tagged the API is covered by COMPATIBILITY.md. The Transmitter passes every module of the OIDF CAEP Interoperability Profile Transmitter plan, and the Receiver every module of the Receiver plan — see conformance/README.md. The full matrix runs daily in CI. OIDF has not yet opened SSF certification. See ROADMAP.md.

Specifications

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
RFC 9967 SCIM events all 12 event types and the scim subject; one event per SET
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.

Usage

GETTING_STARTED.md walks through embedding each role step by step, and docs/guides covers one feature each — session revocation, push or poll, the CAEP Interoperability Profile, SCIM events, storage and scaling, testing, observability and keys in a KMS or HSM. In brief:

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(),
	Assurance:       ssf.AssuranceDevelopment, // AssuranceProduction refuses in-memory stores: use storage/sqlstore
	Authorize:       authorizeAccessToken, // your OAuth resource-server check
	PermitEvent:     permitEvent,          // which Receiver may see which subject's events
	Limits:          transmitter.RecommendedLimits(),
	PushRetry:       transmitter.RecommendedPushRetry(),
})
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: ssf.NewSecret(secret), AuthMethod: receiver.ClientSecretBasic},
	ReplayStore: memstore.NewReplayStore(),
	Assurance:   ssf.AssuranceDevelopment,
	Limits:      receiver.RecommendedLimits(), // replay window, key age, clock skew
})
// 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)
})
pushSecret := ssf.NewSecret("Bearer " + randomToken) // withheld from logs and %v; Reveal() reads it
http.Handle("/ssf/events", rx.PushHandler(receiver.PushOptions{AuthorizationHeader: pushSecret}))
// Creates the stream on the first start; later starts reuse it, updating
// whatever changed, and wait out a Transmitter that is briefly unavailable.
stream, err := rx.EnsureStream(ctx, receiver.StreamRequest{Delivery: &ssf.Delivery{
	Method: ssf.DeliveryPush, EndpointURL: "https://rp.example.com/ssf/events", AuthorizationHeader: pushSecret,
}})

memstore keeps everything in memory, so ssf.AssuranceProduction refuses it. To survive restarts, or to run several instances on one database (HorizontallyScaled, which needs PostgreSQL), 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(ctx, db, sqlstore.Postgres)   // transmitter.Config.Store
replay, err := sqlstore.NewReplayStore(ctx, db, sqlstore.Postgres)  // receiver.Config.ReplayStore
revocations, err := sqlstore.NewRevocationStore(ctx, db, sqlstore.Postgres) // revocation.New

examples/session-revocation runs both sides in one process: cd examples/session-revocation && go run .. Like every example, it is its own module and uses only the public API.

To revoke tokens as events arrive, revocation records what session-revoked, account-disabled and similar events mean and checks the application's validated tokens against it:

rev, err := revocation.New(memstore.NewRevocationStore(), revocation.Options{
	Issuers:   revocation.SameIssuer,           // or StaticTokenIssuers{transmitter: tokenIssuer}
	Events:    revocation.RecommendedEvents(),  // session-revoked, account-disabled, ...
	Retention: 24 * time.Hour,                  // at least the longest token lifetime
	Assurance: ssf.AssuranceDevelopment,        // production needs a durable store, e.g. sqlstore
})
rev.Register(rx)
api = rev.Middleware(tokenOf, api)   // 401 for a token issued before its revocation

Both roles report what they do through optional Hooks in their config, for metrics or traces without a dependency on any metrics library, and answer readiness probes with Ready:

cfg.Hooks = receiver.Hooks{SET: func(ctx context.Context, i receiver.SETInfo) {
	setsTotal.WithLabelValues(string(i.EventType), i.Outcome.String()).Inc() // e.g. Prometheus
}}
http.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
	if err := rx.Ready(r.Context()); err != nil {
		http.Error(w, err.Error(), http.StatusServiceUnavailable)
	}
})

To test an application that plays one role, ssftest runs the other in-process: ssftest.NewTransmitter for testing a Receiver, ssftest.NewReceiver for testing a Transmitter.

tx := ssftest.NewTransmitter(t)
rx, err := receiver.New(ctx, tx.ReceiverConfig(registry))
stream, err := rx.EnsureStream(ctx, receiver.StreamRequest{})
err = tx.Emit(ctx, subject, caep.SessionRevoked{...})
_, err = rx.Poll(ctx, stream, receiver.PollOptions{}) // your handlers run

Design

See ARCHITECTURE.md, and SECURITY.md for the security model and how to report a vulnerability.

Contributing

See CONTRIBUTING.md. Changes are listed in CHANGELOG.md, and UPGRADING.md says what to change for each breaking one.

License

MIT — see LICENSE.

About

SSF CAEP

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages