Go
The jiayang.cloud/sdk module verifies the identity token the edge sends your app and gives you the caller. Its middleware is a plain func(http.Handler) http.Handler, so it works with net/http and most routers.
go get jiayang.cloud/sdkimport jiayang "jiayang.cloud/sdk"The middleware
Section titled “The middleware”jiayang.Middleware answers 401 without a valid caller. Otherwise it puts the caller in the request’s context, and jiayang.UserFrom reads it back.
http.Handle("/", jiayang.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user, _ := jiayang.UserFrom(r.Context()) fmt.Fprintf(w, "hello %s\n", user.Email)})))UserFrom returns false as its second value when the middleware didn’t run on that request.
The caller is a jiayang.User:
type User struct { Kind string // "user", or "service" for a bypass token or an agent Sub string // "usr_…", "service:<token id>", or "agent:<agent id>" Email string // empty for a bypass token or an agent Role string // "viewer", "editor" or "owner" WorkspaceID string AgentName string // the agent's name when an agent called, empty for everyone else}user.IsAgent() says whether one of the workspace’s agents made the request. It and AgentName are new in v0.2.0: with an earlier version, go get jiayang.cloud/sdk@latest.
by := user.Emailif user.IsAgent() { by = user.AgentName + " (agent)"}See the SDK overview for what each field means and what the SDK checks.
json.Marshal(user) writes kind, sub, email, role, workspace_id and agent_name, the names Python and Rust use. A bypass token’s email is null, and so is the agent name of anyone but an agent.
In a handler
Section titled “In a handler”Call jiayang.RequireUser where you’d rather check inside the handler:
user, err := jiayang.RequireUser(r)if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return}Every refusal wraps jiayang.ErrUnauthorized, so errors.Is(err, jiayang.ErrUnauthorized) is true for all of them.
jiayang.Requires is the middleware for a route only some callers may reach. It answers 401 without a caller, and 403 when the caller has less than the role it names.
mux.Handle("POST /orders", jiayang.Requires(jiayang.RoleEditor)(http.HandlerFunc(placeOrder)))In a handler:
if err := jiayang.RequireRole(user, jiayang.RoleEditor); err != nil { http.Error(w, "forbidden", http.StatusForbidden) // errors.Is(err, jiayang.ErrForbidden) return}
if jiayang.HasRole(user, jiayang.RoleOwner) { // show the settings link}The roles are jiayang.RoleViewer, jiayang.RoleEditor and jiayang.RoleOwner.
Your router
Section titled “Your router”// chir.Use(jiayang.Middleware)r.With(jiayang.Requires(jiayang.RoleEditor)).Post("/orders", placeOrder)
// echoe.Use(echo.WrapMiddleware(jiayang.Middleware))
// gin, whose middleware has a shape of its ownr.Use(func(c *gin.Context) { user, err := jiayang.RequireUser(c.Request) if err != nil { c.AbortWithStatus(http.StatusUnauthorized) return } c.Set("user", user)})Webhooks
Section titled “Webhooks”A delivery on a verified public path arrives with a token of kind webhook. RequireWebhook checks it and says which provider signed the delivery. RequireUser refuses a webhook’s token, and RequireWebhook refuses a person’s.
mux := http.NewServeMux()// Outside Middleware, which would refuse the delivery before this handler saw it.mux.Handle("POST /hooks/stripe", jiayang.WebhookMiddleware(jiayang.ProviderStripe)(http.HandlerFunc(stripeHook)))mux.Handle("/", jiayang.Middleware(app))
func stripeHook(w http.ResponseWriter, r *http.Request) { hook, _ := jiayang.WebhookFrom(r.Context()) if handled(hook.Delivery) { // Stripe's event id w.WriteHeader(http.StatusNoContent) return } // decode r.Body as usual w.WriteHeader(http.StatusNoContent)}Or call hook, err := jiayang.RequireWebhook(r, jiayang.ProviderStripe) in a handler. With chi, give the webhook’s route its own middleware rather than the router’s Use:
r.Group(func(r chi.Router) { r.Use(jiayang.Middleware) r.Get("/", index)})r.With(jiayang.WebhookMiddleware(jiayang.ProviderStripe)).Post("/hooks/stripe", stripeHook)WebhookMiddleware answers 401 unauthorized to anything but a verified delivery from the providers it names, and otherwise puts the delivery in the request’s context for WebhookFrom. WebhookFrom returns false as its second value when the middleware didn’t run on that request.
Name the providers the route takes: ProviderStripe, ProviderGitHub, ProviderSlack, ProviderShopify, ProviderStandardWebhooks or ProviderHMACSHA256. A delivery from any other is refused, and so is every delivery when none is named. The delivery is a jiayang.Webhook:
type Webhook struct { Provider Provider // ProviderStripe and the rest Pattern string // the public path it came in on, like "/hooks/stripe" Delivery string // the provider's signed id, empty where it signs none SignedAt time.Time // zero where the provider signs no time WorkspaceID string}As JSON a delivery is provider, pattern, delivery, signed_at and workspace_id, the names Python and Rust use. signed_at is unix seconds, and an id or time the provider doesn’t sign is null.
The body arrives byte for byte as the provider sent it, so decode r.Body as usual.
Configuration
Section titled “Configuration”The package-level functions (Middleware, Requires, RequireUser, RequireWebhook and WebhookMiddleware) read JIAYANG_APP_ID, JIAYANG_IDENTITY_ISSUER and JIAYANG_JWKS_URL from the environment the first time they run, and keep what they found for the life of the process. We set all three.
To pass the values yourself, build a Verifier. It has the same methods, and is safe to share between goroutines.
verifier, err := jiayang.NewVerifier(jiayang.Config{ AppID: os.Getenv("JIAYANG_APP_ID"), Issuer: "https://edge.jiayang.cloud", JWKSURL: "https://edge.jiayang.cloud/.well-known/jwks.json",})if err != nil { log.Fatal(err) // a field was empty}
http.Handle("/", verifier.Middleware(handler))Config also has Dev, whether JIAYANG_DEV is 1. ConfigFromEnv reads it. Only jiayang dev sets it, and without it NewVerifier refuses an http issuer or key set.
verifier.Verify(token) checks a token you already have, the same way, and verifier.VerifyWebhook(token, jiayang.ProviderStripe) a webhook’s.
Refused requests
Section titled “Refused requests”| Status | Body | When |
|---|---|---|
| 401 | unauthorized |
No valid caller |
| 403 | forbidden: this needs editor |
Requires(RoleEditor) and the caller has less |
Both come with Cache-Control: no-store.
Errors
Section titled “Errors”The error says why, for your logs:
err.Error() |
Why |
|---|---|
unauthorized: no identity token |
The request has no X-Jiayang-Identity header |
unauthorized: invalid identity token |
The token failed a check, or the keys couldn’t be fetched |
unauthorized: JIAYANG_APP_ID, JIAYANG_IDENTITY_ISSUER and JIAYANG_JWKS_URL must be set |
The variables weren’t in the environment |
unauthorized: JIAYANG_IDENTITY_ISSUER and JIAYANG_JWKS_URL must use https, except under jiayang dev (JIAYANG_DEV=1, on this machine) |
The issuer or key set is http, and JIAYANG_DEV isn’t 1. Only jiayang dev uses http |
unauthorized: not a webhook this route takes |
From RequireWebhook: a provider the route doesn’t name signed the delivery |
unauthorized: invalid webhook token |
From RequireWebhook: the token’s webhook claims failed a check |
unauthorized: no provider named |
From RequireWebhook: the call named no provider, so it refuses everything |
forbidden: this needs editor |
From RequireRole(user, jiayang.RoleEditor) |
A person’s token given to RequireWebhook, or a webhook’s given to RequireUser, fails as invalid identity token, because its audience is wrong for that call.
Run it locally
Section titled “Run it locally”jiayang dev runs your app on your machine behind a copy of our front door, with real tokens. With a main.go in the project it runs go run .. Otherwise, give the command, as in jiayang dev -- go run ./cmd/server. See Local development.
jiayang devNext steps
Section titled “Next steps”- SDK overview: every claim in the token, and each check.
- Local development.
- Webhooks and public paths.