Skip to content

Commit e58f29a

Browse files
committed
feat(journey-client): wellknown-endpoint-config-support
Re-implement well known configuration and abstract into reusable package
1 parent 296c8c2 commit e58f29a

41 files changed

Lines changed: 1095 additions & 305 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/rich-cows-try.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
'@forgerock/journey-client': minor
3+
'@forgerock/sdk-oidc': minor
4+
'@forgerock/sdk-utilities': minor
5+
'@forgerock/davinci-client': patch
6+
'@forgerock/oidc-client': patch
7+
---
8+
9+
### @forgerock/journey-client
10+
11+
Add well-known OIDC endpoint discovery support. The journey client can now fetch configuration from the `.well-known/openid-configuration` endpoint:
12+
13+
```typescript
14+
const client = await journey({
15+
serverConfig: {
16+
baseUrl: 'https://am.example.com/am/',
17+
wellknown:
18+
'https://am.example.com/am/oauth2/realms/root/realms/alpha/.well-known/openid-configuration',
19+
},
20+
});
21+
```
22+
23+
The realm path can be automatically inferred from the well-known issuer URL.
24+
25+
### @forgerock/sdk-oidc
26+
27+
Add shared well-known module with RTK Query API for OIDC endpoint discovery:
28+
29+
- `wellknownApi` - RTK Query API for fetching well-known configuration
30+
- `createWellknownSelector` - Selector factory for cached well-known data
31+
- `createWellknownError` - Typed error creation from fetch failures
32+
- Re-exports pure utilities from `@forgerock/sdk-utilities`
33+
34+
### @forgerock/sdk-utilities
35+
36+
Add pure well-known utilities:
37+
38+
- `inferRealmFromIssuer` - Extract realm path from AM issuer URLs
39+
- `isValidWellknownUrl` - Validate well-known URLs (HTTPS required, HTTP allowed for localhost)
40+
41+
### @forgerock/davinci-client
42+
43+
Refactored to use shared well-known module from `@forgerock/sdk-oidc`.
44+
45+
### @forgerock/oidc-client
46+
47+
Refactored to use shared well-known module from `@forgerock/sdk-oidc`.

e2e/am-mock-api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@
1616
"uuid": "^13.0.0"
1717
},
1818
"devDependencies": {
19-
"@types/express": "^4.17.17"
19+
"@types/express": "^5.0.0"
2020
}
2121
}

