Skip to main content

Using OIDC in Your Project

This guide walks through setting up the OIDC login flow exactly as the shop template does it — web (cookie strategy) first, then native (header strategy), then local development against the mock identity provider. If you are starting from the shop template, most of this is already in place; use this page to understand and adapt it. If you are starting from the basic template (which has no auth at all), these are the pieces you add.

For the conceptual overview of the flow see OIDC Login Flow.

1. Configuration

Whitelist the protocol

// environment/common.ts
app: {
authentication: {
whitelistedProtocols: ['password', 'oidc']
}
}

Keep 'password' in the list only if you still need the password grant (e.g. during a migration) — its presence is what registers POST auth/login. See Migrating from the password flow.

Token secret and canonical base URL

// environment/common.ts
app: {
canonicalBaseUrl: 'https://my-shop.example.com'
},
server: {
credentials: {
token: {
secret: '{{TOKEN_SECRET|<32-char-fallback-for-local-dev>}}'
}
}
}
  • server.credentials.token.secret encrypts the JWE session tokens. It must be exactly 32 characters; the AuthModule refuses to start without it.
  • app.canonicalBaseUrl matters twice: it is the JWE iss claim, and it seeds the default OIDC redirect_uri (${canonicalBaseUrl}/api/v2/auth/callback). It must be correct per environment, or the IdP will redirect users to the wrong host.

OAuth client credentials (server-side)

The CommerceUserAuthProvider reads its OAuth client from hybris.oauth:

// environment/common.ts
hybris: {
oauth: {
client_id: 'my_client',
client_secret: '{{OAUTH_CLIENT_SECRET|...}}'
// optional: scope (defaults to 'openid'), redirect_uri (defaults to
// `${canonicalBaseUrl}/api/v2/auth/callback`)
}
}

The provider discovers the IdP endpoints from GET {hybris.api}/authorizationserver/oauth/ and falls back to manually configured authorize/token/introspect/revoke URLs when discovery is unavailable.

2. Register the AuthModule (server)

Register the AuthModule first in initModules() — it registers the jwt/cookie/header Hapi auth strategies that other modules' routes reference:

// src/shop/server/module/server.tsx
import { AuthModule } from '@archibald/auth';
import { CommerceStaticAuthProvider, CommerceUserAuthProvider } from '@archibald/commerce/auth';

export class Server extends CoreServer {
public async initModules() {
await this.registerModules([
new AuthModule({
providers: [
new CommerceUserAuthProvider({
config: () => this.configService.get('hybris.api'),
credentials: () => this.configService.get('hybris.oauth')
}),
new CommerceStaticAuthProvider({
config: () => this.configService.get('hybris.api'),
credentials: () => this.configService.get('hybris.oauth')
})
],
options: () => ({
strategy: { type: 'cookie' },
token: { secret: this.configService.get('server.credentials.token.secret') },
refresh: { secret: this.configService.get('server.credentials.token.secret') }
})
})
// ... other modules
]);
}
}

Notes:

  • options must be a function (dynamic accessor) — a static object throws when multi-country support is on.
  • CommerceUserAuthProvider implements the OIDC pieces (loginUrl, redirectCallback, token) including PKCE (S256) and state validation, plus the server-side anonymous cart/wishlist merge on successful login. To customize it, extend it — the portal platform does this with CommerceB2BUserAuthProvider extends CommerceUserAuthProvider.

3. Create the SessionClient (web client)

// src/shop/client/api/creators/session.ts
import { SessionClient, CookieAuthAdapter, type OidcConfig } from '@archibald/auth';
import { api } from 'shop/client/api';

const oidcConfig: OidcConfig = {
queryParamsWhitelist: ['returnPath']
};

export default new SessionClient({ authenticateOnServer: true, adapter: CookieAuthAdapter, api, oidcConfig });
  • oidcConfig.queryParamsWhitelist is the allowlist of query params forwarded from login() to GET auth/login. The template forwards returnPath so users come back to the page they started on; add your own params here if your provider needs them.
  • authenticateOnServer: true lets isLoggedIn() read the SSR session during server rendering, so the logged-in header renders in the initial HTML after the callback redirect.

Register it via the SessionClientProvider in your App component (the template does this through ProviderComposer):

provider(SessionClientProvider, { client: SessionClient }),

4. The login form

With OIDC the storefront no longer renders credential inputs — the IdP does. The template's login form is a single button:

// src/shop/client/features/login/components/form/LoginForm.tsx
function LoginForm() {
const { login, isLoading } = useLogin();

async function handleLogin() {
const params = new URLSearchParams();
params.append('returnPath', window.location.pathname);
await login({
authProtocol: 'oidc',
oidcSearchParams: Object.fromEntries(params.entries())
});
}

return (
<Box aria-label="login form">
<Button appearance="primary" fullWidth onClick={handleLogin} loading={isLoading} disabled={isLoading} type="submit">
<Message code="account.form.button.login" simpleText />
</Button>
</Box>
);
}

login() triggers a full-page navigation to GET /api/v2/auth/login — it resolves with { success: true, redirect: true } and the page unloads. Do not expect a user object or a login event from this call; the session exists on the next SSR render (after GET auth/callback redirected back).

