Last updated 2026-05-28
JWT Keys
notify validates client-facing requests with an HS256 JWT signature.
The verification key is a shared secret you generate, hand to the JWT
issuer (identity), and hand to notify via
NOTIFY_AUTH_JWT_SECRET.
When you'd care
You are setting up a production deployment, rotating a leaked secret, or wiring an existing identity-issued JWT into notify.
Generating the secret
Use a CSPRNG. Anything 32 bytes or larger is fine for HS256 (256 bits of entropy matches the HMAC-SHA-256 output size).
# 32 random bytes as a hex string (64 hex chars)openssl rand -hex 32# → 4f7b9b1c8e0a8e3a... (sample)
# Or via /dev/urandomhead -c 32 /dev/urandom | xxd -p -c 64Pass it to notify
docker run --rm \ -p 8080:8080 -p 8081:8081 -p 9090:9090 \ -e NOTIFY_AUTH_JWT_SECRET=$(openssl rand -hex 32) \ -e NOTIFY_INTERNAL_TOKEN=$(openssl rand -hex 32) \ -e NOTIFY_STORE_DRIVER=memory \ ghcr.io/elloloop/notify:0.1.0In production prefer a secrets manager (Kubernetes Secret, AWS Secrets Manager, Azure Key Vault) and project the value into the container as an env var.
Pinning issuer and audience
When the JWT issuer is identity, set the matching expected claims so notify rejects tokens minted by anyone else:
-e NOTIFY_AUTH_JWT_ISSUER=https://identity.example.com \-e NOTIFY_AUTH_JWT_AUDIENCE=notify
If your identity tenant uses different values, copy them verbatim
from the issuer config. A missing or mismatched claim returns
CodeUnauthenticated.
Token shape
notify expects the standard claims plus a tenant claim:
{ "alg": "HS256", "typ": "JWT"}.{ "sub": "user-alice", "tenant": "acme", "email": "alice@example.com", "iss": "https://identity.example.com", "aud": "notify", "exp": 1748390400, "iat": 1748386800} sub populates Claims.UserID;
tenant (or the alternative tenant_id)
populates Claims.TenantID; email is
optional and surfaces in observability.
Minting a test token
Tiny one-off generator in Python — useful for manual cURLs against a locally-deployed notify with a real JWT secret:
# requires: pip install pyjwtimport time, jwt
SECRET = "your-shared-hex-secret"token = jwt.encode( { "sub": "user-alice", "tenant": "acme", "email": "alice@example.com", "iat": int(time.time()), "exp": int(time.time()) + 900, }, SECRET, algorithm="HS256",)print(token)# requires: go install github.com/golang-jwt/jwt/v5/cmd/jwt@latest (or hand-roll)jwt encode --secret "your-shared-hex-secret" --alg HS256 \ '{"sub":"user-alice","tenant":"acme","exp":1748390400,"iat":1748386800}'Rotation
notify v0.1 validates against a single secret — there is no key ring on the verifier. To rotate without a flap, the standard procedure is:
- Issue a new secret
S2. - Configure the JWT issuer (identity) to dual-issue: every new token signed with
S2, every legacy token still signed withS1tracked bykid. - Run two notify replicas in parallel: one with
NOTIFY_AUTH_JWT_SECRET=S1, one withNOTIFY_AUTH_JWT_SECRET=S2. Route traffic by inspecting thekidin the JWT header at the gateway. - After all
S1-signed tokens have expired (one access-token TTL window), retire theS1replica and the issuer'sS1signing key.
The simpler alternative — rolling everyone out, swapping the secret, rolling back in — is acceptable for short outage windows but is what rotation is supposed to avoid. A future wave will add a multi-key verifier so notify can hold both keys natively.
What if my JWT is RS256?
v0.1 only validates HS256. If your identity provider signs RS256 (any standard OIDC IdP exposing a JWKS endpoint), the recommended shape today is:
- Run a thin reverse proxy / gateway in front of notify that verifies the RS256 token against JWKS, then re-mints an HS256 token for notify's downstream consumption.
- Or run notify in library mode with your own validator wired in via
server.Dependencies.AuthValidator.
Native RS256 + JWKS verification is on the roadmap; track notify issues.
Related
- Auth Model — the full validation flow
- Configuration — every
NOTIFY_AUTH_*var