Skip to content

Commit 9f4d2ba

Browse files
committed
fix(journey-client): add query param handling back to resume function
1 parent 8e77da3 commit 9f4d2ba

4 files changed

Lines changed: 95 additions & 24 deletions

File tree

.changeset/good-numbers-act.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@forgerock/journey-client': patch
3+
---
4+
5+
Restore legacy resume() redirect query-param handling.
6+
7+
resume() now parses and forwards additional URL params (error, errorCode, errorMessage, nonce, RelayState, scope, suspendedId) and uses authIndexValue as a fallback journey value.

interface_mapping.md

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
document: interface_mapping
33
version: '1.0'
4-
last_updated: '2026-04-06'
4+
last_updated: '2026-04-24'
55
legacy_sdk: '@forgerock/javascript-sdk'
66
legacy_source: '.opensource/forgerock-javascript-sdk/packages/javascript-sdk/src'
77
new_packages:
@@ -312,19 +312,19 @@ const oidcClient = await oidc({ config });
312312
313313
### resume() URL Parameter Parsing
314314

315-
The legacy `FRAuth.resume()` automatically parses 10+ URL parameters from the redirect URL and conditionally adjusts behavior. The new `journeyClient.resume()` handles a subset of these:
315+
The legacy `FRAuth.resume()` automatically parses 10+ URL parameters from the redirect URL and conditionally adjusts behavior. The new `journeyClient.resume()` continues to parse these URL parameters and forwards them through as `options.query` values.
316316

317-
| URL Parameter | Legacy Behavior | New Behavior |
318-
| ------------------------------------ | -------------------------------------------- | ------------------------------------------------------ |
319-
| `code` | Extracted, passed as query param to `next()` | Same — extracted and passed through |
320-
| `state` | Extracted, passed as query param | Same |
321-
| `form_post_entry` | Extracted, triggers previous step retrieval | Same |
322-
| `responsekey` | Extracted, triggers previous step retrieval | Same |
323-
| `error`, `errorCode`, `errorMessage` | Extracted, passed as query params | **Not parsed** — check return value for `GenericError` |
324-
| `suspendedId` | Extracted; skips previous step retrieval | **Not parsed** — handle suspended flows manually |
325-
| `RelayState` | Extracted for SAML flows | **Not parsed** |
326-
| `nonce`, `scope` | Extracted, passed as query params | **Not parsed** |
327-
| `authIndexValue` | Used as fallback tree name | **Not parsed** — pass tree via `options.journey` |
317+
| URL Parameter | Legacy Behavior | New Behavior |
318+
| ------------------------------------ | -------------------------------------------- | ----------------------------------- |
319+
| `code` | Extracted, passed as query param to `next()` | Same — extracted and passed through |
320+
| `state` | Extracted, passed as query param | Same |
321+
| `form_post_entry` | Extracted, triggers previous step retrieval | Same |
322+
| `responsekey` | Extracted, triggers previous step retrieval | Same |
323+
| `error`, `errorCode`, `errorMessage` | Extracted, passed as query params | Same |
324+
| `suspendedId` | Extracted, passed as query params | Same |
325+
| `RelayState` | Extracted for SAML flows | Same |
326+
| `nonce`, `scope` | Extracted, passed as query params | Same |
327+
| `authIndexValue` | Extracted, used as fallback journey name | Same |
328328

329329
> **Migration note:** If your app relies on `suspendedId`, `RelayState`, or `authIndexValue` URL parameters being auto-parsed, you must extract them manually from the URL and pass them via `options.query` in the new SDK.
330330
@@ -1133,12 +1133,11 @@ The legacy `@forgerock/token-vault` package provided advanced token security via
11331133
11341134
### Key Behavioral Removals
11351135

1136-
| Legacy Behavior | New Approach |
1137-
| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1138-
| Global config via `Config.set()` | Each client manages its own config independently |
1139-
| Automatic PKCE challenge generation in `OAuth2Client` | `@forgerock/oidc-client` handles PKCE internally |
1140-
| `HttpClient` auto-injecting bearer tokens and refreshing on 401 | Manually get tokens, add `Authorization` header, handle 401 yourself |
1141-
| Token stored in `localStorage` by default | OIDC client uses `localStorage` by default; journey client step storage uses `sessionStorage` |
1142-
| Per-call config overrides via `StepOptions` | **Major change:** Config is fixed at client creation time. Legacy apps that passed different `tree`, `serverConfig`, or `middleware` per-call must create separate client instances. Only `query` params can vary per-call |
1143-
| `FRUser.logout()` silently swallows errors per-step | `oidcClient.user.logout()` returns structured `LogoutErrorResult` with per-operation error details |
1144-
| `FRAuth.resume()` auto-parses 10+ URL params (suspendedId, RelayState, etc.) | `journeyClient.resume()` only parses `code`, `state`, `form_post_entry`, `responsekey`. Other params must be extracted manually |
1136+
| Legacy Behavior | New Approach |
1137+
| --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1138+
| Global config via `Config.set()` | Each client manages its own config independently |
1139+
| Automatic PKCE challenge generation in `OAuth2Client` | `@forgerock/oidc-client` handles PKCE internally |
1140+
| `HttpClient` auto-injecting bearer tokens and refreshing on 401 | Manually get tokens, add `Authorization` header, handle 401 yourself |
1141+
| Token stored in `localStorage` by default | OIDC client uses `localStorage` by default; journey client step storage uses `sessionStorage` |
1142+
| Per-call config overrides via `StepOptions` | **Major change:** Config is fixed at client creation time. Legacy apps that passed different `tree`, `serverConfig`, or `middleware` per-call must create separate client instances. Only `query` params can vary per-call |
1143+
| `FRUser.logout()` silently swallows errors per-step | `oidcClient.user.logout()` returns structured `LogoutErrorResult` with per-operation error details |

