Skip to main content

DXP Auth Flow

This page explains the core concepts behind Stack9 DXP integration — what the two instance types are, how authentication handoff works, and how permissions and API credentials flow through the system.


What is DXP?

Stack9 DXP (Digital Experience Platform) connects two separately deployed Stack9 instances:

  • Provider — the organisation's own Stack9 instance where users log in. It hosts core business data and renders DXP tiles in its navigation bar.
  • Client — a dedicated Stack9 DXP instance hosting the Email, Marketing, and Pages modules. Users never log in to it directly; they arrive via tile click from the Provider.

This separation allows the DXP modules to serve multiple organisations (tenants) from a single deployment, while each organisation continues to manage their own identity and data in their own Stack9 instance.


Glossary

TermMeaning
ProviderThe Stack9 instance configured with dxp.mode = "provider". Issues one-time codes when users click DXP tiles.
ClientThe Stack9 DXP instance configured with dxp.mode = "client". Receives codes, exchanges them, and establishes sessions.
One-time codeA 32-byte random hex string issued by the Provider for each tile click. Valid for 30 seconds (default), single-use.
DXP User GroupA user group (code: dxp-users) auto-created by the Client bootstrap. All DXP-arrived users are members.
DXP User security roleA security role auto-created by the Client bootstrap. Assigned to users provisioned on first DXP login.
Privileged levelAppAccessLevels.Privileged = 3. The access level granted to the DXP User Group on each DXP app.
Exchange secretA pre-shared string (APP_DXP_CODE_EXCHANGE_SECRET) that the Client sends in the x-dxp-secret header to authenticate server-to-server code exchange requests.

Full Authentication Flow

The following sequence diagram shows the complete flow from tile click to authenticated session on the Client.

sequenceDiagram
participant U as Browser (User)
participant P as Provider<br/>(Stack9 Core)
participant R as Redis<br/>(Provider)
participant C as Client<br/>(Stack9 DXP)
participant RC as Redis<br/>(Client)
participant DB as Database<br/>(Client)

U->>P: Authenticated — clicks DXP tile (e.g. Marketing)
P->>P: Generate 32-byte random hex code
P->>R: Store {userId, email, orgId, apiKey, apisBaseUrl, appType, origin}<br/>Key: dxp:authCode:{code} TTL: 30s
P-->>U: HTTP 302 → {clientUrl}/api/auth/dxp/callback<br/>?code=X&origin=Y&redirect=/mkt

U->>C: GET /api/auth/dxp/callback?code=X&origin=Y&redirect=/mkt

Note over C: Server-to-server exchange (browser waits)
C->>P: POST /api/auth/dxp/exchange-code<br/>Header: x-dxp-secret: {exchangeSecret}<br/>Body: { code }
P->>P: 1. Validate x-dxp-secret header
P->>R: 2. Look up code → validate TTL & single-use
P->>R: 3. Delete code immediately (invalidate)
P-->>C: 200 { userId, email, orgId, appType, apiKey, apisBaseUrl, origin }

C->>DB: Find user by email (or provision on first login)
C->>DB: Ensure user is in dxp-users group
C->>RC: Cache { apiKey, apisBaseUrl, appType, providerOrigin }<br/>Key: dxp:apicfg:{localUserId} TTL: session lifetime
C->>DB: Compute JWT roles from local user_groups + app_permissions
C->>C: Sign JWT (adapter, user_id, email, roles, is_administrator)
C-->>U: HTTP 302 → /mkt<br/>Set-Cookie: api-token={jwt} (httpOnly, secure, sameSite=lax)

Key points

  • The one-time code travels through the browser URL but is never shown in a response body or usable after exchange.
  • The apiKey (tenant API key) travels only server-to-server — it is never in any browser-visible URL, cookie, or response.
  • Provider roles are discarded. The Client recomputes roles from its own local user groups and app permissions. This prevents cross-instance role leakage and ensures the Client's own permission model governs access.
  • The origin field in the exchange response is the Provider's public URL. The Client caches it per-session so the DXP app can render a "Back to [Instance]" link.

Permission Model

How DXP users get access

Access on the Client is governed by the dxp-users user group, not by any role inherited from the Provider. During bootstrap, the Client:

  1. Creates the dxp-users user group (once, idempotent).
  2. For each DXP app key (email, mkt, pages), patches the app_permissions record:
    • Sets minimumRole ≤ Privileged (3) so tiles appear in navigation.
    • Adds dxp-users group ID into accessLevels["3"].userGroups.
  3. Patches all app_screen_permissions rows for those app keys:
    • Caps any query role above Privileged (3) down to 3. This is necessary because ScreenPermission.build() defaults new queries to Admin (4), which would block DXP users from every screen query.

Access level hierarchy

LevelNameNumeric value
BasicBasic1
ContributorContributor2
PrivilegedDXP granted level3
AdminAdmin (default for new queries)4

DXP users compute to Privileged (3) because they belong to dxp-users, which is granted at level 3 on each DXP app permission. The bootstrap caps all screen query roles to 3 so that queryRole > userRole never fires for DXP users.

Why bootstrap must run before the first login

The webserver process does not call the bootstrapper. Bootstrap is a separate CI/CD step (node main.js bootstrap). If it has not run for a given Client instance, the dxp-users group won't exist in the database and the callback will return misconfigured.


API Key Injection

After the Client callback, the Provider's tenant apiKey is cached in Redis under dxp:apicfg:{userId} for the duration of the user's session. When the user makes subsequent API requests through the Client, the connector resolution layer reads this cache entry and substitutes the session-scoped apiKey in place of the static APP_S9_APIS_API_KEY environment variable.

This means connector JSON definitions require no changes — the same %%APP_S9_APIS_API_KEY%% placeholder works in both standard deployments (resolved from env var) and DXP deployments (resolved from session cache).

flowchart LR
A[Connector request] --> B{DXP session cache<br/>dxp:apicfg:{userId}\npresent?}
B -- Yes --> C[Use session apiKey]
B -- No --> D[Use APP_S9_APIS_API_KEY<br/>env var]
C --> E[Upstream API call]
D --> E

Return Navigation

The Provider's public URL (origin) is included in the exchange response and cached by the Client alongside the API key. The DXP frontend uses this value to render a "Back to [Instance Name]" link in the application shell, so users can return to their Provider instance without manually navigating.

This URL is only transmitted server-to-server (as part of the exchange payload) and is never visible in the browser redirect.