Skip to main content

Auth & onboarding

Credentials live in environment variables, never in the page. This guide covers how the try-it console authenticates, and how you hand a ready-made environment to a teammate with a setup link.

The environment, first

An environment is a name, a base URL, variables and default headers added to every request — plus a color from a closed palette, shown on the selector: production in red is visible before the send, not after. The selector also carries a badge when credential variables the schema expects are missing, and on a schema’s first load a button offers to create one environment per servers entry — seeding on request, never automatic. Environments declared in the config are added by name on load, without overwriting anything the reader already has; from then on, the reader’s own storage is authoritative.

What the schema declares, the console understands

Every scheme in your securitySchemes gets a working input: bearer, basic, apiKey in a header, a query parameter or a cookie, OAuth2 and OpenID Connect. The convention is one variable per scheme: scheme X reads auth.X. Set it in the active environment and every operation secured by that scheme sends it — leave it empty and the {{var}} interpolation blocks the send rather than leaking a literal {{auth.X}} to your API.

Sensitive variables are masked in the UI, with an honest caveat: values live in the browser’s local storage, unencrypted. Treat a shared machine accordingly.

OAuth2 without a backend

ApiGlow runs Authorization Code + PKCE entirely in the browser: full-page redirect to your identity provider, code exchange in the page, token into auth.X — no server of ours, no client secret in the config, ever. Client credentials is supported too for machine-style APIs. The demo’s OAuth flows point at a mock provider; against your API you supply your real authorization and token URLs.

The one thing the config carries is the public client id, per scheme — never a secret — and the reader can override it with the auth.X.clientId variable:

{ "oauth": { "petstore_auth": { "clientId": "public-client-id" } } }

The drivable flows are exactly those two — Authorization Code + PKCE and client credentials. implicit and password are documented from the schema but carry no “Get a token” button.

What a browser cannot do, said where it matters

Two schemes are rendered with a stated platform limit instead of a broken button, in three places each — the operation doc, the auth summary and the try-it credentials block:

  • mutualTLS is documented but never sent: “a browser cannot present a client certificate — call this endpoint from a client that can (cURL, your own code).”
  • The OAuth2 device-authorization flow (OpenAPI 3.2) is not run: “it polls the token endpoint outside the browser — obtain the token elsewhere and paste it into the variable.”

Both limits are the platform’s, not the schema’s: the documentation stays complete, the console just refuses to pretend.

When the host page already has a session

The docs page is static — no server renders it, so nothing can inject a per-user token at delivery time. But the reader often already has a session (cookie, SSO) in that browser, and your backend could mint an API token for them on demand. The host credentials bridge closes that gap: the page that embeds ApiGlow registers a provider, and the app asks it for credentials when it needs them.

<!-- after the ApiGlow script tag -->
<script type="module">
  apidoc.registerCredentialsProvider(async ({ specId, reason }) => {
    const r = await fetch('/api/docs-token', { credentials: 'include' })
    if (!r.ok) return null
    return { bearerAuth: (await r.json()).access_token }
  })
</script>

The returned map is keyed by scheme names from your securitySchemes: a string fills auth.X, an object fills auth.X.username-style suffixes. The reader lands on docs where the credential badge already reads “from host” — nothing to paste, nothing to configure.

Four rules make it safe to reason about:

  • It fills only the void. A host value applies only where the selected environment is empty; a value the reader types (or an OAuth flow writes) always wins, and clearing it falls back to the host value.
  • Memory only, ever. Host values are never persisted — not in storage, not in history exports (they are captured as sensitive, so redaction applies), not in environment exports. Reload the page, the provider is simply asked again.
  • One retry on 401. If a request whose credentials came from the host gets a 401, the app asks the provider once more (reason: "expired") and replays once — with a visible note in the response panel, never a silent loop.
  • The trust boundary is the page. Only host-page code can register a provider; nothing in an OpenAPI document can trigger or influence one, and the app itself never fetches a credentials URL.

Two more members cover the trivial cases — apidoc.setCredentials(map) pushes values the host already holds, apidoc.clearCredentials() empties the bridge on host logout — and a classic script that may run before the app listens for the apidoc:ready event instead of assuming the global. In multi-spec, one provider serves every spec: it receives the active specId and routes internally.

There is deliberately no config key for any of this: the bridge is a runtime surface, so a token can never end up written in a public page. The provider’s own fetch typically authenticates with the session cookie — credentials: "include" and the CORS constraints that come with it are yours to meet, same as any cookie-authenticated call.

