Skip to content
Authentication

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.

1. Logs in 2. Issues token 3. Query + token 4. Validates token OAuth Provider User DuckDB Client VGI Worker

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.

attach-with-auth.sql
-- 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.

oauth-settings.sql
-- 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. 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. 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. 3

    You log in and approve

    Authentication happens entirely on the identity provider’s own page. VGI never sees your password.

  4. 4

    The provider redirects back with a code

    The redirect lands on the local callback server, carrying a one-time authorization code.

  5. 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. 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. 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. 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. 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. 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. 5

    Once you approve, tokens arrive

    The next poll returns real tokens instead of authorization_pending, and polling stops immediately.

  6. 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. 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. 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. 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. 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.

check-auth-state.sql
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

Where to next