Skip to content

Auth and OIDC

In dev mode, Quayside mints JWTs via a dedicated endpoint that anyone can call. In production, you replace this with a real OIDC integration.

What v1 ships

The minimum auth surface needed to run the proxy end-to-end:

  • POST /api/v1/auth/dev/mint-token — takes (principal_id, class_slug, tenant) and returns a signed JWT. Gated by QUAYSIDE_DEV_MODE=true.
  • Proxy validates the JWT on every call. Issuer, expiry, signature, required claims all enforced.

This is enough for developer laptops, scripts, and CI. It’s not enough for production — anyone who can hit the mint endpoint can mint any token they want.

What you need for production

Two changes from the dev-mode shape:

  1. Disable the dev mint endpoint by setting QUAYSIDE_DEV_MODE=false. The endpoint returns 403; the proxy’s X-Quayside-* header fallback also turns off.
  2. Mint JWTs via your own authenticated flow, then drop them into x-api-key as qsk_<JWT>.

The proxy doesn’t care where the JWT came from. It cares that:

  • the signature verifies against QUAYSIDE_JWT_SECRET
  • iss is quayside
  • it’s not expired
  • the required claims (sub, class_slug, tenant) are present

Two production patterns

Run a small auth service alongside Quayside that:

  1. Authenticates the user via OAuth/PKCE against your IdP (Entra, Okta, Keycloak, etc.)
  2. After OIDC returns, mints a Quayside JWT using the same QUAYSIDE_JWT_SECRET
  3. Returns the qsk_<JWT> to the caller

The flow looks like this:

Engineer's CLI / dashboard
│ OAuth/PKCE redirect
Your IdP (Entra / Okta / ...)
│ authorization code
Quayside auth sidecar
│ validates OIDC ID token, mints Quayside JWT
Engineer's machine ← qsk_<JWT> cached
│ x-api-key: qsk_<JWT>
Quayside proxy → LLM provider

The proxy stays simple. The OIDC integration is a separate concern in a separate process.

B. Your IdP mints directly

If your IdP can sign JWTs with a shared key (or via JWKS that quayside trusts), you can skip the sidecar:

  1. Configure your IdP to mint JWTs with the right shape (iss, sub, class_slug, tenant, exp)
  2. Configure Quayside to trust your IdP’s signing key (HS256 with shared secret today; RS256 + JWKS is a roadmap item)
  3. The IdP’s normal token-issuance flow drops a Quayside-shaped token into the user’s hands

Trade-off: more coupling to your IdP, fewer moving parts.

How class_slug gets into the JWT

This is the design question for any production auth integration: how does the user end up with a JWT that authorises them to call as one specific class?

Some patterns we’ve seen:

  • One class per user role — your IdP maps the user’s group membership to a class slug. engineering group ⇒ eng/dev-tools. Simplest.
  • Class picker at login — the user picks “I’m calling as X” from a list of classes they’re allowed to use. More flexible, more UX.
  • Per-process tokens — each agent process has its own service-account token with a baked-in class slug. Useful for batch jobs.

Quayside doesn’t dictate this. Pick the pattern that fits your IdP’s primitives.

What the proxy validates

Once x-api-key: qsk_<JWT> arrives:

  1. Strip qsk_ prefix
  2. Decode JWT, verify signature against QUAYSIDE_JWT_SECRET
  3. Verify iss == "quayside"
  4. Verify not expired
  5. Verify required claims (sub, class_slug, tenant) are present
  6. Resolve class_slug against the registry (404 + shadow log if unknown)
  7. Auto-claim instance for (class, sub)

The auth integration’s only job is to put a properly-signed JWT in the right shape into the user’s hands. Quayside does the rest.

What’s on the roadmap

  • RS256 + JWKS — public/private key pairs with key rotation
  • Multi-tenant signing keys — different signing keys per tenant
  • Token revocation — a deny-list mechanism for the rare case
  • Service-account class types — distinct from human-owned classes

For v1, plain HS256 + the dev mint endpoint cover the test-and-pilot case. Most customers will want to replace the mint endpoint before going to production.