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:
| Type | Fields | Used with |
|---|---|---|
CookieAuthCredentialsPassword | authProtocol: 'password', username, password | CookieAuthAdapter (web) |
CookieAuthCredentialsOidc | authProtocol: 'oidc', oidcSearchParams? | CookieAuthAdapter (web) |
NativeAuthCredentials | authProtocol: 'oidc', expo AuthRequestConfig fields, redirectPath?, discoveryEndpoint?, authorizationEndpoint? | NativeAuthAdapter |
HeaderAuthCredentials | username, password | HeaderAuthAdapter |
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 receivesApiServiceTokens(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.canonicalBaseUrlis correct per environment — it becomes the base of the OIDCredirect_uri(${canonicalBaseUrl}/api/v2/auth/callback). Register that callback URL with your IdP.hybris.oauthhas the rightclient_id/client_secretfor the authorization code grant (optionallyscope,redirect_uri).- Your
UserAuthProviderimplementsloginUrl(),redirectCallback(), andtoken().CommerceUserAuthProviderships 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
loginevent. The call resolves{ success: true, redirect: true }and the page unloads. Post-login UI state comes from the fresh SSR render after the callback (withauthenticateOnServer: true,useIsLoggedIn()is alreadytrueserver-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'sredirectCallbackSuccess()hook —CommerceUserAuthProvidermerges the anonymous cart and wishlist there — or trigger it from thereturnPathlanding 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 callinglogin({ 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 withlogin({ authProtocol: 'oidc' }).