Bearer tokens and OAuth, built into ATTACH.
DuckDB itself has no concept of who's asking — the closest thing was a static secret for reaching S3 or an API. VGI adds real identity: Google, Microsoft Entra ID, Auth0, or any OAuth 2.0 provider you already run, with PKCE, device-code, and refresh-token handling built into the extension itself — not a library you integrate per worker.
Where this starts
DuckDB doesn't have a concept of identity
DuckDB has no built-in concept of identity — it's one process, running with whatever credentials it's given. The closest thing before VGI was the Secrets Manager: one S3 key or API token, applied uniformly, because the process was reaching a resource on its own account, not acting on behalf of a person.
A VGI worker is often not a bucket — it's a real product with real customers, or an internal system with real employees, and the data behind it is usually entitled: not everyone should see everything. That's an authentication problem, and building an identity provider is out of scope for a database. So VGI doesn't — it forwards tokens from providers that already exist (Google, Microsoft Entra ID, Auth0, any OAuth 2.0 / OIDC provider) straight to the worker, which validates them.
Each attached catalog has its own identity —
there's no single VGI “identity” shared across all of them. It's scoped at the
same granularity as ATTACH itself: one catalog can be
authenticated as you against Entra ID, the next might carry a static bearer token, another
might need no auth at all.
The mechanism
A token in the ATTACH, or nothing at all
A worker that requires credentials takes them the same way any other worker option
does — as part of ATTACH. A static bearer_token
goes straight onto every request. An oauth_refresh_token you
already have lets VGI mint access tokens itself, silently, for as long as that refresh
token stays valid. The two are mutually exclusive — set both and DuckDB refuses to attach.
Leave both out against a worker that requires OAuth, and VGI runs a full interactive flow
the first time a query needs it.
-- A static token, no flow at all
ATTACH 'crm' (TYPE vgi, LOCATION 'https://crm.example.com', bearer_token 'sk_live_...');
-- A refresh token you already have — VGI exchanges it for access tokens itself
ATTACH 'crm' (TYPE vgi, LOCATION 'https://crm.example.com', oauth_refresh_token '...');
-- Neither — VGI runs a full interactive OAuth flow on first query
ATTACH 'crm' (TYPE vgi, LOCATION 'https://crm.example.com');
Either value is redacted from AttachInfo and from telemetry —
it never appears in a query plan, an error message, or a log line. Tokens themselves stay
in memory only, scoped to that one catalog for that one DuckDB process; nothing is written
to disk, and it isn't the DuckDB Secrets Manager doing the holding.
No flag to remember
OAuth, picked automatically
When a worker needs interactive OAuth, VGI decides which flow to run rather than making
you choose one. If the worker's advertised metadata has no
authorization_endpoint at all, it goes straight to device-code —
there's nothing a browser-based flow could do there anyway. Otherwise it detects whether
it's running somewhere a browser can plausibly pop up: a CI runner, an SSH session, a
Docker container, a Colab notebook, or any environment with no DISPLAY
all route to device-code automatically. Everywhere else, it tries PKCE first — and falls
back to device-code on its own if it can't bind a local callback port.
That decision is overridable, not fixed — vgi_oauth_flow forces
one path, and vgi_oauth_timeout_seconds (120s by default) extends
how long an interactive flow is allowed to wait for you.
-- Force device-code even on a machine with a browser
SET vgi_oauth_flow = 'device_code';
-- Give an interactive flow longer than the 120s default to complete
SET vgi_oauth_timeout_seconds = 300;
When there's a browser
The PKCE flow
The default path on a desktop machine. DuckDB proves it's the same client that started the flow by generating a one-time secret and only ever sending its hash — so even the local callback server that receives the redirect can't be tricked into handing tokens to a different process.
-
1
ATTACH starts a local callback server
DuckDB opens a short-lived listener on 127.0.0.1 and generates a PKCE code_verifier / code_challenge pair (SHA-256, via OpenSSL).
-
2
Your system browser opens
The authorization URL carries code_challenge_method=S256 — nothing secret is in it, so it’s safe to have crossed the network.
-
3
You log in and approve
Authentication happens entirely on the identity provider’s own page. VGI never sees your password.
-
4
The provider redirects back with a code
The redirect lands on the local callback server, carrying a one-time authorization code.
-
5
DuckDB exchanges the code for tokens
The code plus the original code_verifier go to the token endpoint together — the verifier proves it’s the same client that started the flow.
-
6
Query runs, authenticated
The local server shuts down and the access token attaches to every request on that catalog.
When there isn't
The device-code flow
RFC 8628, in full — not a stripped-down version. Nothing about it needs a browser or an open port on the machine running DuckDB, which is exactly why it's the automatic choice over SSH, in CI, or inside a container.
-
1
ATTACH requests a device code
DuckDB asks the authorization server for a device_code and a short user_code — no browser or listener needed on this machine at all.
-
2
A code and URL are printed
The user_code and a verification URL show up wherever DuckDB is running — a terminal over SSH, a CI job log, a headless box with no display.
-
3
You approve on any other device
Open the URL on your phone or laptop, enter the code, log in. The machine running DuckDB never needs a browser of its own.
-
4
DuckDB polls in the background
The client polls the token endpoint at the interval the server sets, honoring authorization_pending and slow_down rather than hammering it.
-
5
Once you approve, tokens arrive
The next poll returns real tokens instead of authorization_pending, and polling stops immediately.
-
6
Query runs, authenticated
Same as the PKCE path from here — the access token attaches to every request on that catalog.
After the first login
Staying signed in
Refresh isn't proactive — VGI doesn't track an access token's expiry and race to beat it. It's reactive: the worker returns a 401, and only then does VGI reach for the refresh token. That keeps the common case (a token that's still valid) doing no extra work at all.
-
1
A request comes back 401
The worker rejects an expired access token on an ordinary query — nothing about the flow up to this point changes.
-
2
The stored refresh token is used
DuckDB exchanges it for a new access token via a grant_type=refresh_token request — no browser, no re-login.
-
3
The original request retries automatically
The query that triggered the 401 runs again with the new token, invisibly, from the caller’s side.
-
4
Concurrent queries wait, not race
If several queries hit 401 at once, only the first triggers a refresh; the rest block on it and reuse the result instead of each starting their own.
If the refresh itself fails for a reason other than an expired or revoked grant, VGI falls back to a full interactive flow rather than failing the query outright — the same PKCE or device-code choice described above, run again from scratch.
In the browser
The same flows, inside WebAssembly
Everywhere on this site where you can run a query directly in the browser — the
10-second tour,
the extension docs' “Try it in your browser” buttons — that's the real VGI extension
compiled to WASM, and it handles OAuth there too, just not by redirecting the page. A
popup opens instead, the extension's Worker thread blocks on it via a
SharedArrayBuffer until the popup reports back a code, and the
redirect URI is a static page on this site (/oauth-callback.html)
rather than anything VGI-specific. To avoid ever holding a
client_secret in browser JavaScript, the WASM build prefers a
server-advertised PKCE token-exchange proxy for the final code-for-token exchange when the
provider offers one.
What's actually connected
Checking what's connected
Two table functions expose live auth state without leaving SQL.
vgi_oauth_tokens() reports, per attached catalog, whether a
token is active, expired, or absent, how long until it expires, and whether a refresh
token is on file. vgi_oauth_identity() goes further and parses
the OIDC claims out of the current access token — subject, email, name, issuer — useful
for confirming who a worker thinks you are, not just whether you're connected.
Those claims come from the token as presented — TLS-trusted, not independently
re-verified against the identity provider on every call.
SELECT * FROM vgi_oauth_tokens();
-- catalog_name | origin | status | expires_in | has_refresh_token
SELECT * FROM vgi_oauth_identity();
-- catalog_name | origin | authenticated | sub | email | name | issuer | claims_json
Keep going