Skip to content
Query.Farm
Talk with Us

Authenticate callers

Require authentication for a worker served over HTTP, and read the caller’s identity inside your functions.

Authentication is an HTTP-only concern

Auth applies only to the HTTP transport. A stdio or Unix-socket worker is launched by the engine and runs co-located and trusted — there is no remote caller to authenticate — so it always sees vgirpc.Anonymous(). Reach for this page once you serve over HTTP.

TransportAuth behaviour
HTTP, with SetAuthenticateValidated per request; the caller’s *vgirpc.AuthContext reaches your functions
HTTP, without itEvery request anonymous
stdio / Unix socketAlways anonymous (co-located, trusted)
TCPAlways anonymous — and not trusted. Raw Arrow IPC carries no auth and no encryption.

One method, taking a vgirpc.AuthenticateFunc — a plain func(*http.Request) (*vgirpc.AuthContext, error):

w := vgi.NewWorker(vgi.WithCatalogName("acme"))
w.RegisterScalar(NewDouble())

w.SetAuthenticate(vgirpc.BearerAuthenticateStatic(map[string]*vgirpc.AuthContext{
  os.Getenv("ACME_TOKEN"): {Domain: "bearer", Authenticated: true, Principal: "alice"},
}))

if err := w.RunHttp("127.0.0.1:8080"); err != nil {
  log.Fatal(err)
}

BearerAuthenticateStatic compares tokens with subtle.ConstantTimeCompare against every entry and deliberately does not short-circuit on a match, so response timing does not leak which bytes of a guessed token were right. Prefer it to a map lookup of your own.

For anything else, BearerAuthenticate(validate) hands you the raw token and leaves validation to you — a database lookup, an introspection call, whatever your scheme needs:

w.SetAuthenticate(vgirpc.BearerAuthenticate(func(token string) (*vgirpc.AuthContext, error) {
  principal, err := lookUpToken(token)
  if err != nil {
      return nil, &vgirpc.RpcError{Type: "ValueError", Message: "Unknown bearer token"}
  }
  return &vgirpc.AuthContext{Domain: "bearer", Authenticated: true, Principal: principal}, nil
}))
How a rejection becomes a 401

Return an *vgirpc.RpcError with Type "ValueError" or "PermissionError" and the request gets HTTP 401. Any other error is a 500. So reach for RpcError when the caller is wrong, and a plain error when you are — a JWKS endpoint that won’t answer is a 500, not a 401.

ChainAuthenticate(a, b, …) tries each in order. A ValueError falls through to the next; a PermissionError or a non-RpcError stops the chain immediately. That asymmetry is the useful part: “this isn’t my scheme” keeps going, “this is my scheme and you failed it” does not.

The resolved context is on both BindParams.Auth and ProcessParams.Auth:

func (*WhoAmIFn) Process(_ context.Context, params *vgi.ProcessParams,
  state *whoState, out *vgirpc.OutputCollector,
) error {
  if err := params.Auth.RequireAuthenticated(); err != nil {
      return err        // already a PermissionError — return it unwrapped
  }
  ...
}

RequireAuthenticated returns a PermissionError RpcError when the caller is anonymous. Return it as-is — wrapping it with %w loses the type.

AuthContext carries Domain (the scheme), Authenticated, Principal, and a Claims map[string]any for anything your validator extracted from the token.

Check at bind, not just at process

BindParams.Auth means you can reject an unauthorised call once, before any data moves, rather than per batch. Bind is also where a claims-based check belongs — deciding which rows a principal may see is a filter you apply while scanning, but deciding whether they may call the function at all is a bind-time question.

For client-certificate auth, vgirpc builds the AuthenticateFunc for you — pick the variant that matches how the certificate reaches the process:

ConstructorUse when
MtlsAuthenticateThe worker terminates TLS itself
MtlsAuthenticateXfccA proxy terminates TLS and forwards the x-forwarded-client-cert header
MtlsAuthenticateFingerprintIdentity is a pinned certificate fingerprint
MtlsAuthenticateSubjectIdentity is the certificate subject

Each returns (AuthenticateFunc, error) — the error is configuration validation, so check it at startup rather than at request time.

Two more setters, both HTTP-only:

  • SetOAuthResourceMetadata publishes RFC 9728 Protected Resource Metadata at a well-known endpoint and adds a WWW-Authenticate header to 401 responses, so a client can discover which authorization server to talk to. Set Resource, AuthorizationServers, and — for clients that need it — the ClientID extension.
  • SetOAuthPkce enables the browser-based PKCE login flow: the worker serves /_oauth/callback, /_oauth/logout and a token-exchange proxy, and redirects unauthenticated browser GETs to the authorization server.
PKCE needs the other two, and says so

RunHttp refuses to start unless SetAuthenticate is set, SetOAuthResourceMetadata is set, and that metadata carries both a ClientID and at least one AuthorizationServers entry — you get oauth pkce: vgirpc: SetOAuthPkce requires … naming the missing piece. The order you call the three setters in doesn’t matter; the worker applies them in dependency order at startup.

CookieAuthenticate(inner, cookieName) wraps an authenticator so a browser session cookie is accepted alongside the Authorization header — that is what makes the PKCE flow usable from a browser and from a CLI at the same time.