Standalone app sign-in
Sign your users in to a standalone app with its own sign-in client, and find its issuer, client ID and allowed addresses.
Rolling out
App sign-in for standalone apps is being switched on cell by cell. Until it's on for yours, the app's Authentication card shows Not switched on yet and users can't sign in with it yet. The settings you see and save there are the ones that will apply.
Each standalone app gets its own sign-in client in your workspace's App authentication. Your app's front end signs users in with it, and your app's backend accepts only tokens issued to it, so a token meant for another app, or for your Shell site, is refused.
Use it when your standalone app has its own front end and your users are managed in App authentication. A Shell app doesn't need it: the Shell site signs users in for every Shell app.
Before you start
- A standalone app whose main Service is deployed, so the app has an address.
- Groundfloor sign-in for the workspace, on Administer → App authentication. Your users and their roles live there.
- To change the settings, the Writer, Admin or Owner role in the workspace. Any workspace role can view them.
Using your own identity provider?
With External sign-in, Groundfloor doesn't create a sign-in client for your apps. Register the app with your provider, sign users in there, and have your backend check your provider's tokens with its issuer, keys and client ID. See Check the token.
How sign-in works
- The client ID is
app-<app-id>. It's also the audience your backend checks. - Users sign in with the authorization code flow and PKCE (
S256). There's no client secret, and the password grant is off. - The client doesn't give out offline sessions (long-lived refresh tokens): don't ask for the
offline_accessscope. - Any user of the workspace can sign in to any of its apps; the app's Who can open an app setting doesn't apply to this sign-in. Signing in only proves who the user is, so your backend checks their roles.
- The token works only with your own backend. The Groundfloor API refuses it, so your backend reads and writes Groundfloor data with an M2M client.
Allowed addresses
After sign-in, users are sent back only to an exact redirect URI: https://<host>/auth/callback on each of the app's hosts. The hosts are the main Service's Deployment URL host and every custom domain that's verified. Wildcards are never used, and a domain that isn't verified yet isn't included. A local dev server you add as an extra origin gets redirect URIs too; see Develop locally. If the app has no address yet and no local origin, there are no redirect URIs and nobody can sign in.
You can add to these on the app's Authentication card or with gf apps auth:
| Setting | Limit | Rules |
|---|---|---|
| Extra callback paths | Up to 5, besides /auth/callback | An absolute path such as /login/done, on the app's own hosts and on local dev origins. No wildcard, query, fragment, . or .. segment, or encoded ., / or \ (%2e, %2f, %5c). Up to 200 characters |
| Extra origins | Up to 20 | Other web addresses allowed to call the sign-in service from the browser. https://host[:port] with a domain name, or http://localhost[:port] and http://127.0.0.1[:port]. No IP addresses, paths or wildcards. A local origin (http://localhost or http://127.0.0.1) also becomes a redirect URI: <origin>/auth/callback and <origin><path> for each extra callback path. Any other extra origin is never a redirect URI |
Redirect URIs and allowed origins update by themselves when you add or verify a custom domain, change an extra path or origin, or archive the app. Archiving turns sign-in off for the app.
Find your issuer and client ID
Your front end needs the issuer and client ID; your backend needs the issuer, the client ID (as the audience) and the key address (JWKS URL). Get them in any of these ways:
| Where | What you get |
|---|---|
| Environment variables on the main Service | When you deploy the app's main Service, Groundfloor sets GROUNDFLOOR_APP_ID, GROUNDFLOOR_AUTH_ISSUER, GROUNDFLOOR_AUTH_JWKS_URL, GROUNDFLOOR_AUTH_AUDIENCE and GROUNDFLOOR_AUTH_CLIENT_ID on the running Service, for your backend. They aren't in your front end's build. Values you set yourself win. They aren't secret |
| The Client Portal | On the app's page, the Authentication card shows the Client ID, Issuer and JWKS URL, with copy buttons |
| The CLI | gf apps auth <app>, or gf apps auth <app> --json for scripts |
| From the browser | GET https://platform.groundfloor.cloud/v1/public/app-auth/by-host?host=<your app's host>, with no token. See below |
The environment variables are set only while app sign-in is switched on and the workspace uses Groundfloor sign-in. Your other Coderunners don't get them: copy the values from the Client Portal or gf apps auth if another backend checks the same tokens.
Look them up from the browser
If you haven't put the values in your front end's build, it can ask for them at runtime. Any web address can call this, with no cookies or token, so it works from your custom domains too:
curl "https://platform.groundfloor.cloud/v1/public/app-auth/by-host?host=shop.example.com"{
"issuer": "https://auth.groundfloor.cloud/realms/gf-ws-3f6c…",
"client_id": "app-7b21…",
"realm": "gf-ws-3f6c…"
}It answers only for a standalone app's Deployment URL host or verified custom domain, in a workspace with Groundfloor sign-in. Any other host, a Shell app, an archived app or a workspace with External sign-in gets 404. It's rate-limited, so cache the answer for the page's lifetime.
Sign users in from the browser
Any OpenID Connect library that supports PKCE works. This example uses oidc-client-ts with getAppAuthConfig from the app sign-in library, which makes the lookup above for the page's own host:
import { UserManager } from "oidc-client-ts";
import { getAppAuthConfig } from "@groundfloorcloud/app-auth/browser";
// Pass the Groundfloor API address, for example from your build config.
const cfg = await getAppAuthConfig({ controlPlaneUrl: "https://platform.groundfloor.cloud" });
if (!cfg) throw new Error("This address has no app sign-in");
export const auth = new UserManager({
authority: cfg.issuer,
client_id: cfg.clientId,
redirect_uri: cfg.redirectUri, // https://<this host>/auth/callback
response_type: "code", // PKCE (S256) is on by default
scope: "openid", // never offline_access
});
// Send the user to the sign-in page.
export const signIn = () => auth.signinRedirect();
// Call your backend with the user's token.
export async function api(path: string, init: RequestInit = {}) {
const user = await auth.getUser();
if (!user || user.expired) return signIn();
return fetch(path, {
...init,
headers: { ...init.headers, Authorization: `Bearer ${user.access_token}` },
});
}// Runs on /auth/callback: finish sign-in, then go back into the app.
import { auth } from "./auth";
await auth.signinRedirectCallback();
window.location.replace("/");- Your Service must answer
/auth/callbackwith your front end, for example by servingindex.htmlfor every path it doesn't know. - To skip the lookup, copy the issuer and client ID from
gf apps authinto your front end's build config and use them instead ofgetAppAuthConfig. TheGROUNDFLOOR_AUTH_*variables are set on the running backend, not in your front end's build. - After sign-out, the browser can only return to one of the redirect URIs.
Develop locally
To sign in from a dev server on your own machine, add its address as an extra origin:
gf apps auth add-origin shop http://localhost:5173http://localhost:5173/auth/callback (and the same address with each extra callback path) is now a redirect URI as well as an allowed origin. Open the app on your dev server and sign in as usual: getAppAuthConfig looks up the app's real host, so pass it as host, for example getAppAuthConfig({ controlPlaneUrl: "https://platform.groundfloor.cloud", host: "shop.example.com" }), and set redirect_uri to window.location.origin + "/auth/callback". Only http://localhost and http://127.0.0.1 origins work this way; other extra origins are never redirect URIs. Remove the origin with gf apps auth remove-origin when you no longer need it.
Check the token in your backend
Your backend checks every request's token: the signature, iss, exp, that aud contains and azp equals app-<app-id>, and that it isn't a Groundfloor management token (no x-client-key or x-userid claim). Then it uses sub as the user's ID and checks roles. With Multi-tenant on, it also filters by the organization claim; turning Multi-tenant off removes that claim from app tokens. See Check the token in a standalone app's backend.
Change the settings
Go to Build → Apps, open the app, and use the Authentication card. Add or remove entries under Extra callback paths and Extra origins. Redirect URIs and Allowed origins show the full lists.
gf apps auth shop # show the settings
gf apps auth add-origin shop http://localhost:5173
gf apps auth add-callback shop /login/done
gf apps auth remove-origin shop http://localhost:5173
gf apps auth remove-callback shop /login/doneThe sign-in service is updated in the background, usually within moments. An archived app's settings can't be changed.
Status
The Authentication card and gf apps auth show where the sign-in client stands:
| Status (Client Portal) | sync_status | Meaning |
|---|---|---|
| In sync | in_sync | The sign-in client matches these settings |
| Updating | pending | A change is on its way. Check again in a moment |
| Not switched on yet | sync_disabled | App sign-in isn't switched on for your cell yet. Users can't sign in; these settings apply once it is |
| Not used | not_applicable | The workspace has no app sign-in clients: it uses External sign-in, Groundfloor sign-in is off or not ready yet, or the workspace is archived. The card says which |
| Not managed by Groundfloor | unmanaged | A sign-in client with this ID already exists that Groundfloor didn't create, so it's left alone. Contact support |
| Status unknown | unknown | The status couldn't be read just now. Try again |
Troubleshooting
| Problem | Likely cause | What to do |
|---|---|---|
| The sign-in page says the redirect URI is invalid | The app sent users back to an address that isn't listed | Use /auth/callback, or add the path under Extra callback paths. Check the host is the Deployment URL host, a verified custom domain, or a local dev origin you added (http://localhost or http://127.0.0.1 only) |
| The browser blocks the call to the sign-in service (CORS) | The page's address isn't an allowed origin | Add it under Extra origins |
getAppAuthConfig returns null, or the lookup gives 404 | The host isn't the app's address, the domain isn't verified, the app is a Shell app or archived, the workspace doesn't use Groundfloor sign-in, or app sign-in isn't switched on yet | Check the host and the Authentication card |
| Not switched on yet | App sign-in is still rolling out | Wait until it's switched on for your cell |
No GROUNDFLOOR_AUTH_* variables | The Coderunner isn't the app's main Service, app sign-in isn't switched on, or the workspace doesn't use Groundfloor sign-in | Deploy the main Service again once sign-in is on, or copy the values from gf apps auth |
The backend refuses a token with wrong_audience | The token was issued to another app, or it's a Shell site token | Sign in through this app's client |
Next steps
- Who can reach your Service — check app tokens in your backend.
- App sign-in library — the Node.js helpers.
- App authentication — users, roles and sign-in modes.