Skip to main content

Migrating from the Password Flow

The legacy username/password login (POST auth/login with credentials collected in the storefront) is deprecated since v9 in favor of the OIDC authorization code flow — CommerceUserAuthProvider.login() carries @deprecated v9 - use oidc login flow instead of the password flow. This page covers upgrading an existing project: first the v9 API breaking changes you hit regardless of which flow you use, then the actual switch to OIDC.

Both flows can run side by side during the migration — the protocol is chosen per login() call, and password- and OIDC-issued sessions share the same token, refresh, and logout machinery.

Part 1: v9 breaking changes in @archibald/auth

These apply to every project upgrading to v9, even if you stay on the password flow for now.

SessionClient and hooks are generic over credentials

SessionClient<User> is now SessionClient<User, Credentials>; useLogin and useSessionClient take the same generics. Type your client with the credentials union you actually use:

// before
new SessionClient<User>({ ... });

// after — web (supports both protocols)
import type { CombinedCookieAuthCredentials } from '@archibald/auth';
new SessionClient<User, CombinedCookieAuthCredentials>({ ... });

// after — native
import type { NativeAuthCredentials } from '@archibald/auth';
new SessionClient<User, NativeAuthCredentials>({ ... });

AuthCredentials no longer allows arbitrary keys

The base interface is reduced to { authProtocol?: 'password' | 'oidc' }. Use the specific credential types:

TypeFieldsUsed with
CookieAuthCredentialsPasswordauthProtocol: 'password', username, passwordCookieAuthAdapter (web)
CookieAuthCredentialsOidcauthProtocol: 'oidc', oidcSearchParams?CookieAuthAdapter (web)
NativeAuthCredentialsauthProtocol: 'oidc', expo AuthRequestConfig fields, redirectPath?, discoveryEndpoint?, authorizationEndpoint?NativeAuthAdapter
HeaderAuthCredentialsusername, passwordHeaderAuthAdapter

If you passed extra custom keys through login(), model them explicitly in your own credentials interface extending one of these.

AuthService.logout() takes no arguments

logout(token: string)logout(). Tokens are now resolved from the request context; the service revokes access and refresh tokens across all registered providers.

UserAuthProvider contract changes

Custom providers must be updated:

  • logout() now receives ApiServiceTokens (both access and refresh token) instead of a single string.
  • loginUrl() is now async (Promise<string>) and mandatory.
  • token() is now mandatory.

If your provider has no meaningful OIDC support (yet), implement loginUrl/token to throw or return null — they are only invoked when a client actually starts an OIDC login, which requires 'oidc' in the protocol whitelist.

Part 2: switching the login to OIDC

Step 1 — whitelist oidc

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

This registers GET auth/login, POST auth/token, and GET auth/callback alongside the existing password endpoint. Nothing changes for existing users yet — the CookieAuthAdapter still defaults to authProtocol: 'password' when none is given.

Step 2 — server prerequisites

Usually already in place for a password-flow project, but verify:

  • app.canonicalBaseUrl is correct per environment — it becomes the base of the OIDC redirect_uri (${canonicalBaseUrl}/api/v2/auth/callback). Register that callback URL with your IdP.
  • hybris.oauth has the right client_id/client_secret for the authorization code grant (optionally scope, redirect_uri).
  • Your UserAuthProvider implements loginUrl(), redirectCallback(), and token(). CommerceUserAuthProvider ships all three; custom providers need them implemented (see Authentication Providers).

The AuthModule registration itself is unchanged.

Step 3 — pass oidcConfig to the SessionClient

const oidcConfig: OidcConfig = { queryParamsWhitelist: ['returnPath'] };
export default new SessionClient({ authenticateOnServer: true, adapter: CookieAuthAdapter, api, oidcConfig });

Without a whitelist entry, returnPath (and any other param) is silently dropped from the redirect to GET auth/login.

Step 4 — change the login call site

Before (password):

const { login, isLoading, error } = useLogin();

async function onSubmit(data: FormData) {
const response = await login({
authProtocol: 'password',
username: data.username,
password: data.password
});
// response.user available, 'login' event published, SPA stays mounted
}

After (OIDC):

const { login, isLoading } = useLogin();

async function handleLogin() {
await login({
authProtocol: 'oidc',
oidcSearchParams: { returnPath: window.location.pathname }
});
// full-page redirect — nothing after this line meaningfully runs
}

What to unlearn:

  • Drop the username/password inputs — credential entry moves to the IdP's login page. The template's form is a single button (see the setup guide).
  • No user object, no login event. The call resolves { success: true, redirect: true } and the page unloads. Post-login UI state comes from the fresh SSR render after the callback (with authenticateOnServer: true, useIsLoggedIn() is already true server-side).
  • Client-side post-login work doesn't run. Anything you did after await login(...) (cart merge, analytics, redirects) is cut off by the navigation. Move it server-side into the provider's redirectCallbackSuccess() hook — CommerceUserAuthProvider merges the anonymous cart and wishlist there — or trigger it from the returnPath landing page.
  • Form validation errors disappear — wrong credentials are handled and re-prompted by the IdP. Client-side error handling shrinks to redirect failures.

Step 5 — logout and the rest

No changes: useLogOut, POST auth/logout, POST auth/refresh, GET auth/check, GET auth/user are protocol-independent. OIDC-issued sessions carry authProtocol: 'oidc' in their claims and the provider revokes tokens at the IdP on logout.

Step 6 — update tests

  • Unit: assert the new credentials shape — expect(login).toHaveBeenCalledWith({ authProtocol: 'oidc', oidcSearchParams: { returnPath: '/en/login' } }).
  • E2E: the credential inputs now live on the IdP's page; after the round trip you land on a fresh SSR page — re-wait for hydration. See the setup guide.
  • Local dev/CI: the shop template's mock module serves a full mock authorization server, so no real IdP is needed.

Step 7 — retire the password flow (optional)

Once nothing calls login({ authProtocol: 'password', ... }):

authentication: {
whitelistedProtocols: ['oidc']
}

This deregisters POST auth/login entirely (server- and client-side enforced). Note that registration/change-password flows that manage the account's password via the Commerce API are unaffected — OIDC moves credential entry to the IdP; it does not remove passwords from the account model.

Reference: template before/after

The shop template contains both states, useful as a live diff:

  • After (OIDC): src/shop/client/features/login/components/form/LoginForm.tsx — button-only form.
  • Before (password): src/portal/client/features/login/components/form/LoginForm.tsx — react-hook-form + zod username/password form calling login({ authProtocol: 'password', ... }).
  • Mixed: src/shop/client/features/registration/components/form/RegistrationForm.tsx — creates the account with a password via the Commerce API, then auto-logs-in with login({ authProtocol: 'oidc' }).