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 Sub string // "usr_…", or "service:<token id>" Email string // empty for a bypass token Role string // "viewer", "editor" or "owner" WorkspaceID string}See the SDK overview for what each field means and what the SDK checks.
json.Marshal(user) writes kind, sub, email, role and workspace_id, the names Python and Rust use. A bypass token’s email is null.
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, not a person’s. 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. The platform sets 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))verifier.Verify(token) checks a token you already have, the same way, and verifier.VerifyWebhook(token, jiayang.ProviderStripe) a webhook’s.
What a refused request gets
Section titled “What a refused request gets”| 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: 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: its audience is the other kind’s.
Run it locally
Section titled “Run it locally”jiayang dev runs your app behind the platform’s front door on your machine, with real tokens. With a main.go in the project it starts it with go run ., and otherwise you name the command: jiayang dev -- go run ./cmd/server. See Local development.
jiayang dev