sign

command
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 31, 2026 License: MIT Imports: 19 Imported by: 0

README

examples/sign

Sign-as-API worked example for plan §2 (subscription-signer collapse). Demonstrates the post-§2.3 model: the gateway is a pure signer, a downstream service holds the signer secret, does its own authz, and calls the gateway over gRPC to mint subscription tokens.

What runs

One binary, three things in-process:

Component Listens Role
Gateway GraphQL :8080 Public surface (queries / mutations / subscriptions).
Control-plane gRPC :50090 SignSubscriptionToken lives here. Bearer-gated.
Auth-shim HTTP :8090 Receives client subscribe-token requests, does business authz, calls back into the gateway over gRPC with the signer-secret bearer.

The shim is the part you'd write in your own backend. The gateway is the library — it never sees your user identities or entitlements.

Run

$ go run ./examples/sign
control plane listening on 127.0.0.1:50090
graphql listening on 127.0.0.1:8080
auth-shim listening on 127.0.0.1:8090

If the default ports collide, override with --graphql, --control-plane, --shim.

Try it

Alice signing her own channel works:

$ curl -sS -X POST http://localhost:8090/subscribe-token \
    -H 'Authorization: Bearer demo-user-alice' \
    -H 'Content-Type: application/json' \
    -d '{"channel":"events.user.alice.likes","ttl_seconds":60}'
{"hmac":"…","timestamp":1778012345,"channel":"events.user.alice.likes","kid":""}

Alice trying to sign bob's channel is rejected by the shim, not the gateway:

$ curl -sS -X POST http://localhost:8090/subscribe-token \
    -H 'Authorization: Bearer demo-user-alice' \
    -d '{"channel":"events.user.bob.likes"}'
{"error":"forbidden"}

A request with no shim bearer is rejected by the shim before any sign call goes out:

$ curl -sS -X POST http://localhost:8090/subscribe-token \
    -d '{"channel":"events.user.alice"}'
{"error":"unauthenticated"}

Bypassing the shim and hitting SignSubscriptionToken directly is gated by the gateway:

$ gwag sign --gateway 127.0.0.1:50090 \
           --channel events.user.alice
--bearer is required with --gateway (the sign endpoint is bearer-gated)

$ gwag sign --gateway 127.0.0.1:50090 \
           --bearer 11111111111111111111111111111111 \
           --channel events.user.alice
hmac=…
ts=1778012345

Why this shape

Inverts the earlier "authorizer delegate" model where the gateway called back out at sign time. That forced the gateway to predict what context the authorizer needed (user ID? IP? scope? custom claims?) and bake it into a delegate proto. The signer-as-API model puts the authz decision in the service that already has full request context — composition over prediction.

Two bearers, two roles:

  • Shim's inbound bearer (Bearer demo-user-<id>) — authenticates the user to the shim. Real services use JWTs, sessions, etc.
  • Gateway's inbound bearer (Bearer <signer-secret>) — authenticates the service to the gateway. "This service speaks for me; sign what it asks." The gateway has zero opinion about the end user.

The signer secret is rotatable independently of the gateway's admin/boot token (plan §2.1) — leak the signer secret, rotate it, admin token unaffected.

What's intentionally toy

  • allowSubscribe is a one-line prefix check (alice can sign events.user.alice.*). Real services consult their entitlement model.
  • userFromBearer accepts any string after Bearer demo-user-. Real services validate signed tokens.
  • Demo secrets are hardcoded hex (11…, 22…). Real deployments use --signer-secret $(openssl rand -hex 32) or equivalent and store the value out-of-band.

Documentation

Overview

Sign-as-API worked example. Demonstrates the post-§2.3 model: the gateway is a pure signer (no pull-delegate), a downstream service holds the signer secret, does its own authz, and calls the gateway over gRPC to mint subscription tokens.

Three things run in-process so this stays one binary:

  • gateway — embedded NATS, control-plane gRPC at :50090, GraphQL at :8080. Configured with a signer secret; the bearer gate fires for any wire call to SignSubscriptionToken.
  • control-plane gRPC server on :50090 — the calling service uses this exact wire path, presenting the signer secret as bearer.
  • auth-shim — HTTP service on :8090. Receives client subscribe-token requests, does business authz (see allowSubscribe below), then signs via the gateway. Returns hmac+ts to the client.

Run:

go run ./examples/sign

Then:

$ curl -sS -X POST http://localhost:8090/subscribe-token \
    -H 'Authorization: Bearer demo-user-alice' \
    -H 'Content-Type: application/json' \
    -d '{"channel": "events.user.alice", "ttl_seconds": 60}'
{"hmac":"…","timestamp":1778012345,"channel":"events.user.alice","kid":""}

$ curl -sS -X POST http://localhost:8090/subscribe-token \
    -H 'Authorization: Bearer demo-user-alice' \
    -d '{"channel": "events.user.bob"}'   # alice can't sign bob's
{"error":"forbidden"}

The gateway never sees the user identity — that's the auth-shim's job. The gateway just trusts the signer-secret bearer ("this service speaks for me"); the service has the request context to make the per-user decision.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL