App sign-in library
Check app sign-in tokens in a Node.js backend and look up a standalone app's sign-in settings in the browser.
Coming soon on npm
@groundfloorcloud/app-auth isn't published on npm yet, so npm install doesn't find it. Until it is, check tokens as shown in Who can reach your Service. App sign-in itself is rolling out.
@groundfloorcloud/app-auth checks, in your backend, that a request comes from a user signed in to your app, and gives your front end its standalone app's sign-in settings. It's for your app's users, not your team.
npm install @groundfloorcloud/app-authIt's open source under the MIT licence. It needs Node.js 20.19 or later and works with ESM and CommonJS. Its one dependency is jose; the browser entry, @groundfloorcloud/app-auth/browser, doesn't include it.
Check tokens in your backend
import { requireAppUser, verifyAppToken, AppAuthError } from "@groundfloorcloud/app-auth";
app.use("/api", requireAppUser()); // 401 without a valid app token
app.post("/api/posts", requireAppUser({ roles: ["editor"] }), handler); // 403 without the role
try {
const user = await verifyAppToken(token);
} catch (err) {
if (err instanceof AppAuthError) res.status(err.status).json({ error: err.code });
}| Function | What it does |
|---|---|
requireAppUser(options?) | Express-style middleware. Reads Authorization: Bearer <token>, checks it, and puts the user on res.locals.appUser and req.appUser. Answers 401 with {"error": "<code>"} and WWW-Authenticate: Bearer, or 403 for a missing role or company |
createAppTokenVerifier(options?) | Returns a function that checks a token and resolves to the user. Reads its settings once, so a missing setting fails when your server starts |
verifyAppToken(token, options?) | Checks one token in a single call |
verifyShellToken(token, options?), createShellTokenVerifier(options?) | The same for a Shell app's backend, which gets the Shell site token (azp shell) with Groundfloor sign-in. Every check applies except the audience. Pass issuer. See Every app on your Shell site can use the user's token |
hasAnyRole(user, roles) | true if the user has at least one of the roles |
bearerToken(header) | The token from an Authorization header, or null |
readRoles(claims), readOrganization(claims) | Read roles, and companies as { aliases, ids }, from a token's claims |
An app token is accepted only if it's signed with RS256 by a key from the JWKS URL, iss equals the issuer exactly, aud contains the app's client ID, azp equals it (a token without azp is refused), exp hasn't passed (with 30 seconds of clock leeway by default, at most 60; nbf and iat, when present, get the same leeway), typ (when present) is Bearer, sub is present, and there's no x-client-key or x-userid claim. Those claims mark Groundfloor's own Coderunner management and developer tokens, which are refused with wrong_token_type. The keys are cached for 10 minutes and fetched again, at most every 30 seconds, when a token names a key the cache doesn't have.
The user
| Field | What it is |
|---|---|
sub | The user's ID. Store and match their records by it, not by email |
roles | The user's roles, without offline_access, uma_authorization and default-roles-*. A malformed claim reads as no roles |
organization | The aliases of the user's companies (tenants), with Multi-tenant on; otherwise empty |
organizationIds | The companies' IDs, when the token has them |
email | The user's email address, or null |
clientId | The client the token was issued to (azp) |
claims | Every claim in the token |
Any user of the workspace can sign in to any of its apps, so always check roles. With Multi-tenant on, a role applies in every company the user belongs to: filter reads and writes by organization too, or use requireOrganization.
Options
Options you pass win over environment variables.
| Option | Default |
|---|---|
issuer | GROUNDFLOOR_AUTH_ISSUER |
audience | GROUNDFLOOR_AUTH_AUDIENCE (the app's client ID, app-<app-id>) |
jwksUrl | GROUNDFLOOR_AUTH_JWKS_URL, else {issuer}/protocol/openid-connect/certs. https only, or http on localhost |
requireOrganization | false. true needs any company; a tenant's alias or ID needs that company. Otherwise 403 organization_required |
clockToleranceSeconds | 30. Any value from 0 to 60; more than 60 is refused |
jwksCooldownMs, jwksCacheMaxAgeMs, jwksTimeoutMs | 30000, 600000, 5000 |
env | process.env |
requireAppUser also takes roles (the user needs at least one), shell: true to check Shell site tokens instead, and onError(err, req) for logging. Never log the token itself.
Environment variables
Groundfloor sets these on a standalone app's running main Service when it deploys, unless you set them yourself, while app sign-in is switched on. They're your backend's runtime environment, not variables in your front end's build. See Find your issuer and client ID.
| Variable | Value |
|---|---|
GROUNDFLOOR_AUTH_ISSUER | The issuer of the app's tokens |
GROUNDFLOOR_AUTH_JWKS_URL | {issuer}/protocol/openid-connect/certs |
GROUNDFLOOR_AUTH_AUDIENCE | app-<app-id> |
GROUNDFLOOR_AUTH_CLIENT_ID | app-<app-id>, for the front end's sign-in |
GROUNDFLOOR_APP_ID | The app's ID |
Errors
AppAuthError has a code and the HTTP status to answer with:
| Status | code |
|---|---|
401 | missing_token, invalid_token, token_expired, wrong_issuer, wrong_audience, wrong_client, wrong_token_type, missing_subject |
403 | role_required, organization_required |
500 | config: a setting is missing or wrong in your backend |
503 | jwks_unavailable: the signing keys couldn't be fetched |
Get sign-in settings in the browser
import { getAppAuthConfig } from "@groundfloorcloud/app-auth/browser";
const cfg = await getAppAuthConfig({ controlPlaneUrl: "https://platform.groundfloor.cloud" });
// { issuer, clientId, realm, redirectUri: "https://<this host>/auth/callback" }, or nullgetAppAuthConfig asks Groundfloor for the sign-in settings of the standalone app on the page's own host, and resolves to null when that host has no app sign-in. Pass controlPlaneUrl, for example from your build config. The lookup accepts calls from any web address, so it works on your custom domains. Hand the result to an OpenID Connect library with PKCE; see Sign users in from the browser.
| Option | Default |
|---|---|
controlPlaneUrl | Groundfloor's main API address. Pass https://platform.groundfloor.cloud so it matches the cell your workspace is on |
host | The page's host (location.hostname). On a local dev server, pass the app's real host; redirectUri is then https://<host>/auth/callback, so use your dev server's own callback address instead |
callbackPath | /auth/callback |
fetch, signal | The browser's fetch, no abort signal |
getAppToken() is for Shell apps, once per-app tokens for Shell apps arrive. Until then it throws "not available yet".
Next steps
- Standalone app sign-in — set up sign-in for a standalone app.
- Who can reach your Service — what your backend must check, with a Python example.