Auth0 setup

processOS uses Auth0 with Organizations for identity and affiliation. Deployment ownership and app grants come from the required deployment policy, not Auth0 organization roles. Keep existing participant accounts and organizations in place.


The login flow

/login                    identity sign-in, no organization
   ↓
/login/select-org         pick an org
   ↓
silent org-scoped authorize   (organization + prompt=none)
   ↓
app                       token now carries org_id + permissions

Two authorizations. The first proves who you are; the second scopes you to an org and is what puts permissions in the access token.

Invite links skip the first: one org-scoped authorize with organization + invitation.

Client state lives in localStorage as process-platform-token and process-platform-organization, per app origin.

Silent session renewal

Access tokens last 24 hours and each app keeps its own. So users are not sent to the sign-in page every day on every app, an app renews silently against the Auth0 session instead:

  • When: on a protected page with no token, an expired token, or one with less than 10 minutes left (the app's auth guard), and on a 401 from the API. Never on a timer, so a form the user is filling in is not navigated away from.
  • How: a top-level /api/auth/login?prompt=none&renew=1&returnUrl=… (plus organization when the app already has one). With a live Auth0 session (signed in to any processOS app on the same tenant) the user comes straight back to the same page, signed in, with no click. A new app goes through identity renewal and then the usual organization step.
  • Fallback: if Auth0 answers login_required, consent_required or interaction_required, the callback sends the user to this app's /login with the same return path. The signed OAuth state carries renew, so a failed renewal never escalates to an interactive login on its own.
  • Loop guard: at most one silent attempt per tab every 2 minutes (sessionStorage process-platform-silent-renewal).
  • Sign-out: the Sign out link sets process-platform-signed-out, which suppresses renewal on that app until the next completed sign-in (the callback clears it).

Nothing about Auth0 changes: same tenant settings, token lifetime and sign-in methods.


Setup

1. Tenant

Enable Organizations. Create each org and note its org_… id.

2. Application

Enable Organizations for the app. Set Application Login URI to {APP_BASE_URL}/login.

Add every web origin — default and each skin — to:

  • Allowed Callback URLs
  • Allowed Logout URLs
  • Allowed Web Origins

3. API

Configure the audience and supported Auth0 login/access-token flow. Existing RBAC claims can remain for compatibility with other consumers, but this executor ignores them as authorization grants.

4. App roles and membership

Provision the deployment policy with verified-email memberships and explicit operator grants. Do not fix an app 403 by adding a global Auth0 role; check the target app's declaration instead. Unverified identities are denied even if their email is declared. An empty unscoped /api/me.permissions can be normal: app grants are in applicationPermissions or the target-scoped identity response.

5. Management API credentials

AUTH0_MGMT_CLIENT_ID / AUTH0_MGMT_CLIENT_SECRET, an M2M application with scopes:

read:organizations    read:users    read:roles

Used for the org picker and verified identity/member resolution, including impersonation. App permissions come from declarations, not provider role lookup. Explicit invitations require additional provider permissions and configured onboarding settings; startup never sends them.

On the API, Settings → Allow Skipping User Consent.

Without it the silent org-scoped authorize returns consent_required and login stalls. The app retries without prompt=none so Auth0 can show the consent screen once, but enabling the setting avoids that path.


Verifying

curl -s $BASE/api/me -H "Authorization: Bearer $TOKEN" \
     -H "X-Process-Platform-Organization: org_..."
{ "userId": "auth0|…", "organizationId": "org_…",
  "permissions": ["templates:read", "processes:read", …],
  "canImpersonate": false, "impersonating": false }

An empty permissions array with a valid token means step 4 — roles assigned globally rather than at org level.


Impersonation

Requires user:impersonate. The caller sends:

X-Impersonate-User-Id: auth0|…
X-Impersonate-Organization-Id: org_…      (optional, defaults to the actor's org)

The effective principal becomes that user in the target org, with that user's org-role permissions fetched via the Management API. The actor's own JWT and X-Process-Platform-Organization stay on the actor's org — only the effective principal switches.

The real actor is preserved on principal.impersonatedBy and surfaced as actor on /api/me.

Because it resolves permissions live, user:impersonate is effectively tenant-wide access to every org the target belongs to. Grant it narrowly. Note also that impersonated writes use the effective user id in canonical field/completion audit. The original actor is exposed on /api/me, but that is not a separately persisted original-actor audit guarantee for every command.


Local and scripted authentication

Use the supported Auth0 login/access-token flow for application and script requests. There is no packaged token-minting CLI: the former mint-long-lived-jwt package command pointed to a missing script and has been removed. JWT_SECRET is not an application access-token setting.

Repository verification uses controlled synthetic identities/JWKS on isolated local origins. Those fixtures test permission and organization behavior without live credentials; they are not a production authentication bypass or a substitute for verifying the actual Auth0 configuration.