Skip to content

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.

Terminal window
go get jiayang.cloud/sdk
import jiayang "jiayang.cloud/sdk"

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.

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.

// chi
r.Use(jiayang.Middleware)
r.With(jiayang.Requires(jiayang.RoleEditor)).Post("/orders", placeOrder)
// echo
e.Use(echo.WrapMiddleware(jiayang.Middleware))
// gin, whose middleware has a shape of its own
r.Use(func(c *gin.Context) {
user, err := jiayang.RequireUser(c.Request)
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
c.Set("user", user)
})

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.

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.

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.

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.

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.

Terminal window
jiayang dev