Skip to content

Go

The forculuslabs.tech/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 forculuslabs.tech/sdk
import forculus "forculuslabs.tech/sdk"

forculus.Middleware answers 401 without a valid caller. Otherwise it puts the caller in the request’s context, and forculus.UserFrom reads it back.

http.Handle("/", forculus.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, _ := forculus.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 forculus.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 forculuslabs.tech/sdk@latest.

by := user.Email
if 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.

Call forculus.RequireUser where you’d rather check inside the handler:

user, err := forculus.RequireUser(r)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}

Every refusal wraps forculus.ErrUnauthorized, so errors.Is(err, forculus.ErrUnauthorized) is true for all of them.

forculus.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", forculus.Requires(forculus.RoleEditor)(http.HandlerFunc(placeOrder)))

In a handler:

if err := forculus.RequireRole(user, forculus.RoleEditor); err != nil {
http.Error(w, "forbidden", http.StatusForbidden) // errors.Is(err, forculus.ErrForbidden)
return
}
if forculus.HasRole(user, forculus.RoleOwner) {
// show the settings link
}

The roles are forculus.RoleViewer, forculus.RoleEditor and forculus.RoleOwner.

// chi
r.Use(forculus.Middleware)
r.With(forculus.Requires(forculus.RoleEditor)).Post("/orders", placeOrder)
// echo
e.Use(echo.WrapMiddleware(forculus.Middleware))
// gin, whose middleware has a shape of its own
r.Use(func(c *gin.Context) {
user, err := forculus.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. 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", forculus.WebhookMiddleware(forculus.ProviderStripe)(http.HandlerFunc(stripeHook)))
mux.Handle("/", forculus.Middleware(app))
func stripeHook(w http.ResponseWriter, r *http.Request) {
hook, _ := forculus.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 := forculus.RequireWebhook(r, forculus.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(forculus.Middleware)
r.Get("/", index)
})
r.With(forculus.WebhookMiddleware(forculus.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 forculus.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 FORCULUS_APP_ID, FORCULUS_IDENTITY_ISSUER and FORCULUS_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 := forculus.NewVerifier(forculus.Config{
AppID: os.Getenv("FORCULUS_APP_ID"),
Issuer: "https://edge.forculuslabs.tech",
JWKSURL: "https://edge.forculuslabs.tech/.well-known/jwks.json",
})
if err != nil {
log.Fatal(err) // a field was empty
}
http.Handle("/", verifier.Middleware(handler))

Config also has Dev, whether FORCULUS_DEV is 1. ConfigFromEnv reads it. Only forculus 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, forculus.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-Forculus-Identity header
unauthorized: invalid identity token The token failed a check, or the keys couldn’t be fetched
unauthorized: FORCULUS_APP_ID, FORCULUS_IDENTITY_ISSUER and FORCULUS_JWKS_URL must be set The variables weren’t in the environment
unauthorized: FORCULUS_IDENTITY_ISSUER and FORCULUS_JWKS_URL must use https, except under forculus dev (FORCULUS_DEV=1, on this machine) The issuer or key set is http, and FORCULUS_DEV isn’t 1. Only forculus 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, forculus.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.

forculus 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 forculus dev -- go run ./cmd/server. See Local development.

Terminal window
forculus dev