on GitHub

Authentication

A client authenticates once, at the WebSocket upgrade, with a bearer token. The question is where that token comes from. Putting it in the ATTACH statement works and is the worst option — it lands in your SQL history, your logs, and anything that echoes the statement back.

Secrets

DuckDB has a secret manager. Store the token there and the ATTACH carries no credential at all:

CREATE SECRET n6kdemo (
  TYPE n6k,
  TOKEN 'demo-token',
  SCOPE 'n6kd-demo-106595059298.us-central1.run.app'
);

ATTACH 'n6ks://n6kd-demo-106595059298.us-central1.run.app' AS demo (TYPE n6k);

duckdb_secrets() redacts the value, so the token does not reappear in introspection.

Scope

SCOPE is matched as a literal prefix of the URL with the scheme stripped (host[:port][/prefix]):

SCOPEMatches
'data.example'that host, any port, any path
'data.example:443'that host on 443 only
'data.example:443/reports'one mount point on that host
omittedany host — a catch-all

Because the scheme is stripped on both sides, a bare hostname matches whether you attached with n6k://, n6ks://, or host/port options.

Rotation

Replace the secret; the next attach and every subsequent request use the new value. Nothing is re-declared and nothing is restarted:

CREATE OR REPLACE SECRET n6kdemo (
  TYPE n6k, TOKEN 'the-new-token', SCOPE 'data.example'
);

Precedence

  1. An inline token '…' option on the ATTACH
  2. A matching TYPE n6k secret
  3. A TYPE n6k_refresh secret, which mints one via OAuth (native only)
  4. Otherwise no header is sent — the connection is anonymous

Anonymous is a real mode, not just a failure state. A local or test server that needs no auth is attached with:

ATTACH 'n6k://localhost:8080' AS dev (TYPE n6k, anonymous true);

anonymous true skips steps 2 and 3 outright, which also stops a stale n6k_refresh secret from breaking an attach that never needed credentials — an unreachable issuer would otherwise fail the connect during OAuth discovery. It is mutually exclusive with an inline token.

OAuth

For deployments with a real identity provider, the client can hold a durable credential and mint short-lived data-service JWTs on demand. There are no n6k-specific endpoints involved; the authorization server only has to implement the standards:

RFCUsed for
8414Discovery (/.well-known/oauth-authorization-server)
8628Device login
6749Token endpoint
8693Token exchange — durable subject token → data-service JWT
7519 / 7517 / 7518 / 8037JWT, published key set, EdDSA

n6k_login runs the device flow, prints a verification URL, waits for you to approve it, and stores the resulting n6k_refresh secret:

SELECT * FROM n6k_login('https://auth.example', resource := 'data-service');
ATTACH 'n6ks://data.example' AS db (TYPE n6k);   -- mints and renews automatically

From then on, when a request needs a token and none is cached — or the cached JWT is within about a minute of expiry — the extension performs discovery, does an RFC 8693 exchange, and caches the minted JWT as a temporary secret. The exchange is single-flight per host, so a burst of queries triggers one mint.

Native only

n6k_login and the n6k_refresh mint are native-only and throw in the browser, where the host application owns the OAuth lifecycle and supplies the access secret directly.

What auth does not do

Auth answers who is connecting. It does not answer what they may touch — that is the server's permission layer, and it is a separate file with a separate lifecycle. A valid token still gets Table 'audit_log' is read-only if the catalog says so. See permissions.

One more limit worth knowing: the token is validated once, at the upgrade handshake, and never re-checked on a live connection. Revoking a token stops the next attach, not the session already running.