Authentication
IdPlane OIDC for federated apps calling Control Plane APIs via auth.groundfloor.cloud.
Audience: Shell host and federated remote developers
IdP: IdPlane at auth.groundfloor.cloud
Control Plane validates IdPlane-issued access tokens on workspace-scoped APIs. Federated bootstrap (by-slug) is public and needs no token.
Two auth planes (do not mix them)
| Plane | Who | Issuer | Used for |
|---|---|---|---|
| Platform (Control Plane) | Portal admins, operators, devs building apps | IdPlane platform realm | /v1/workspaces/… pillar APIs |
| Workspace site (optional) | End users of a customer app | Per-workspace OIDC issuer | Shell on {slug}.app.groundfloor.cloud |
Data APIs (secrets, vault, files, LLM) require a platform IdPlane bearer token unless workspace site auth is enabled for that workspace.
IdPlane configuration
Control Plane API
Configure Control Plane to trust IdPlane's issuer and accepted OIDC client ids (audience / authorized party). The issuer URL must match the token iss claim (no trailing slash).
Production IdPlane host: https://auth.groundfloor.cloud
Browser apps (Shell host, Portal, federated remote)
Point your OIDC public client (PKCE, no client secret) at IdPlane. Register each front-end client id with Control Plane so its tokens are accepted.
Obtaining an access token
In the app
Prefer Groundfloor's auth helper (or your Shell host auth context) rather than embedding raw OIDC wiring in every remote:
// Pseudocode — use the Shell / Portal auth helper
const token = await getAccessToken(); // refreshes before expiry
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/v1/workspaces/${workspaceId}/secrets`,
{ headers: { Authorization: `Bearer ${token}` } },
);Using @groundfloor/api-client
import {
configureControlPlaneClient,
setControlPlaneAuthProvider,
} from "@groundfloor/api-client";
configureControlPlaneClient({ baseURL: process.env.NEXT_PUBLIC_API_URL });
setControlPlaneAuthProvider(async () => getAccessToken());CLI
gf login # browser sign-in via IdPlane (PKCE)
export TOKEN="$(gf token)"See Developers.
Programmatic / CLI access
The Control Plane does not issue login tokens. Obtain access tokens from IdPlane's OIDC token endpoint (discoverable from the issuer's OpenID configuration) and pass them as Authorization: Bearer … on every API call.
Option A — User token via refresh (recommended for scripts)
- Sign in once in the Portal or Shell (authorization code + PKCE).
- Capture the refresh token from your IdPlane session.
- Exchange it for a fresh access token at IdPlane's token endpoint (
grant_type=refresh_token).
On first successful API call, Portal JIT-provisions the user row from JWT claims (sub, email).
Option B — Client credentials (automation / CI)
Create a confidential IdPlane client with a service account. Add its client id to Control Plane's accepted audiences. The service account still needs workspace membership and ReBAC roles.
Option C — Direct grant (dev only)
Password grant may be available on some IdPlane clients in non-production cells. Prefer gf login or refresh tokens.
Requirements for any token
| Check | Detail |
|---|---|
| Issuer | Token iss must match Control Plane's configured IdPlane issuer |
| Audience | Token aud or azp must be an accepted client id |
| Membership | User (or service account) needs workspace membership for scoped routes |
| Expiry | Refresh before expiry; access tokens are short-lived |
Federated remote inside Shell
When the remote runs inside the Shell host:
- Preferred: Shell exposes the platform token to remotes (React context, shared auth module). Call
setControlPlaneAuthProvideror setAuthorizationbefore pillar API calls. - Alternative (dev only): Remote runs its own IdPlane public client against the same realm — heavier, duplicates login.
Do not embed long-lived API keys in the federated bundle. Use IdPlane session tokens for user-scoped calls; store service keys in Secrets and reveal them server-side only.
Permissions (ReBAC)
Control Plane checks SpiceDB permissions on workspace-scoped routes:
| Permission | Typical use |
|---|---|
read | List secrets (keys only), vault query, files list/download, LLM models/usage |
write | Upsert secrets, file upload, vault create/update |
delete | Delete secrets, files, vault records |
administer | Mint LLM virtual key, workspace auth config, DDL |
A 403 means the user is authenticated but lacks the relation on that workspace.
Workspace site auth (opt-in)
When workspace site auth is enabled (GET/PUT /v1/workspaces/{id}/auth), Control Plane also accepts tokens from the workspace OIDC issuer on workspace APIs. Platform IdPlane tokens are tried first (Portal UI path).
Public auth discovery (for Shell login redirects):
GET /v1/public/workspaces/by-host?host={hostname}Enable Groundfloor-managed mode (requires administer):
PUT /v1/workspaces/{workspace_id}/auth
Content-Type: application/json
Authorization: Bearer {platform_token}
{ "mode": "groundfloor" }Shell should OIDC-login against the returned workspace issuer / client_id, not the platform Portal client. Until provision completes, poll GET /v1/workspaces/{id}/auth.
What not to do
| Anti-pattern | Why |
|---|---|
| Forward browser JWT to Dataplane | Use Control Plane /vault proxy; service keys are server-side |
| Hardcode Dataplane keys in front-end | Tenant isolation break |
Call GET …/secrets/{key} from untrusted browser code | Reveal is audited; values belong on server or trusted BFF |
| Skip token refresh | IdPlane access tokens expire; refresh before API calls |
Local dev checklist
- IdPlane client exists for your Shell app (
auth.groundfloor.cloud) - Client id accepted by Control Plane
- User is a member of the workspace
-
NEXT_PUBLIC_API_URLpoints at Control Plane - CORS includes your Shell dev origin
See Local dev recipes for curl examples with a token.