The template's registration flow uses the same trick: after creating the account via the Commerce API it calls login({ authProtocol: 'oidc' }) to bounce the fresh user through the IdP.

5. Native (React Native / Expo)

The app platform shadows the session creator (file shadowingsrc/app/... overrides src/shop/... when building platform=app):

// src/app/client/api/creators/session.ts
import { SessionClient, StorageAdapter, type NativeAuthCredentials } from '@archibald/auth';
import { NativeAuthAdapter } from '@archibald/auth';
import { deleteItemAsync, getItemAsync, setItemAsync } from 'expo-secure-store';

class CustomNativeStorageAdapter extends StorageAdapter<string> {
/* set/get/remove wrapping expo-secure-store */
}

api.register(authBeforeMiddleware); // attaches Authorization: Bearer <token>
api.register(authUnauthorizedMiddleware); // refreshes on 401, logs out if refresh fails

export default new SessionClient<User, NativeAuthCredentials>({
authenticateOnServer: false,
adapter: NativeAuthAdapter,
storage: CustomNativeStorageAdapter,
api
});

NativeAuthAdapter is exported only from the @archibald/auth expo entry point (it depends on expo-auth-session).

The app server registers the same providers but with strategy: { type: 'header' } — tokens are returned in the POST auth/token response body instead of cookies, and persisted to the secure store.

The client OIDC parameters live in project config (typed in the template via ProjectAppConfig):

// environment/common.ts (app platform tenant)
app: {
commerce: {
oidc: {
clientId: 'trusted_client',
discoveryEndpoint: 'https://<commerce-host>/authorizationserver/oauth/',
authorizationEndpoint: 'https://<commerce-host>/authorizationserver/oauth/authorize'
}
}
}

And the native login form passes them straight through:

// src/app/client/features/login/components/form/LoginForm.tsx
const oidcConfig = Config.get('app.commerce.oidc');
if (!oidcConfig) throw new Error('OIDC configuration is missing');
await login({ ...oidcConfig, redirectPath: '/', usePKCE: false, authProtocol: 'oidc' });

6. Local development: the mock IdP

With hybris.api.mocked: true the shop template's MockModule serves a complete mock authorization server, so the full OIDC round trip works offline:

MethodPathPurpose
GET/authorizationserver/oauth/ and .../.well-known/openid-configurationDiscovery document
GET/POST/authorizationserver/oauth/authorizeHybris-lookalike login page; on success redirects with code=MOCK_CODE_<username>
POST/authorizationserver/oauth/tokenToken endpoint (all four grant types)
POST/authorizationserver/oauth/introspectToken introspection
POST/authorizationserver/oauth/revokeToken revocation (logout)

The mock accounts are erika.musterfrau@hybris.com and max.mustermann@hybris.com (password = the email address). Point the native client at the mock in environment/local.ts:

commerce: {
oidc: {
clientId: 'client_id',
redirectPath: '/',
discoveryEndpoint: 'http://{{PUBLIC_IP|null}}:{{SERVER_PORT|3100}}/authorizationserver/oauth',
authorizationEndpoint: 'http://{{PUBLIC_IP|null}}:{{SERVER_PORT|3100}}/authorizationserver/oauth/authorize'
}
}
Local port collisions

When the configured SERVER_PORT differs from the port in app.canonicalBaseUrl, CommerceUserAuthProvider rewrites the port in the redirect_uri accordingly — so a dev server that fell back to another port still gets a working callback.

7. Testing the flow (e2e)

The Playwright page object shows the observable steps and the two classic pitfalls:

// src/shop/e2e-playwright/pages/LoginPage.ts
async login(username = getTestUser().email, password = getTestUser().password) {
await waitForHydration(this.page, selectors.loginPage.form.formSelector);
await this.page.locator(selectors.loginPage.form.loginButton).click(); // starts the OIDC redirect
await this.fillLoginFormAndSubmit(username, password); // fills the IdP's page, not yours
const cookies = await this.page.context().cookies();
expect(cookies.find((cookie) => cookie.name === 'nct')).toBeDefined();
expect(cookies.find((cookie) => cookie.name === 'ncr')).toBeDefined();
await waitForHydration(this.page, '#app');
return this;
}
  • The login form is a lazily hydrated island — clicking before hydration silently drops the redirect. Wait for hydration first.
  • The username/password/submit selectors belong to the IdP's login page (locally: the mock's HTML), not your storefront.
  • The callback lands on a fresh SSR page — wait for #app to hydrate again before further interaction.

Checklist

  1. app.authentication.whitelistedProtocols includes 'oidc'.
  2. server.credentials.token.secret set (32 chars) and app.canonicalBaseUrl correct per environment.
  3. AuthModule registered first, options as a function, strategy: { type: 'cookie' } (web) / { type: 'header' } (native).
  4. hybris.oauth client credentials configured (plus optional scope / redirect_uri).
  5. SessionClient with CookieAuthAdapter + oidcConfig.queryParamsWhitelist.
  6. Login form calls login({ authProtocol: 'oidc', oidcSearchParams: { returnPath } }).
  7. Native: app.commerce.oidc config, shadowed session creator with NativeAuthAdapter + secure-store adapter + bearer/401 middlewares.
  8. Local dev: mock auth routes active via hybris.api.mocked: true.