e2e/mock-api-v2/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"nanoid": "5.1.6"
2727
},
2828
"devDependencies": {
29-
"@effect/vitest": "catalog:effect"
29+
"@effect/vitest": "catalog:effect",
30+
"vitest": "catalog:vitest"
3031
},
3132
"nx": {
3233
"tags": ["scope:e2e"],

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { createClientStore, handleUpdateValidateError, RootState } from './clien
1515
import { nodeSlice } from './node.slice.js';
1616
import { davinciApi } from './davinci.api.js';
1717
import { configSlice } from './config.slice.js';
18-
import { wellknownApi } from './wellknown.api.js';
18+
import { wellknownApi, createWellknownError } from '@forgerock/sdk-oidc';
1919

2020
import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware';
2121
/**
@@ -88,14 +88,14 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({
8888
throw error;
8989
}
9090

91-
const { data: openIdResponse } = await store.dispatch(
92-
wellknownApi.endpoints.wellknown.initiate(config.serverConfig.wellknown),
91+
const { data: openIdResponse, error: fetchError } = await store.dispatch(
92+
wellknownApi.endpoints.configuration.initiate(config.serverConfig.wellknown),
9393
);
9494

95-
if (!openIdResponse) {
96-
const error = new Error('error fetching `wellknown` response for OpenId Configuration');
97-
log.error(error.message);
98-
throw error;
95+
if (fetchError || !openIdResponse) {
96+
const genericError = createWellknownError(fetchError);
97+
log.error(`${genericError.error}: ${genericError.message}`);
98+
throw new Error(genericError.message);
9999
}
100100

101101
store.dispatch(configSlice.actions.set({ ...config, wellknownResponse: openIdResponse }));
@@ -249,11 +249,11 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({
249249

250250
return node;
251251
} catch (err) {
252-
const error = err as Error;
253-
log.error(error.message);
252+
const errorMessage = err instanceof Error ? err.message : String(err);
253+
log.error(errorMessage);
254254
return {
255255
error: {
256-
message: error.message ?? 'An unexpected error occurred during resume operation',
256+
message: errorMessage || 'An unexpected error occurred during resume operation',
257257
type: 'internal_error',
258258
},
259259
type: 'internal_error',
@@ -336,10 +336,10 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({
336336
store.dispatch(nodeSlice.actions.update({ id, value, index }));
337337
return null;
338338
} catch (err) {
339-
const error = err as Error;
339+
const errorMessage = err instanceof Error ? err.message : String(err);
340340
return {
341341
type: 'internal_error',
342-
error: { message: error.message, type: 'internal_error' },
342+
error: { message: errorMessage, type: 'internal_error' },
343343
};
344344
}
345345
};

packages/davinci-client/src/lib/client.store.utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { configSlice } from './config.slice.js';
1313
import { nodeSlice } from './node.slice.js';
1414
import { davinciApi } from './davinci.api.js';
1515
import { ErrorNode, ContinueNode, StartNode, SuccessNode } from '../types.js';
16-
import { wellknownApi } from './wellknown.api.js';
16+
import { wellknownApi } from '@forgerock/sdk-oidc';
1717
import { InternalErrorResponse } from './client.types.js';
1818

1919
export function createClientStore<ActionType extends ActionTypes>({

packages/davinci-client/src/lib/config.types.test-d.ts

Lines changed: 32 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
*/
77
import { describe, expectTypeOf, it } from 'vitest';
88
import type { DaVinciConfig, InternalDaVinciConfig } from './config.types.js';
9-
import type { AsyncLegacyConfigOptions } from '@forgerock/sdk-types';
10-
import type { WellknownResponse } from './wellknown.types.js';
9+
import type { AsyncLegacyConfigOptions, WellknownResponse } from '@forgerock/sdk-types';
1110

1211
describe('Config Types', () => {
1312
describe('DaVinciConfig', () => {
@@ -51,30 +50,9 @@ describe('Config Types', () => {
5150
authorization_endpoint: 'https://example.com/auth',
5251
token_endpoint: 'https://example.com/token',
5352
userinfo_endpoint: 'https://example.com/userinfo',
54-
jwks_uri: 'https://example.com/jwks',
55-
revocation_endpoint: 'https://example.com/register',
5653
end_session_endpoint: 'https://example.com/logout',
57-
pushed_authorization_request_endpoint: '',
58-
check_session_iframe: '',
59-
introspection_endpoint: '',
60-
device_authorization_endpoint: '',
61-
claims_parameter_supported: '',
62-
request_parameter_supported: '',
63-
request_uri_parameter_supported: '',
64-
require_pushed_authorization_requests: '',
65-
scopes_supported: [],
66-
response_types_supported: [],
67-
response_modes_supported: [],
68-
grant_types_supported: [],
69-
subject_types_supported: [],
70-
id_token_signing_alg_values_supported: [],
71-
userinfo_signing_alg_values_supported: [],
72-
request_object_signing_alg_values_supported: [],
73-
token_endpoint_auth_methods_supported: [],
74-
token_endpoint_auth_signing_alg_values_supported: [],
75-
claim_types_supported: [],
76-
claims_supported: [],
77-
code_challenge_methods_supported: [],
54+
introspection_endpoint: 'https://example.com/introspect',
55+
revocation_endpoint: 'https://example.com/revoke',
7856
},
7957
responseType: 'code',
8058
serverConfig: {},
@@ -85,7 +63,6 @@ describe('Config Types', () => {
8563

8664
it('should combine DaVinciConfig and wellknownResponse', () => {
8765
const config: InternalDaVinciConfig = {
88-
// DaVinciConfig properties
8966
clientId: 'test-client',
9067
scope: 'openid profile',
9168
serverConfig: {
@@ -94,36 +71,16 @@ describe('Config Types', () => {
9471
},
9572
redirectUri: 'https://app.example.com/callback',
9673
responseType: 'code',
97-
// InternalDaVinciConfig specific property
9874
wellknownResponse: {
9975
issuer: 'https://example.com',
10076
authorization_endpoint: 'https://example.com/auth',
10177
token_endpoint: 'https://example.com/token',
10278
userinfo_endpoint: 'https://example.com/userinfo',
103-
jwks_uri: 'https://example.com/jwks',
104-
revocation_endpoint: 'https://example.com/revoke',
10579
end_session_endpoint: 'https://example.com/logout',
106-
pushed_authorization_request_endpoint: '',
107-
check_session_iframe: '',
108-
introspection_endpoint: '',
109-
device_authorization_endpoint: '',
110-
claims_parameter_supported: '',
111-
request_parameter_supported: '',
112-
request_uri_parameter_supported: '',
113-
require_pushed_authorization_requests: '',
114-
scopes_supported: [],
115-
response_types_supported: [],
116-
response_modes_supported: [],
117-
grant_types_supported: [],
118-
subject_types_supported: [],
119-
id_token_signing_alg_values_supported: [],
120-
userinfo_signing_alg_values_supported: [],
121-
request_object_signing_alg_values_supported: [],
122-
token_endpoint_auth_methods_supported: [],
123-
token_endpoint_auth_signing_alg_values_supported: [],
124-
claim_types_supported: [],
125-
claims_supported: [],
126-
code_challenge_methods_supported: [],
80+
introspection_endpoint: 'https://example.com/introspect',
81+
revocation_endpoint: 'https://example.com/revoke',
82+
jwks_uri: 'https://example.com/jwks',
83+
scopes_supported: ['openid', 'profile'],
12784
},
12885
};
12986
expectTypeOf(config).toMatchTypeOf<InternalDaVinciConfig>();
@@ -132,45 +89,24 @@ describe('Config Types', () => {
13289
});
13390

13491
describe('WellknownResponse', () => {
135-
it('should have all required OIDC properties', () => {
92+
it('should have required OIDC properties', () => {
13693
const wellknown: WellknownResponse = {
13794
issuer: 'https://example.com',
13895
authorization_endpoint: 'https://example.com/auth',
13996
token_endpoint: 'https://example.com/token',
14097
userinfo_endpoint: 'https://example.com/userinfo',
141-
jwks_uri: 'https://example.com/jwks',
142-
revocation_endpoint: 'https://example.com/revoke',
14398
end_session_endpoint: 'https://example.com/logout',
144-
pushed_authorization_request_endpoint: '',
145-
check_session_iframe: '',
146-
introspection_endpoint: '',
147-
device_authorization_endpoint: '',
148-
claims_parameter_supported: '',
149-
request_parameter_supported: '',
150-
request_uri_parameter_supported: '',
151-
require_pushed_authorization_requests: '',
152-
scopes_supported: [],
153-
response_types_supported: [],
154-
response_modes_supported: [],
155-
grant_types_supported: [],
156-
subject_types_supported: [],
157-
id_token_signing_alg_values_supported: [],
158-
userinfo_signing_alg_values_supported: [],
159-
request_object_signing_alg_values_supported: [],
160-
token_endpoint_auth_methods_supported: [],
161-
token_endpoint_auth_signing_alg_values_supported: [],
162-
claim_types_supported: [],
163-
claims_supported: [],
164-
code_challenge_methods_supported: [],
99+
introspection_endpoint: 'https://example.com/introspect',
100+
revocation_endpoint: 'https://example.com/revoke',
165101
};
166102

167103
expectTypeOf<WellknownResponse>().toHaveProperty('issuer').toBeString();
168104
expectTypeOf<WellknownResponse>().toHaveProperty('authorization_endpoint').toBeString();
169105
expectTypeOf<WellknownResponse>().toHaveProperty('token_endpoint').toBeString();
170106
expectTypeOf<WellknownResponse>().toHaveProperty('userinfo_endpoint').toBeString();
171-
expectTypeOf<WellknownResponse>().toHaveProperty('jwks_uri').toBeString();
172-
expectTypeOf<WellknownResponse>().toHaveProperty('revocation_endpoint').toBeString();
173107
expectTypeOf<WellknownResponse>().toHaveProperty('end_session_endpoint').toBeString();
108+
expectTypeOf<WellknownResponse>().toHaveProperty('introspection_endpoint').toBeString();
109+
expectTypeOf<WellknownResponse>().toHaveProperty('revocation_endpoint').toBeString();
174110

175111
expectTypeOf(wellknown).toMatchTypeOf<WellknownResponse>();
176112
});
@@ -181,93 +117,58 @@ describe('WellknownResponse', () => {
181117
authorization_endpoint: 'https://example.com/auth',
182118
token_endpoint: 'https://example.com/token',
183119
userinfo_endpoint: 'https://example.com/userinfo',
184-
jwks_uri: 'https://example.com/jwks',
185-
revocation_endpoint: 'https://example.com/revoke',
186120
end_session_endpoint: 'https://example.com/logout',
187-
// Optional properties
121+
introspection_endpoint: 'https://example.com/introspect',
122+
revocation_endpoint: 'https://example.com/revoke',
123+
jwks_uri: 'https://example.com/jwks',
188124
scopes_supported: ['openid', 'profile', 'email'],
189125
response_types_supported: ['code', 'token'],
190126
grant_types_supported: ['authorization_code', 'refresh_token'],
191127
subject_types_supported: ['public'],
192128
id_token_signing_alg_values_supported: ['RS256'],
193129
token_endpoint_auth_methods_supported: ['client_secret_basic'],
194-
pushed_authorization_request_endpoint: '',
195-
check_session_iframe: '',
196-
introspection_endpoint: '',
197-
device_authorization_endpoint: '',
198-
claims_parameter_supported: '',
199-
request_parameter_supported: '',
200-
request_uri_parameter_supported: '',
201-
require_pushed_authorization_requests: '',
202-
response_modes_supported: [],
203-
userinfo_signing_alg_values_supported: [],
204-
request_object_signing_alg_values_supported: [],
205-
token_endpoint_auth_signing_alg_values_supported: [],
206-
claim_types_supported: [],
207-
claims_supported: [],
208-
code_challenge_methods_supported: [],
209130
};
210131

211-
// Test optional properties are allowed but not required
212132
expectTypeOf<WellknownResponse>().toHaveProperty('scopes_supported');
213133
expectTypeOf<WellknownResponse>().toHaveProperty('response_types_supported');
214134
expectTypeOf<WellknownResponse>().toHaveProperty('grant_types_supported');
135+
expectTypeOf<WellknownResponse>().toHaveProperty('jwks_uri');
215136

216137
expectTypeOf(wellknownWithOptionals).toMatchTypeOf<WellknownResponse>();
217138
});
218139

219-
it('should validate property types', () => {
220-
// Test that array properties must contain strings
221-
expectTypeOf<WellknownResponse['scopes_supported']>().toEqualTypeOf<string[]>();
222-
expectTypeOf<WellknownResponse['response_types_supported']>().toEqualTypeOf<string[]>();
223-
expectTypeOf<WellknownResponse['grant_types_supported']>().toEqualTypeOf<string[]>();
224-
expectTypeOf<WellknownResponse['subject_types_supported']>().toEqualTypeOf<string[]>();
140+
it('should validate optional array property types', () => {
141+
expectTypeOf<WellknownResponse['scopes_supported']>().toEqualTypeOf<string[] | undefined>();
142+
expectTypeOf<WellknownResponse['response_types_supported']>().toEqualTypeOf<
143+
string[] | undefined
144+
>();
145+
expectTypeOf<WellknownResponse['grant_types_supported']>().toEqualTypeOf<
146+
string[] | undefined
147+
>();
148+
expectTypeOf<WellknownResponse['subject_types_supported']>().toEqualTypeOf<
149+
string[] | undefined
150+
>();
225151
expectTypeOf<WellknownResponse['id_token_signing_alg_values_supported']>().toEqualTypeOf<
226-
string[]
152+
string[] | undefined
227153
>();
228154
expectTypeOf<WellknownResponse['token_endpoint_auth_methods_supported']>().toEqualTypeOf<
229-
string[]
155+
string[] | undefined
230156
>();
231157
});
232158

233-
it('should enforce URL format for endpoint properties', () => {
159+
it('should enforce URL format for required endpoint properties', () => {
234160
const wellknown: WellknownResponse = {
235161
issuer: 'https://example.com',
236162
authorization_endpoint: 'https://example.com/auth',
237163
token_endpoint: 'https://example.com/token',
238164
userinfo_endpoint: 'https://example.com/userinfo',
239-
jwks_uri: 'https://example.com/jwks',
240-
revocation_endpoint: 'https://example.com/register',
241165
end_session_endpoint: 'https://example.com/logout',
242-
pushed_authorization_request_endpoint: '',
243-
check_session_iframe: '',
244-
introspection_endpoint: '',
245-
device_authorization_endpoint: '',
246-
claims_parameter_supported: '',
247-
request_parameter_supported: '',
248-
request_uri_parameter_supported: '',
249-
require_pushed_authorization_requests: '',
250-
scopes_supported: [],
251-
response_types_supported: [],
252-
response_modes_supported: [],
253-
grant_types_supported: [],
254-
subject_types_supported: [],
255-
id_token_signing_alg_values_supported: [],
256-
userinfo_signing_alg_values_supported: [],
257-
request_object_signing_alg_values_supported: [],
258-
token_endpoint_auth_methods_supported: [],
259-
token_endpoint_auth_signing_alg_values_supported: [],
260-
claim_types_supported: [],
261-
claims_supported: [],
262-
code_challenge_methods_supported: [],
166+
introspection_endpoint: 'https://example.com/introspect',
167+
revocation_endpoint: 'https://example.com/revoke',
263168
};
264169

265-
// Type assertion to ensure all endpoint properties are strings (URLs)
266170
expectTypeOf(wellknown.authorization_endpoint).toBeString();
267171
expectTypeOf(wellknown.token_endpoint).toBeString();
268172
expectTypeOf(wellknown.userinfo_endpoint).toBeString();
269-
expectTypeOf(wellknown.jwks_uri).toBeString();
270-
expectTypeOf(wellknown.revocation_endpoint).toBeString();
271-
expectTypeOf(wellknown.end_session_endpoint).toBeString();
272173
});
273174
});

0 commit comments

Comments
 (0)