Skip to main content

OIDC Login Flow

Since v9, Archibald supports login via OpenID Connect (OIDC) using the OAuth2 Authorization Code flow with PKCE. Instead of collecting a username and password in the storefront and exchanging them for a token (the legacy password flow), the user is redirected to the identity provider (IdP — e.g. the SAP Commerce authorization server), authenticates there, and is redirected back with an authorization code that the BFF exchanges for tokens.

Both flows share the same client API (SessionClient, useLogin, useLogOut, useUser, useIsLoggedIn), the same JWE session tokens (nct/ncr cookies in the cookie strategy), and the same refresh/logout endpoints. Which flow runs is decided per login call via credentials.authProtocol.

Enabling protocols: the whitelist

Auth endpoints are registered conditionally based on app.authentication.whitelistedProtocols (type AuthProtocol[], i.e. 'password' | 'oidc'):

// environment/common.ts
app: {
authentication: {
whitelistedProtocols: ['password', 'oidc']
}
}
EndpointHandlerRegistered when
POST auth/loginAuthController.login'password' whitelisted
GET auth/loginAuthController.loginUrl (302 to the IdP)'oidc' whitelisted
POST auth/tokenAuthController.token (code → token exchange, used by native)'oidc' whitelisted
GET auth/callbackAuthController.redirectCallback'oidc' whitelisted
POST auth/refreshAuthController.refreshalways
POST auth/logoutAuthController.logoutalways
GET auth/checkAuthController.check (mode: 'try')always
GET auth/userAuthController.user (mode: 'required')always
info

auth/login is overloaded by HTTP method: POST is the password login, GET starts the OIDC redirect. With both protocols whitelisted (the shop template default) both handlers coexist. Paths are relative to your API schema — with the template's app.api config the effective URLs are /api/v2/auth/....

The same whitelist is enforced on the client: CookieAuthAdapter.onLogIn throws Protocol not supported for a non-whitelisted authProtocol.

Key properties:

  • Two full-page redirects. The SPA unloads when the flow starts; SessionClient.logIn() returns { success: true, redirect: true } and does not publish a login event or set loggedIn — the logged-in state materializes on the fresh SSR render after the callback redirect.
  • PKCE and state are handled server-side by the UserAuthProvider (e.g. CommerceUserAuthProvider using openid-client): code_verifier, auth_state, and auth_return_path are stored in short-lived (5 min) httpOnly cookies, validated and cleared in the callback.
  • redirect_uri defaults to ${app.canonicalBaseUrl}/api/v2/auth/callback — a correct canonicalBaseUrl per environment is mandatory.
  • Session cookies (nct, ncr) are set during the callback request via AuthService.checkSignAndSetToken(), before the final redirect is issued.
  • Query param forwarding is allowlisted. Only params listed in the SessionClient's oidcConfig.queryParamsWhitelist (e.g. ['returnPath']) survive from login() to GET auth/login.

Native flow (header strategy)

On React Native/Expo the redirect dance happens in an in-app browser via expo-auth-session, and the code exchange goes through POST auth/token:

  1. login({ authProtocol: 'oidc', clientId, discoveryEndpoint, ... })NativeAuthAdapter.onLogIn.
  2. The adapter builds an AuthRequest (responseType: Code, redirectUri from makeRedirectUri({ path: redirectPath })), resolves the discovery document (fetchDiscoveryAsync, falling back to a bare authorizationEndpoint), and opens the system browser with promptAsync.
  3. On success the deep link returns a code; the adapter posts { code, code_verifier, redirect_uri, authProtocol: 'oidc' } to POST auth/token.
  4. AuthService.token()UserAuthProvider.token() exchanges the code. With strategy: { type: 'header' } the signed tokens come back in the JSON body and the adapter persists them to the client storage (secure store); middleware attaches Authorization: Bearer <token> to subsequent requests.

PKCE on the native path is driven by expo-auth-session on the client (the code_verifier never leaves the device except in the exchange request); there is no server-side state cookie validation here.

Where the flows converge

AuthService.login() (password) and AuthService.token() (OIDC exchange) run the same internal pipeline, differing only in which provider hook is called and which authProtocol is stamped into the token claims:

  1. UserAuthProvider.login(credentials) / .token(credentials) / .redirectCallback() returns provider tokens.
  2. All registered SystemAuthProviders log in and contribute their tokens.
  3. UserAuthProvider.loadUser(tokens) fetches the user.
  4. checkSignAndSetToken() signs everything into JWE session tokens and (cookie strategy) sets nct/ncr.

Because the authProtocol is stamped per claim and preserved on refresh, password- and OIDC-issued sessions refresh, validate, and log out identically.