Authenticate callers
Require authentication for a worker served over HTTP, and read the caller’s identity inside your functions.
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.
| Transport | Auth behaviour |
|---|---|
HTTP, with SetAuthenticate | Validated per request; the caller’s *vgirpc.AuthContext reaches your functions |
| HTTP, without it | Every request anonymous |
| stdio / Unix socket | Always anonymous (co-located, trusted) |
| TCP | Always anonymous — and not trusted. Raw Arrow IPC carries no auth and no encryption. |
Wire it up
Section titled “Wire it up”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
}))
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.
Read the caller in your functions
Section titled “Read the caller in your functions”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.
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:
| Constructor | Use when |
|---|---|
MtlsAuthenticate | The worker terminates TLS itself |
MtlsAuthenticateXfcc | A proxy terminates TLS and forwards the x-forwarded-client-cert header |
MtlsAuthenticateFingerprint | Identity is a pinned certificate fingerprint |
MtlsAuthenticateSubject | Identity is the certificate subject |
Each returns (AuthenticateFunc, error) — the error is configuration validation, so check it at
startup rather than at request time.
OAuth and browser login
Section titled “OAuth and browser login”Two more setters, both HTTP-only:
SetOAuthResourceMetadatapublishes RFC 9728 Protected Resource Metadata at a well-known endpoint and adds aWWW-Authenticateheader to 401 responses, so a client can discover which authorization server to talk to. SetResource,AuthorizationServers, and — for clients that need it — theClientIDextension.SetOAuthPkceenables the browser-based PKCE login flow: the worker serves/_oauth/callback,/_oauth/logoutand a token-exchange proxy, and redirects unauthenticated browserGETs to the authorization server.
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.
Next steps
Section titled “Next steps”- Stand up the HTTP service first → Serve over HTTP.
- Returning the rejection correctly → Report errors well.
- The transport’s own auth model → vgi-rpc.