A lightweight Shared Signals Framework receiver based on SGNL's ssfreceiver library that validates incoming Security Event Tokens (SETs) and forwards them to one or more sinks.
For easy deployment, see the Docker deployment guide.
We've created some guides for common use cases we call "recipes":
- Go 1.26+
- A running SSF transmitter that supports push delivery
go install github.com/twosense/ssf-forwarder/cmd/ssf-forwarder@latestOr build from source:
git clone https://github.com/twosense/ssf-forwarder
cd ssf-forwarder
go build -o ssf-forwarder ./cmd/ssf-forwarderCreate a YAML config file. The only required fields are receiver.public_url, transmitter.metadata_url, transmitter.auth, and at least one sink.
receiver:
public_url: "https://receiver.example.com" # externally reachable URL for this service
listen_addr: ":8080" # default: :8080
endpoint: "/events" # default: /events
transmitter:
metadata_url: "https://transmitter.example.com/.well-known/ssf-configuration"
auth:
type: bearer
token: "your-token-here"
events_requested:
- "https://schemas.openid.net/secevent/caep/event-type/session-revoked"
- "https://schemas.openid.net/secevent/caep/event-type/credential-change"
sinks:
- type: webhook
url: "https://webhook.example.com/events"The service registers public_url + endpoint as the push delivery URL when it connects to the transmitter. Make sure that address is reachable by the transmitter.
Bearer token:
transmitter:
auth:
type: bearer
token: "your-token-here"OAuth2 client credentials:
transmitter:
auth:
type: oauth2
token_url: "https://auth.example.com/token"
client_id: "your-client-id"
client_secret: "your-client-secret"By default, the raw SET (the JWT string) is POST-ed to the webhook URL, with the original Content-Type header forwarded. The request is retried up to three times with exponential backoff.
Add or override headers:
sinks:
- type: webhook
url: "https://webhook.example.com/events"
headers:
Authorization: "Bearer sink-token"
X-Source: "ssf-forwarder"Rewrite the request body with a Go template:
The template has access to .RawToken (the raw JWT string) and .Claims (a map of the decoded JWT payload claims).
sinks:
- type: webhook
url: "https://webhook.example.com/events"
headers:
"Content-Type": "application/json"
body_template: |
{"token": "{{.RawToken}}", "issuer": "{{index .Claims "iss"}}"}Multiple sinks:
All sinks receive every event. Delivery to each sink is attempted concurrently.
sinks:
- type: webhook
url: "https://first.example.com/events"
- type: webhook
url: "https://second.example.com/events"
headers:
Authorization: "Bearer other-token"The log sink prints structured information about each received SET to stdout. Useful for debugging or as an audit trail alongside other sinks.
sinks:
- type: logEach event is logged with the following fields: issuer, jti, iat, event_types, and txn (if present).
ssf-forwarder --config config.yamlThe --config flag defaults to config.yaml in the current directory.
On startup, the service:
- Fetches transmitter metadata from
metadata_url - Registers a push stream with the transmitter (or reuses an existing one)
- Starts listening for incoming SETs
On shutdown (SIGINT/SIGTERM), the stream is deleted from the transmitter before the process exits.
ssf-forwarder supports all CAEP event types defined in the CAEP specification.
| Event type | URI |
|---|---|
| Session Revoked | https://schemas.openid.net/secevent/caep/event-type/session-revoked |
| Token Claims Change | https://schemas.openid.net/secevent/caep/event-type/token-claims-change |
| Credential Change | https://schemas.openid.net/secevent/caep/event-type/credential-change |
| Assurance Level Change | https://schemas.openid.net/secevent/caep/event-type/assurance-level-change |
| Device Compliance Change | https://schemas.openid.net/secevent/caep/event-type/device-compliance-change |
| Session Established | https://schemas.openid.net/secevent/caep/event-type/session-established |
| Session Presented | https://schemas.openid.net/secevent/caep/event-type/session-presented |
| Risk Level Change | https://schemas.openid.net/secevent/caep/event-type/risk-level-change |
| Verification | https://schemas.openid.net/secevent/ssf/event-type/verification |
| Stream Updated | https://schemas.openid.net/secevent/ssf/event-type/stream-updated |
go test ./...
go vet ./...The end-to-end tests in test/e2e/ run the real compiled binary against an in-process fake transmitter and webhook sink. They are excluded from go test ./... by a build tag and must be run explicitly:
go test -tags e2e -count 1 ./test/e2e/...The test builds the binary from source automatically — no extra setup required.
You can also run the E2E tests against the built Docker image. This requires host networking, so it will only work on Linux:
E2E_DOCKER=1 go test -tags e2e -count 1 ./test/e2e/...