packages/journey-client/src/lib/client.store.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,52 @@ describe('journey-client', () => {
249249
}
250250
});
251251

252+
test('resume_WithPreviousStepInStorage_ForwardsLegacyUrlParams', async () => {
253+
const previousStepPayload: Step = {
254+
callbacks: [{ type: callbackType.RedirectCallback, input: [], output: [] }],
255+
};
256+
mockStorageInstance.get.mockResolvedValue({ step: previousStepPayload });
257+
const nextStepPayload: Step = { authId: 'test-auth-id', callbacks: [] };
258+
setupMockFetch(nextStepPayload);
259+
260+
const client = await journey({ config: mockConfig });
261+
const resumeUrl =
262+
'https://app.com/callback?code=123&state=abc&error=access_denied&errorCode=E1&errorMessage=oops&form_post_entry=fp&nonce=n1&RelayState=rs&responsekey=rk&scope=openid&suspendedId=s1';
263+
await client.resume(resumeUrl, {});
264+
265+
const request = mockFetch.mock.calls[1][0] as Request;
266+
const url = new URL(request.url);
267+
expect(url.searchParams.get('code')).toBe('123');
268+
expect(url.searchParams.get('state')).toBe('abc');
269+
expect(url.searchParams.get('error')).toBe('access_denied');
270+
expect(url.searchParams.get('errorCode')).toBe('E1');
271+
expect(url.searchParams.get('errorMessage')).toBe('oops');
272+
expect(url.searchParams.get('form_post_entry')).toBe('fp');
273+
expect(url.searchParams.get('nonce')).toBe('n1');
274+
expect(url.searchParams.get('RelayState')).toBe('rs');
275+
expect(url.searchParams.get('responsekey')).toBe('rk');
276+
expect(url.searchParams.get('scope')).toBe('openid');
277+
expect(url.searchParams.get('suspendedId')).toBe('s1');
278+
});
279+
280+
test('resume_WithPreviousStepInStorage_AllowsOptionsQueryToOverrideUrlParams', async () => {
281+
const previousStepPayload: Step = {
282+
callbacks: [{ type: callbackType.RedirectCallback, input: [], output: [] }],
283+
};
284+
mockStorageInstance.get.mockResolvedValue({ step: previousStepPayload });
285+
const nextStepPayload: Step = { authId: 'test-auth-id', callbacks: [] };
286+
setupMockFetch(nextStepPayload);
287+
288+
const client = await journey({ config: mockConfig });
289+
const resumeUrl = 'https://app.com/callback?code=123&state=abc';
290+
await client.resume(resumeUrl, { query: { code: 'override' } });
291+
292+
const request = mockFetch.mock.calls[1][0] as Request;
293+
const url = new URL(request.url);
294+
expect(url.searchParams.get('code')).toBe('override');
295+
expect(url.searchParams.get('state')).toBe('abc');
296+
});
297+
252298
test('resume_WithPlainStepObjectInStorage_CorrectlyResumes', async () => {
253299
const plainStepPayload: Step = {
254300
callbacks: [

packages/journey-client/src/lib/client.store.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,17 @@ export async function journey<ActionType extends ActionTypes = ActionTypes>({
206206
resume: async (url: string, options?: ResumeOptions): Promise<JourneyResult> => {
207207
const parsedUrl = new URL(url);
208208
const code = parsedUrl.searchParams.get('code');
209+
const error = parsedUrl.searchParams.get('error');
210+
const errorCode = parsedUrl.searchParams.get('errorCode');
211+
const errorMessage = parsedUrl.searchParams.get('errorMessage');
209212
const state = parsedUrl.searchParams.get('state');
210213
const form_post_entry = parsedUrl.searchParams.get('form_post_entry');
214+
const nonce = parsedUrl.searchParams.get('nonce');
215+
const RelayState = parsedUrl.searchParams.get('RelayState');
211216
const responsekey = parsedUrl.searchParams.get('responsekey');
217+
const scope = parsedUrl.searchParams.get('scope');
218+
const suspendedId = parsedUrl.searchParams.get('suspendedId');
219+
const authIndexValue = parsedUrl.searchParams.get('authIndexValue') ?? undefined;
212220

213221
let previousStep: JourneyStep | undefined;
214222

@@ -247,12 +255,23 @@ export async function journey<ActionType extends ActionTypes = ActionTypes>({
247255
const resumeOptions = {
248256
...options,
249257
query: {
250-
...(options && options.query),
251258
...(code && { code }),
252-
...(state && { state }),
259+
...(error && { error }),
260+
...(errorCode && { errorCode }),
261+
...(errorMessage && { errorMessage }),
253262
...(form_post_entry && { form_post_entry }),
263+
...(nonce && { nonce }),
264+
...(RelayState && { RelayState }),
254265
...(responsekey && { responsekey }),
266+
...(scope && { scope }),
267+
...(state && { state }),
268+
...(suspendedId && { suspendedId }),
269+
270+
...(options && options.query),
255271
},
272+
...((options?.journey ?? authIndexValue) && {
273+
journey: options?.journey ?? authIndexValue,
274+
}),
256275
};
257276

258277
if (previousStep) {

0 commit comments

Comments
 (0)