Groundfloor Docs

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_access scope.
  • 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:

SettingLimitRules
Extra callback pathsUp to 5, besides /auth/callbackAn 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 originsUp to 20Other 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:

WhereWhat you get
Environment variables on the main ServiceWhen 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 PortalOn the app's page, the Authentication card shows the Client ID, Issuer and JWKS URL, with copy buttons
The CLIgf apps auth <app>, or gf apps auth <app> --json for scripts
From the browserGET 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:

auth.ts
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}` },
  });
}
callback.ts
// 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/callback with your front end, for example by serving index.html for every path it doesn't know.
  • To skip the lookup, copy the issuer and client ID from gf apps auth into your front end's build config and use them instead of getAppAuthConfig. The GROUNDFLOOR_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:5173

http://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/done

The 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_statusMeaning
In syncin_syncThe sign-in client matches these settings
UpdatingpendingA change is on its way. Check again in a moment
Not switched on yetsync_disabledApp sign-in isn't switched on for your cell yet. Users can't sign in; these settings apply once it is
Not usednot_applicableThe 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 GroundfloorunmanagedA sign-in client with this ID already exists that Groundfloor didn't create, so it's left alone. Contact support
Status unknownunknownThe status couldn't be read just now. Try again

Troubleshooting

ProblemLikely causeWhat to do
The sign-in page says the redirect URI is invalidThe app sent users back to an address that isn't listedUse /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 originAdd it under Extra origins
getAppAuthConfig returns null, or the lookup gives 404The 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 yetCheck the host and the Authentication card
Not switched on yetApp sign-in is still rolling outWait until it's switched on for your cell
No GROUNDFLOOR_AUTH_* variablesThe Coderunner isn't the app's main Service, app sign-in isn't switched on, or the workspace doesn't use Groundfloor sign-inDeploy the main Service again once sign-in is on, or copy the values from gf apps auth
The backend refuses a token with wrong_audienceThe token was issued to another app, or it's a Shell site tokenSign in through this app's client

Next steps

On this page