The environment manager carries a band of its own — Set a teammate up — with two ways in. The first shares the environment you have: you pick what travels, checked variables carry their value, unchecked ones travel by name with an empty value — a skeleton your teammate fills in with their own credentials. Non-sensitive rows start checked, sensitive ones start unchecked.

If you do include a credential, the dialog says it plainly, in place rather than behind a confirm: whoever holds the link holds that credential, and a link outlives the conversation that carried it. Very long links get a warning too, past 2,000 characters — some chat clients truncate silently, which is the worst thing a link can do.

Building one without creating the environment

Sharing starts from an environment you already own — which means creating it locally first, with values you may not want to keep on your machine. The second entry point in that band, Build from scratch, removes the requirement: a form that describes the environment the team should have, and produces the link. It is also offered as a card on the welcome view, which is where a lead actually lands.

The form is a pure generator: it writes nothing, ever. Building a link is not creating an environment; the dialog has no path to your storage at all. Fill in a name, a base URL, a color, variable rows and default-header rows — each variable row carrying a sensitive flag and a send the value checkbox, unchecked by default for sensitive rows, so a credential travels only when you say so.

The link rebuilds as you type, and the bounds are checked live, naming the one that failed rather than handing you a link the recipient would refuse. Preview as recipient closes the builder and opens the link right there — not a rendering of the landing dialog, the landing dialog, so the preview cannot drift from what your teammate sees. Cancel writes nothing; Apply is a real apply, and the button says so. On a multi-spec install the link targets the spec you are on.

Here is the same idea, running on this page rather than in the app — a real setup link, in the exact format ApiGlow reads, pointed at the demo:

BUILD A SETUP LINK · TRY IT ON THE DEMO

Unchecked: auth.api_key travels by name with an empty value — the skeleton your teammate fills in.

Open the link: you are now on the receiving end, and nothing is written until you agree. The link opens a review table — each variable marked created, updated or kept, sensitive values hidden, empty ones flagged “you fill this in” — and the link is immediately stripped from the address bar. Three guards refuse quietly wrong situations: an unreadable link changes nothing, a link built for another spec changes nothing, and on an environmentsLocked install a setup link cannot change environments at all.

Neither form holds any magic, and you do not need one to produce a link: it is your docs URL followed by a setup pseudo-parameter in the hash, carrying base64url-encoded JSON. That is what makes onboarding links something a CI job or an intranet page can generate for a whole team.

https://docs.example.com/#/?setup=<base64url(payload)>

The payload, version 1:

{
  "v": 1,
  "spec": "petstore",
  "env": {
    "name": "Staging",
    "baseUrl": "https://staging.petstore.example",
    "vars": [
      ["auth.api_key", "", true],
      ["team", "docs-squad"]
    ],
    "headers": [["X-Team", "docs"]]
  }
}

What you need to know to write one yourself:

  • Each variable is a row [name, value] or [name, value, true] — the third slot marks it sensitive (the recipient’s field is masked from the moment it is created). The flag travels even when the value does not.
  • An empty value is the skeleton: the variable is created by name and the recipient fills it in.
  • spec is optional but recommended on a multi-spec install: without it there is no wrong-API guard.
  • baseUrl, color and headers are optional.
  • The encoding is unpadded base64url (+-, /_, = stripped). In Node: Buffer.from(JSON.stringify(payload)).toString("base64url").
  • Bounds enforced on read: 8 KB of decoded JSON, 50 variables, 20 headers, 200 characters per name, 4 KB per value — past any of them the whole link is refused as unreadable, and a name used twice refuses it too. And past ~2,000 URL characters, beware chat clients that truncate silently.

The generated first call

features: { "onboarding": true } adds a “First call” page at the top of the reference nav (#/first-call), for the reader who has never sent anything: ApiGlow picks the simplest read the schema declares — a GET, no body, nothing left to type once the declared examples are pre-filled — and shows it under a three-step preamble: pick a language, enter credentials, press Send. The steps happen in the ordinary try-it rail, so the reader ends up exactly where they will work. Off by default, and absent anyway when the schema declares no such read.

Locked deployments

environmentsLocked is for controlled corporate installs: environments come from the site’s configuration and readers cannot edit them — which also disables incoming setup links. See the configuration reference for the key, and Try-it & CORS for why a correctly authenticated call can still be blocked by the browser.