OIDC Login for Commerce Storefronts: Tokens, Flows, and the Commerce Session Bridge
OIDC as the storefront auth backbone: the authorization code flow with PKCE, the three tokens, the commerce session bridge, cart merge, and logout semantics.
Marcus Lindholm
SAP Commerce Integration Specialist
OCC and Integration APIs, SAP Integration Suite, S/4HANA and ERP integration, and asynchronous messaging.
If SAML is the federation protocol of the enterprise intranet, OpenID Connect is the one the storefront actually wants: token-based, SPA-friendly, mobile-native, built on OAuth 2.0 machinery your API layer already speaks. Modern commerce estates converge on the same topology: the storefront authenticates users against an identity layer via OIDC, and that identity becomes a commerce session through a token bridge. This guide covers the protocol precisely enough to debug it, the commerce-specific bridging patterns, and the design decisions (claims, lifetimes, logout) that separate a clean login architecture from a drawer of workarounds.
The Flow, Step by Step#
The authorization code flow is the storefront default (with PKCE, which removes the need for a client secret in public clients like SPAs and apps):
- Redirect to the OP: the storefront sends the user to the OpenID Provider's authorize endpoint with
client_id,redirect_uri, requestedscopes (always includingopenid), astatevalue, and the PKCE code challenge. The user authenticates at the provider, never typing credentials into your storefront, which is the security point of the whole exercise. - Optional consent: the user approves the requested scopes (what the storefront may learn about them).
- Code back: the provider redirects to your
redirect_uriwith a short-lived authorization code. - Code for tokens: the storefront (or its backend-for-frontend) exchanges code plus PKCE verifier at the token endpoint.
- Userinfo as needed: with a valid access token, the userinfo endpoint returns the claims the granted scopes cover.
Three tokens come back, and the discipline of using each for its purpose prevents a whole class of bugs:
- ID token (a JWT): proof of authentication plus identity claims, for the client's consumption. It is not an API credential; treating it as one is the most common OIDC misuse.
- Access token: the credential for protected resources (userinfo, and in the bridged patterns below, your APIs). Short-lived by design.
- Refresh token: the long-lived credential for getting new access tokens without re-authentication. It is the token whose storage you must actually worry about: httpOnly cookie via a BFF, or secure storage in native apps, never localStorage if you can avoid it.
The Commerce Bridge: From Identity to Session#
An authenticated OIDC identity is not yet a commerce session; OCC needs to know who this is.
Before the patterns, the correction that reframes all three: SAP Commerce is an OpenID Provider, not a relying party. It runs its own OAuth 2.0 authorization server, exposing the token endpoint at /authorizationserver/oauth/token alongside /authorize, /jwks, /userinfo, /revoke, and /introspect. There is no documented setting that points the platform at somebody else's issuer. Any design premised on "Commerce validates tokens our CIAM minted" needs checking against your actual version before it becomes a plan.
Check the version carefully, because the stack changed underneath this. On the JDK 17 stack the authorization server is the oauth2 extension, and its web root is configurable through oauth2.webroot. On the JDK 21 stack oauth2 is removed and replaced by three extensions: authorizationserver, resourceserver, and oauth2commons.
The supported flows changed with it, and this is the detail most likely to break a design copied from an older project. The JDK 17 stack supports all four classic OAuth flows, including resource-owner password and implicit. The JDK 21 stack supports neither password nor implicit. What it does support is authorization code, authorization code with PKCE, refresh token, client credentials, and a custom SAML token flow, with public clients restricted to authorization code with PKCE, refresh token, and the SAML flow. That is a straightforward improvement, and it also means a storefront still authenticating by posting a username and password to the token endpoint has a migration ahead of it, not just an upgrade.
Every client that may ask for a token is a row of the OAuthClientDetails type:
INSERT_UPDATE OAuthClientDetails; clientId[unique=true]; public; authorities; scope; authorizedGrantTypes ; registeredRedirectUri
; acme_storefront ; true ; ROLE_CLIENT; basic; authorization_code,refresh_token; https://shop.acme.com/callback
Three columns there decide most of the debugging you will do later:
authorizedGrantTypesis a whitelist, not documentation. List only what that client actually uses. The row above is a public client, which is why it holds no secret and no password grant.authoritiesseparatesROLE_CLIENTfromROLE_TRUSTED_CLIENT: a client that may act only as itself, versus one that may act on behalf of a user. Getting this wrong is how a storefront client quietly gains more reach than the security review believed.registeredRedirectUriis the field behind the most common failure in the list further down. It is matched exactly, so every environment's callback URL has to be present, and a value that works in staging does not work in production unless somebody added it.
On the JDK 21 stack the type also carries public, requireProofKey, and loginPageUri, which is where PKCE enforcement and the login redirect are configured rather than assumed.
One naming warning. SAP's older documentation used sample client ids that a great many projects copied verbatim into production: trusted_client, mobile_android, client-side. These were always samples in importable ImpEx rather than shipped essential data, and the current documentation has replaced the literal names with placeholders and an instruction to use unique, non-default client ids. Treat any of those three found in a production estate as a finding. The composable storefront's own default is a separate, public client, mobile_android_public.
Keep the row in versioned ImpEx, per environment, and "who can get a token, and for what" has a file as its answer instead of a person.
With that established, three patterns cover the estate:
1. A custom token granter, with the external IdP doing the authenticating. This is the pattern SAP actually documents for external identity providers, and it is narrower than the one teams usually sketch. The external IdP authenticates the person; a customTokenGranter you configure on the platform accepts that proof and mints a normal OCC token for the mapped customer. Commerce still issues the token your API calls carry. Two constraints worth knowing before you design around it: you get one custom token granter per environment, so a second identity source is a rewrite rather than a second registration, and the OCC side remains SAP's token, so your access-token lifetime is governed by the platform, not by your OP.
2. Token exchange at a bridge endpoint. The storefront authenticates against the OP, then presents the resulting token to a small bridging service (or a custom commerce endpoint) that validates it and issues the commerce-side OCC token for the mapped customer. More moving parts, but it decouples your identity provider choice from commerce's OAuth configuration, which matters mid-migration.
3. CIAM as OP for the whole estate. The pattern the CX Works source documents from the provider side: your customer base, exposed as an OpenID Provider, so any relying party (the storefront, the mobile app, a partner portal, a support tool) authenticates against it with standard OIDC. Claims and scopes become your integration contract: five standard scopes (openid, email, profile, address, phone) plus custom claims mapped from profile fields, grouped into custom scopes per consumer's need. The side-by-side guide's least-privilege ethic applies verbatim: a relying party gets the scopes its function requires, not profile everything.
Whichever pattern: the identity mapping rule from the SAML guide holds. Decide the immutable key claim (the sub claim is the contract; email is mutable), the JIT-versus-reject policy for first-time arrivals, and the ownership split of profile fields. And the cart problem is where commerce diverges from generic OIDC tutorials: the anonymous cart must merge into the authenticated context at login, which means your bridge cannot just mint a token; it must trigger the same cart-merge semantics local login has, and your tests must cover login-with-full-cart as a first-class flow.
That merge is an explicit call rather than a side effect of authenticating, which is exactly why bridges forget it:
POST /occ/v2/{baseSiteId}/users/{userId}/carts?oldCartId={anonymousCartGuid}&toMergeCartGuid={userCartGuid}
One trap on the way. You will find toMergeCartId in circulation, while the OCC reference and the controller's own method signature say toMergeCartGuid. The controller wins. If a merge silently does nothing while still returning a success response, a misspelled optional query parameter is the first thing to check, because an unbound optional parameter fails quietly rather than loudly.
Lifetimes, Sessions, and Logout#
- Access tokens short (minutes to an hour), refresh tokens bounded, absolute session ceilings decided. The storefront experience of "still logged in" is the refresh token quietly working; its lifetime is your session policy, so set it from business risk (checkout-capable session length), not library defaults.
- Silent renewal in SPAs happens via the refresh flow through the BFF or the provider's session endpoints; design for renewal failure (provider maintenance, revoked consent) degrading to re-login gracefully rather than a broken half-authenticated state.
- Logout is three logouts: clear the storefront's local state, revoke/end the commerce session, and RP-initiated logout at the provider if the business wants true single logout. Decide explicitly whether logging out of the storefront should end the user's session at the provider (and thus at sibling applications); B2C usually says no, workforce and B2B often say yes. Half-implemented single logout, where the storefront looks logged out but one redirect silently logs you back in, is a recurring pen-test finding.
Failure Modes Worth Pre-Debugging#
The login-error dashboard's usual suspects, with their signatures:
redirect_urimismatch: the provider rejects at authorize or token time because the registered URI and the sent URI differ by a slash, a port, or an environment. Registration must cover every environment's exact URIs; wildcard temptation is a security hole, per-environment registration is the answer. Note that there are two registries to keep aligned, not one: the provider's client configuration and, where Commerce issues the token,registeredRedirectUrion theOAuthClientDetailsrow. A mismatch in either produces the same opaque rejection, so check both before assuming the provider is at fault.- Missing
openidscope: you get OAuth, not OIDC: no ID token, confused code. Always first in the scope list. - Clock skew on JWT validation:
iat/expfailures that "only happen sometimes"; NTP plus a small leeway. - State/nonce mishandling: dropped
statebreaks the round trip (and its absence invites CSRF on the callback); libraries handle it, hand-rolled flows forget it. Use the library. - Key rotation surprises: providers rotate signing keys; validators must fetch JWKS dynamically and cache politely, not pin a key from the day of integration.
- Consent loops: scope changes re-prompt users; coordinate scope additions with release notes, or Monday's deploy generates Tuesday's "site keeps asking me things" tickets.
Instrument the funnel itself: authorize redirects, callbacks, token exchanges, bridge successes, each counted; the difference between steps is where logins die, and without the funnel you are debugging from user anecdotes.
Checklist#
- Authorization code + PKCE everywhere public; no implicit flow anywhere; secrets only in confidential backends
- Token roles respected: ID token for the client, access token for APIs, refresh token stored properly
- Commerce bridge chosen deliberately; cart merge on login covered by tests
-
subas the identity key; JIT policy written; profile field ownership split documented - Lifetimes set from business risk; renewal failure degrades gracefully; logout semantics decided at all three layers
- Every environment's redirect URIs registered exactly; JWKS fetched dynamically
- Login funnel metrics on a dashboard, alarmed like checkout (because it is checkout's front door)
OIDC's virtue is that all of this is standard: the same flow, tokens, and failure modes every well-documented provider and library already handles. The engineering that remains yours is the commerce-specific 20%: the session bridge, the cart merge, the claims contract, and the logout semantics. Spend the design effort there, keep the protocol vanilla, and login becomes the least interesting reliable part of your storefront, which is exactly what login should be.