Plugin Store · Identity provider
Developers self-serve a budgeted key via SSO.
Overview
The OIDC auth method turns your identity provider into the way developers get a busbar key. busbar hosts token-exchange itself at one fixed core route: GET /auth/token serves an SSO login page in the browser, and POST /auth/token takes an OIDC token the caller already holds. Either way the dev gets back their own key: self-scoped to user:<sub>, its budget auto-provisioned from the team’s child_default on first exchange, the secret shown on sign-in and again any time you sign in.
You don’t configure a URL for the exchange: set busbar’s own public_url and it builds /auth/token itself (that’s also the base URL a dev points their AI tool at). Every token is verified against the issuer’s live JWKS (the method never trusts an unsigned claim), and the configured role claim (e.g. groups) drives policy.
Define the provider once under the top-level identity-providers: map (module: oidc plus its own settings:) and busbar exposes the exchange automatically. A per-provider browser_login block is the switch for the hosted sign-in page: include it (it carries the IdP client secret) and that method gets a button on the login page; omit it and the provider is POST-only, for devs who bring their own token. Define a second provider with its own browser_login and the page becomes a chooser (Microsoft / GitHub). role_bindings.oidc.<group> maps an SSO group to a busbar team, and therefore to that team’s budget and pools.
It’s one key per person, not a key list: the key is deterministic, derived from the signed-in identity under auth.signing_key. Re-login just shows a dev their same key again, next to a Refresh button that rotates it (the old key dies, the new one takes its place, itself re-showable on every later sign-in). How long an issued key stays valid is auth.key_ttl (default 90d): an admin-set config value, sibling of signing_key, never a per-dev choice. See How token-exchange works for the shared token system behind this.
One method, any issuer: the only thing that changes between providers is issuer (busbar discovers each one’s login + JWKS endpoints from it). See the provider table below for the exact issuer URL per IdP.
OIDC isn’t only for minting keys. Set auth.chain: [oidc] instead of [keys] and busbar verifies an OIDC token on every request straight against the data plane (no token-exchange, no issued key at all): the same per-request verification busbar 1.5.1 already shipped.
Supported providers
Only issuer changes between providers; busbar discovers each one’s login + JWKS endpoints from it.
| Provider | Issuer URL | Notes |
|---|---|---|
| Microsoft Entra ID | https://login.microsoftonline.com/<tenant-id>/v2.0 | Set role_claim: roles to use app-roles, which sidesteps Entra’s >200-group token overage. |
| Okta | https://<org>.okta.com | Or …/oauth2/<server> for a custom authorization server. |
| Auth0 | https://<tenant>.us.auth0.com/ | — |
| Keycloak | https://<host>/realms/<realm> | — |
https://accounts.google.com | Set scopes: [openid, email, profile]. No groups claim by default: bind by email/hosted-domain or wire Workspace directory groups. |
Install it
The OIDC provider ships as a signed kind: auth plugin. Let busbar auto-download it on boot: add it to plugins.fetch (verified by sha256 + ed25519 signature + allowlist before load), set your public_url, and define the provider:
public_url: "https://busbar.example.com" # busbar builds /auth/token itself
plugins:
enabled: true
fetch:
- github: GetBusbar/auth-oidc@1.0.2 # auto-downloaded, version-pinned
identity-providers:
oidc:
module: oidc
settings:
issuer: "https://login.microsoftonline.com/<tenant-id>/v2.0"
audience: "<entra-client-id>" # also the default client_id
browser_login: # present → hosted login button on
client_secret: { env: OIDC_CLIENT_SECRET }
auth:
signing_key: { file: /run/secrets/busbar-signing.key }
chain: [keys] # devs' issued keys authenticate here
key_ttl: 90d # admin-set; default 90d, no dev choice
role_bindings:
oidc:
"<entra-dev-group>": { group: engineering } New in busbar 1.5.2 (token-exchange). The plugin is independently versioned from busbar itself; pin both explicitly in production (the @1.0.2 in fetch is the pin).
Prefer to install by hand? No plugins.fetch config needed: download the signed tarball (links above), drop it in your plugins.dir, and busbar loads it at boot; signature and trust checks still apply. You can also push it live over the admin API. See the plugin install docs.
Settings
| Setting | Required | Default | Description |
|---|---|---|---|
issuer | Yes | — | OIDC issuer URL; a token’s iss claim must match it exactly. busbar discovers the login and JWKS endpoints from it. |
audience | Yes | — | Expected aud (your Entra app/client id); a token’s audience must match exactly. Also the default client_id for browser login. |
key_ttl | No | 90d | How long a key issued via token-exchange stays valid, set on auth.key_ttl (sibling of auth.signing_key); admin-set only, not a per-method or per-dev choice. |
role_claim | No | groups | Token claim mapped onto the principal’s groups; set roles to use Entra app-roles. |
browser_login | No | — | Presence is the switch for the hosted sign-in page: include it and this method gets a button on GET /auth/token; omit it and the method is POST-only (the dev brings their own token). |
browser_login.client_secret | No | — | Required when browser_login is set: the IdP app’s client secret, as a secret reference ({ env: } / { file: }), never a plaintext literal. |
browser_login.client_id | No | audience | Only if it differs from audience (with Entra they’re the same). |
jwks_url | No | discovered | JWKS endpoint. When omitted, resolved from the issuer’s /.well-known/openid-configuration. |
ca_cert_pem | No | — | Additional trusted root CA (PEM) for an internal-CA issuer; layered on the public roots, never disables validation. |
Configuration scenarios
When to use: A developer with just a browser should be able to get a key. busbar hosts the SSO sign-in page and hands back the dev’s own budgeted key, ideal for Cursor / Claude Code / VSCode BYOK, where the tool just needs a static key and a base URL.
public_url: "https://busbar.example.com" # busbar builds /auth/token itself
plugins:
enabled: true
fetch:
- github: GetBusbar/auth-oidc@1.0.2
identity-providers:
oidc:
module: oidc
settings:
issuer: "https://login.microsoftonline.com/<tenant-id>/v2.0"
audience: "<entra-client-id>"
browser_login: # presence = hosted login button on
client_secret: { env: OIDC_CLIENT_SECRET }
auth:
signing_key: { file: /run/secrets/busbar-signing.key }
chain: [keys] # the issued key authenticates here
key_ttl: 90d # admin-set; default 90d, no dev choice
role_bindings:
oidc:
"<entra-dev-group>": { group: engineering } A dev opens GET /auth/token (busbar builds the URL from public_url), signs in with Entra, and busbar returns their own static key: self-scoped user:<sub>, budget auto-provisioned from the team’s child_default on first login, the secret shown on sign-in and again any time you sign in. Add a second method with its own browser_login and the page becomes a chooser (Microsoft / GitHub).
When to use: Headless or CI: the caller already holds an OIDC token (an IdP CLI, an internal app, <code>az account get-access-token</code>). No hosted page, no client secret, just the token exchange.
public_url: "https://busbar.example.com"
plugins:
enabled: true
fetch:
- github: GetBusbar/auth-oidc@1.0.2
identity-providers:
oidc: # no browser_login → POST-only
module: oidc
settings:
issuer: "https://login.microsoftonline.com/<tenant-id>/v2.0"
audience: "<entra-client-id>"
auth:
signing_key: { file: /run/secrets/busbar-signing.key }
chain: [keys]
key_ttl: 90d # admin-set; default 90d, no dev choice
role_bindings:
oidc:
"<entra-dev-group>": { group: engineering } Omit browser_login and the method is token-only: a dev (or CI) POSTs an OIDC token they already hold to /auth/token and gets back the same self-scoped, budgeted key, created on first exchange, with the secret shown then and re-shown on every later exchange.
When to use: No key at all: every request itself must carry a valid OIDC token, checked live against the issuer on the data plane. This is the existing 1.5.1 behavior: still valid, and stackable alongside the two token-exchange scenarios above.
identity-providers:
oidc:
module: oidc
settings:
issuer: "https://login.microsoftonline.com/<tenant-id>/v2.0"
audience: "<entra-client-id>"
auth:
chain: [oidc] # every request verified against the issuer, no issued key
role_bindings:
oidc:
"<entra-dev-group>": { group: engineering }
plugins:
enabled: true
fetch:
- github: GetBusbar/auth-oidc@1.0.2 No public_url, no browser_login, no key_ttl; nothing is issued or exchanged. Each caller presents its own OIDC token on every request; busbar verifies it live against the issuer’s JWKS and maps its groups claim through role_bindings.oidc the same way. Use this when callers already hold short-lived tokens and a long-lived busbar key isn’t wanted.
When to use: Not tied to one IdP: the same method fronts any OIDC-compliant issuer. You change one thing (<code>issuer</code>) and busbar discovers that provider’s login + JWKS endpoints from it. The sign-in button’s label and icon are inferred from the issuer host, so there’s no label to configure.
public_url: "https://busbar.example.com" # busbar builds /auth/token itself
plugins:
enabled: true
fetch:
- github: GetBusbar/auth-oidc@1.0.2
identity-providers:
oidc: # your choice of name; module: oidc backs it
module: oidc
settings:
issuer: "https://accounts.google.com" # the only line that changes per provider
audience: "<google-client-id>"
browser_login: # button label + icon inferred from issuer
client_secret: { env: GOOGLE_SECRET }
auth:
signing_key: { file: /run/secrets/busbar-signing.key }
chain: [keys] # the issued key authenticates here
key_ttl: 90d # admin-set; default 90d, no dev choice
role_bindings:
oidc:
"<workspace-group>": { group: engineering } # Google: wire Workspace groups (see note) The identity-provider name (here oidc) is your choice, not the module name: identity-providers: entries are name → {module, …}, and auth.chain / role_bindings reference that name. Two named providers can share one module, so running more than one OIDC issuer at once (say oidc-google and oidc-okta, each module: oidc) is supported; the single-instance example above is the common case, not a limit. Only issuer changes between providers, and busbar discovers each one’s login + JWKS from it; the sign-in button is inferred from the issuer host, nothing to set. Okta: https://<org>.okta.com. Auth0: https://<tenant>.us.auth0.com/. Keycloak: https://<host>/realms/<realm>. Google (shown) puts no groups claim in the token by default; bind by email/hosted-domain or wire Workspace directory groups before group-based role_bindings will match.
When to use: The management plane can take OIDC too, not just the operator token; a token is a token on both planes (new in 1.5.2). Let an SSO admin group sign in to run mutations, while the same method still mints budgeted keys for devs.
public_url: "https://busbar.example.com"
plugins:
enabled: true
fetch:
- github: GetBusbar/auth-oidc@1.0.2
identity-providers:
oidc:
module: oidc
settings:
issuer: "https://login.microsoftonline.com/<tenant-id>/v2.0"
audience: "<entra-client-id>"
browser_login:
client_secret: { env: OIDC_CLIENT_SECRET }
auth:
signing_key: { file: /run/secrets/busbar-signing.key }
chain: [keys] # data plane: devs' issued keys
admin_auth: [admin-tokens, oidc] # admin plane: operator token OR OIDC
key_ttl: 90d # admin-set; default 90d, no dev choice
role_bindings:
oidc:
"<entra-dev-group>": { group: engineering } # data-plane budget
"<entra-admin-group>": { admin_scope: full } # admin authority: read-only | full Admin scopes are exactly two after the 1.5.2 collapse: read-only (every read) and full (every mutation). An SSO admin group maps to one of them via role_bindings.oidc.<group>.admin_scope, so admin_auth: [admin-tokens, oidc] lets the operator token or an OIDC sign-in onto the admin plane. The one method does both jobs at once: a dev group gets a data-plane budget ({ group: … }), an admin group gets admin authority ({ admin_scope: … }). For an untrusted third-party identity provider, cap what it can ever grant with an optional max_admin_scope on its identity-providers: definition (omitted = the most restrictive default, read-only; use none for an external IdP that should never reach the admin plane).