Skip to content

Commit c056c6a

Browse files
committed
feat(oidc): refactor-wellknown-architecture
refactor well known after discussion SDKS-4665
1 parent 9a8ca14 commit c056c6a

18 files changed

Lines changed: 686 additions & 668 deletions

e2e/journey-app/main.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
*/
77
import './style.css';
88

9-
import { journey, isJourneyClient } from '@forgerock/journey-client';
9+
import { journey } from '@forgerock/journey-client';
1010

11-
import type { RequestMiddleware } from '@forgerock/journey-client/types';
11+
import type { JourneyClient, RequestMiddleware } from '@forgerock/journey-client/types';
1212

1313
import { renderCallbacks } from './callback-map.js';
1414
import { renderQRCodeStep } from './components/qr-code.js';
@@ -40,7 +40,7 @@ if (searchParams.get('middleware') === 'true') {
4040
},
4141
(req, action, next) => {
4242
switch (action.type) {
43-
case 'END_SESSION':
43+
case 'JOURNEY_TERMINATE':
4444
req.url.searchParams.set('end-session-middleware', 'end-session');
4545
req.headers.append('x-end-session-middleware', 'end-session');
4646
break;
@@ -55,19 +55,15 @@ if (searchParams.get('middleware') === 'true') {
5555
const formEl = document.getElementById('form') as HTMLFormElement;
5656
const journeyEl = document.getElementById('journey') as HTMLDivElement;
5757

58-
const journeyClientResult = await journey({ config: config, requestMiddleware });
59-
if (!isJourneyClient(journeyClientResult)) {
60-
console.error('Failed to initialize journey client:', journeyClientResult.message);
61-
errorEl.textContent = journeyClientResult.message ?? 'Unknown error';
58+
let journeyClient: JourneyClient;
59+
try {
60+
journeyClient = await journey({ config: config, requestMiddleware });
61+
} catch (error) {
62+
const message = error instanceof Error ? error.message : 'Unknown error';
63+
console.error('Failed to initialize journey client:', message);
64+
errorEl.textContent = message;
6265
return;
6366
}
64-
/**
65-
* Re-assign to a new const after type narrowing.
66-
* TypeScript's type narrowing doesn't persist into closures (event handlers, callbacks)
67-
* because it can't prove the variable wasn't reassigned between the guard and closure execution.
68-
* Creating a new const binding after the guard preserves the narrowed type for nested functions.
69-
*/
70-
const journeyClient = journeyClientResult;
7167
let step = await journeyClient.start({ journey: journeyName });
7268

7369
function renderComplete() {

e2e/journey-app/server-configs.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,19 @@ import type { JourneyClientConfig } from '@forgerock/journey-client/types';
99
/**
1010
* Server configurations for E2E tests.
1111
*
12-
* Both baseUrl and realmPath are automatically inferred from the wellknown URL:
13-
* - baseUrl: extracted from the path before `/oauth2/`
14-
* - realmPath: extracted from the issuer URL in the wellknown response
12+
* All configuration (baseUrl, authenticate/sessions paths) is automatically
13+
* derived from the well-known response via `convertWellknown()`.
1514
*/
1615
export const serverConfigs: Record<string, JourneyClientConfig> = {
1716
basic: {
1817
serverConfig: {
1918
wellknown: 'http://localhost:9443/am/oauth2/realms/root/.well-known/openid-configuration',
20-
// baseUrl inferred: http://localhost:9443/am/
21-
// realmPath inferred from issuer: 'root'
2219
},
2320
},
2421
tenant: {
2522
serverConfig: {
2623
wellknown:
2724
'https://openam-sdks.forgeblocks.com/am/oauth2/realms/root/realms/alpha/.well-known/openid-configuration',
28-
// baseUrl inferred: https://openam-sdks.forgeblocks.com/am/
29-
// realmPath inferred from issuer: 'alpha'
3025
},
3126
},
3227
};

packages/journey-client/README.md

Lines changed: 72 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,24 @@
44

55
> **Note**: This client is designed specifically for ForgeRock AM servers. For PingOne DaVinci flows, use `@forgerock/davinci-client`. For standard OIDC operations, use `@forgerock/oidc-client`.
66
7+
## Table of Contents
8+
9+
- [Features](#features)
10+
- [Installation](#installation)
11+
- [Quick Start](#quick-start)
12+
- [Configuration](#configuration)
13+
- [API Reference](#api-reference)
14+
- [Working with Callbacks](#working-with-callbacks)
15+
- [Request Middleware](#request-middleware)
16+
- [Error Handling](#error-handling)
17+
- [Building](#building)
18+
- [Testing](#testing)
19+
720
## Features
821

922
- **Wellknown Discovery**: Automatically discovers server configuration from the OIDC wellknown endpoint
23+
- **Automatic Path Derivation**: Derives `baseUrl`, `authenticate`, and `sessions` paths directly from the well-known response
1024
- **Stateful Client**: Manages authentication journey state internally
11-
- **Error-as-Value Pattern**: Returns errors as values instead of throwing, enabling type-safe error handling
1225
- **Callback Handling**: Provides a structured way to interact with various authentication callbacks
1326
- **Redux Toolkit & RTK Query**: Built on modern state management for predictable state and efficient API interactions
1427

@@ -25,115 +38,98 @@ yarn add @forgerock/journey-client
2538
## Quick Start
2639

2740
```typescript
28-
import { journey, isJourneyClient } from '@forgerock/journey-client';
41+
import { journey } from '@forgerock/journey-client';
2942
import { callbackType } from '@forgerock/sdk-types';
3043

3144
async function authenticateUser() {
3245
// Initialize the client with wellknown discovery
33-
const result = await journey({
34-
config: {
35-
serverConfig: {
36-
wellknown: 'https://am.example.com/am/oauth2/alpha/.well-known/openid-configuration',
46+
try {
47+
const client = await journey({
48+
config: {
49+
serverConfig: {
50+
wellknown: 'https://am.example.com/am/oauth2/alpha/.well-known/openid-configuration',
51+
},
3752
},
38-
// realmPath is optional - inferred from wellknown issuer
39-
},
40-
});
41-
42-
// Handle initialization errors using the type guard
43-
if (!isJourneyClient(result)) {
44-
console.error('Failed to initialize:', result.message);
45-
return;
46-
}
53+
});
4754

48-
const client = result;
55+
// Start the authentication journey
56+
let step = await client.start({ journey: 'Login' });
4957

50-
// Start the authentication journey
51-
let step = await client.start({ journey: 'Login' });
58+
// Handle callbacks in a loop until success or failure
59+
while (step?.type === 'Step') {
60+
const nameCallbacks = step.getCallbacksOfType(callbackType.NameCallback);
61+
for (const cb of nameCallbacks) {
62+
cb.setName('demo');
63+
}
5264

53-
// Handle callbacks in a loop until success or failure
54-
while (step?.type === 'Step') {
55-
// Handle NameCallback
56-
const nameCallbacks = step.getCallbacksOfType(callbackType.NameCallback);
57-
for (const cb of nameCallbacks) {
58-
cb.setName('demo');
59-
}
65+
const passwordCallbacks = step.getCallbacksOfType(callbackType.PasswordCallback);
66+
for (const cb of passwordCallbacks) {
67+
cb.setPassword('password');
68+
}
6069

61-
// Handle PasswordCallback
62-
const passwordCallbacks = step.getCallbacksOfType(callbackType.PasswordCallback);
63-
for (const cb of passwordCallbacks) {
64-
cb.setPassword('password');
70+
step = await client.next(step);
6571
}
6672

67-
// Submit and get next step
68-
step = await client.next(step);
69-
}
70-
71-
// Check the final result
72-
if (step?.type === 'LoginSuccess') {
73-
console.log('Login successful!', step.getSessionToken());
74-
} else if (step?.type === 'LoginFailure') {
75-
console.error('Login failed:', step.payload.message);
73+
// Check the final result
74+
if (step?.type === 'LoginSuccess') {
75+
console.log('Login successful!', step.getSessionToken());
76+
} else if (step?.type === 'LoginFailure') {
77+
console.error('Login failed:', step.payload.message);
78+
}
79+
} catch (error) {
80+
console.error('Failed to initialize client:', error);
7681
}
7782
}
7883
```
7984

8085
## Configuration
8186

82-
The client uses OIDC wellknown discovery to automatically configure itself:
87+
The client requires only the OIDC wellknown endpoint URL. All other configuration is derived automatically from the well-known response:
8388

8489
```typescript
8590
import type { JourneyClientConfig } from '@forgerock/journey-client/types';
8691

8792
const config: JourneyClientConfig = {
8893
serverConfig: {
89-
// Required: OIDC discovery endpoint
9094
wellknown: 'https://am.example.com/am/oauth2/alpha/.well-known/openid-configuration',
91-
// Optional: Custom path overrides
92-
paths: {
93-
authenticate: '/custom/authenticate',
94-
},
95-
// Optional: Request timeout in milliseconds
96-
timeout: 30000,
9795
},
98-
// Optional: Realm path (inferred from wellknown issuer if not provided)
99-
realmPath: 'alpha',
10096
};
10197
```
10298

103-
### Automatic Inference
99+
### Automatic Derivation
104100

105-
The client automatically infers configuration from the wellknown URL:
101+
The client automatically derives all needed configuration from the well-known response:
106102

107-
| Property | Inferred From |
108-
| ----------- | ----------------------------------------------------- |
109-
| `baseUrl` | Extracted from wellknown URL path (before `/oauth2/`) |
110-
| `realmPath` | Extracted from the `issuer` in the wellknown response |
103+
| Property | Derived From |
104+
| -------------- | -------------------------------------------------------------------- |
105+
| `baseUrl` | `authorization_endpoint` origin |
106+
| `authenticate` | Issuer path with `/oauth2` replaced by `/json`, plus `/authenticate` |
107+
| `sessions` | Issuer path with `/oauth2` replaced by `/json`, plus `/sessions/` |
111108

112109
## API Reference
113110

114111
### `journey(options)`
115112

116-
Factory function that creates a journey client instance.
113+
Factory function that creates a journey client instance. Throws on initialization failure (invalid URL, fetch error, non-AM server).
117114

118115
```typescript
119-
const result = await journey({
116+
const client = await journey({
120117
config: JourneyClientConfig,
121118
requestMiddleware?: RequestMiddleware[],
122119
logger?: { level: LogLevel; custom?: CustomLogger },
123120
});
124121
```
125122

126-
**Returns**: `Promise<JourneyClient | GenericError>`
123+
**Returns**: `Promise<JourneyClient>`
127124

128-
Use the `isJourneyClient()` type guard to narrow the result:
125+
**Throws**: `Error` if the wellknown URL is invalid, the fetch fails, or the server is not a ForgeRock AM instance.
129126

130127
```typescript
131-
if (!isJourneyClient(result)) {
132-
// result is GenericError
133-
console.error(result.error, result.message);
134-
return;
128+
try {
129+
const client = await journey({ config });
130+
} catch (error) {
131+
console.error('Initialization failed:', error.message);
135132
}
136-
// result is JourneyClient
137133
```
138134

139135
### Client Methods
@@ -223,37 +219,31 @@ const loggingMiddleware: RequestMiddleware = (req, action, next) => {
223219
next();
224220
};
225221

226-
const result = await journey({
222+
const client = await journey({
227223
config,
228224
requestMiddleware: [loggingMiddleware],
229225
});
230226
```
231227

232228
### Middleware Actions
233229

234-
| Action Type | Description |
235-
| --------------- | ----------------------- |
236-
| `JOURNEY_START` | Starting a new journey |
237-
| `JOURNEY_NEXT` | Submitting a step |
238-
| `END_SESSION` | Terminating the session |
230+
| Action Type | Description |
231+
| ------------------- | ----------------------- |
232+
| `JOURNEY_START` | Starting a new journey |
233+
| `JOURNEY_NEXT` | Submitting a step |
234+
| `JOURNEY_TERMINATE` | Terminating the session |
239235

240236
## Error Handling
241237

242-
The client uses an error-as-value pattern instead of throwing exceptions:
238+
The `journey()` factory throws on initialization failure. Use try/catch:
243239

244240
```typescript
245-
const result = await journey({ config });
246-
247-
if (!isJourneyClient(result)) {
248-
// Handle initialization error
249-
switch (result.type) {
250-
case 'wellknown_error':
251-
console.error('Configuration error:', result.message);
252-
break;
253-
default:
254-
console.error('Unknown error:', result.error);
255-
}
256-
return;
241+
try {
242+
const client = await journey({ config });
243+
// client is guaranteed to be a JourneyClient
244+
} catch (error) {
245+
// Handle initialization errors (invalid URL, fetch failure, non-AM server)
246+
console.error('Failed to initialize:', error.message);
257247
}
258248
```
259249

0 commit comments

Comments
 (0)