VGI supports bearer token, JWT/JWKS, and RFC 9728 OAuth resource metadata for HTTP transport. Authentication is fully optional — when unconfigured, all requests are anonymous.
The simplest auth setup uses static bearer tokens via environment variable:
VGI_BEARER_TOKENS="token1=alice,token2=bob" vgi-serve my_worker.py --httpEach entry is split on the first =, so principals may contain = (e.g.
base64 values). However, tokens must not contain = or , because
those characters are used as delimiters.
Unauthenticated requests receive HTTP 401. Authenticated requests include
the principal in the AuthContext available to functions.
| Variable | Description |
|---|---|
VGI_BEARER_TOKENS |
Comma-separated token=principal pairs for static bearer auth |
VGI_JWT_ISSUER |
JWT issuer URL (requires vgi-python[oauth] extra) |
VGI_JWT_AUDIENCE |
JWT audience string, comma-separated for multiple audiences (required when VGI_JWT_ISSUER is set) |
VGI_JWT_JWKS_URI |
JWKS endpoint URL (auto-discovered from issuer if omitted) |
VGI_OAUTH_RESOURCE |
OAuth resource URL for RFC 9728 metadata |
VGI_OAUTH_AUTH_SERVERS |
Comma-separated authorization server URLs |
VGI_OAUTH_SCOPES |
Comma-separated supported scopes (optional) |
VGI_OAUTH_RESOURCE_NAME |
Human-readable resource name (optional) |
VGI_OAUTH_CLIENT_ID |
Client ID for MCP compatibility (optional, URL-safe chars only) |
VGI_OAUTH_CLIENT_SECRET |
Client secret for OAuth (optional, URL-safe chars only) |
VGI_OAUTH_USE_ID_TOKEN |
When 1/true/yes, clients use OIDC id_token as Bearer instead of access_token |
When both VGI_BEARER_TOKENS and VGI_JWT_ISSUER are set, they are
chained — JWT validation is attempted first, falling back to bearer token
lookup.
vgi_rpc.Identity.v1's issue_grant mints a standing credential that
unattended automation later presents as an ordinary bearer. Over HTTP the
worker accepts two kinds of identity credential after the deployment's own
authenticator:
- Sealed grants (opt-in). Set
VGI_RPC_GRANT_KEYS(orvgi-serve --http --grant-key KEY, repeatable).issue_grantthen mints avgig1.grant sealed with the first key, unless the worker overridesmint_grant. Every key verifies, so to rotate you add the new key first and drop the old one once its grants have expired. A request carryingAuthorization: Bearer vgig1.…is authenticated as the grant's owner, withdomain="grant"and claimsgrant_id,scopesandpurpose. The claims carry noauth_time, so a grant cannot mint another grant. resolve_token. A worker that overrides [Worker.resolve_token][vgi.worker.Worker.resolve_token] has it consulted for any other bearer, withdomain="token".Nonemeans the credential is unknown, which is a 401 unless something else accepts it.AuthUnavailableErroris a 503 with the hook'sRetry-After.
The order is the deployment's authenticator (JWT, static bearer), then sealed
grants, then resolve_token. A vgig1. token that does not verify is a 401
that stops the chain: it never reaches resolve_token. A JWS-shaped token or
one over 4096 bytes never reaches the hook either. Your own authenticate
callback must raise ValueError for a credential it does not recognise, so the
chain moves on. One that answers anonymous for every request ends the chain
before grants are checked. With no authenticator configured, a request with no
Authorization header stays anonymous as before.
| Variable | Description |
|---|---|
VGI_RPC_GRANT_KEYS |
Comma-separated standard-base64 keys, 32 bytes each. The first mints and all verify. Unset means grants are off. A malformed key stops startup |
VGI_RPC_GRANT_AUDIENCE |
Bound into every grant, so two deployments that share a key still reject each other's grants |
VGI_RPC_GRANT_MAX_TTL_SECONDS |
Ceiling on a grant's lifetime (default 7 days) |
The startup rules are unchanged. Overriding resolve_token still requires
VGI_INTROSPECT_PRINCIPALS. Grant keys alone need no allowlist, because
minting answers only for the caller. Grants and resolve_token bearers are
HTTP-only, like the rest of Identity. When VGI_PROXY_PROOF_MODE is on, both
are accepted only inside the proxy-proof gate, never as an alternative to it.
from vgi.serve import create_app, load_worker_class
from vgi.auth import bearer_authenticate_static, OAuthResourceMetadata
from vgi_rpc.rpc import AuthContext
# Static bearer tokens
authenticate = bearer_authenticate_static(tokens={
"secret-token-1": AuthContext(principal="alice", authenticated=True, domain="bearer"),
"secret-token-2": AuthContext(principal="bob", authenticated=True, domain="bearer"),
})
app = create_app(
load_worker_class("my_worker:MyWorker"),
authenticate=authenticate,
oauth_resource_metadata=OAuthResourceMetadata(
resource="https://api.example.com",
authorization_servers=("https://auth.example.com",),
client_id="my-client-id",
use_id_token_as_bearer=True, # use OIDC id_token as Bearer
),
)When authenticate is passed programmatically, environment variables are
ignored.
Use Auth with Annotated on a compute() parameter:
from typing import Annotated
import pyarrow as pa
from vgi import ScalarFunction, Param, Returns, Auth
from vgi.auth import AuthContext
class WhoAmI(ScalarFunction):
class Meta:
name = "whoami"
@classmethod
def compute(
cls,
x: Annotated[pa.Int64Array, Param(doc="dummy input")],
auth: Annotated[AuthContext, Auth()],
) -> Annotated[pa.StringArray, Returns()]:
name = auth.principal or "anonymous"
return pa.array([name] * len(x))Table functions access auth via params.auth_context:
from vgi.table_function import TableFunctionGenerator, ProcessParams
class SecureTable(TableFunctionGenerator):
@classmethod
def process(cls, params, state, out):
if not params.auth_context.authenticated:
raise PermissionError("Authentication required")
# ... produce output ...Auth is also available during on_bind() via params.auth_context on
both BindParams (table functions) and BindParameters (scalar functions).
| Transport | Auth Behavior |
|---|---|
HTTP with authenticate |
Auth validated per-request, AuthContext propagated |
HTTP without authenticate |
All requests anonymous |
| Stdio (subprocess) | Always AuthContext.anonymous() |
from vgi_rpc.rpc import AuthContext
# Check authentication
ctx = AuthContext(principal="alice", authenticated=True, domain="bearer")
assert ctx.authenticated is True
assert ctx.principal == "alice"
assert ctx.domain == "bearer"
# Anonymous context
anon = AuthContext.anonymous()
assert anon.authenticated is False
assert anon.principal is None
# Require authentication (raises PermissionError if not authenticated)
ctx.require_authenticated() # OK
anon.require_authenticated() # raises PermissionError