Skip to content

Commit b917bb2

Browse files
authored
Merge pull request #363 from ForgeRock/oidc-userinfo
feat(oidc-client): improving types and completing userinfo
2 parents 0cef673 + 6c06e70 commit b917bb2

19 files changed

Lines changed: 442 additions & 176 deletions
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@forgerock/oidc-client': minor
3+
'@forgerock/storage': patch
4+
'@forgerock/davinci-client': patch
5+
'@forgerock/sdk-types': patch
6+
---
7+
8+
Implement OIDC logout and user info request; includes type updates and global error type

e2e/oidc-app/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
<link rel="stylesheet" href="/src/styles.css" />
1111

1212
<style>
13-
#logout {
13+
#logout,
14+
#userinfo {
1415
display: none;
1516
}
1617
</style>
@@ -19,6 +20,7 @@
1920
<h1>Welcome</h1>
2021
<button id="login">Login</button>
2122
<button id="logout">Logout</button>
23+
<button id="userinfo">User Info</button>
2224
<script type="module" src="/src/main.ts"></script>
2325
</body>
2426
</html>

e2e/oidc-app/src/main.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,22 @@ async function app() {
5454
} else {
5555
console.log('Token Exchange Response:', tokenResponse);
5656
document.getElementById('logout')!.style.display = 'block';
57+
document.getElementById('userinfo')!.style.display = 'block';
5758
document.getElementById('login')!.style.display = 'none';
5859
}
5960
}
6061
});
6162

63+
document.getElementById('userinfo')?.addEventListener('click', async () => {
64+
const userInfo = await oidcClient.user.info();
65+
66+
if ('error' in userInfo) {
67+
console.error('User Info Error:', userInfo);
68+
} else {
69+
console.log('User Info:', userInfo);
70+
}
71+
});
72+
6273
document.getElementById('logout')?.addEventListener('click', async () => {
6374
const response = await oidcClient.user.logout();
6475

@@ -67,6 +78,7 @@ async function app() {
6778
} else {
6879
console.log('Logout successful');
6980
document.getElementById('logout')!.style.display = 'none';
81+
document.getElementById('userinfo')!.style.display = 'none';
7082
document.getElementById('login')!.style.display = 'block';
7183
}
7284
});

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
*/
77
/* eslint-disable @typescript-eslint/no-unused-vars */
88
import { describe, expectTypeOf, it } from 'vitest';
9+
10+
import type { GenericError } from '@forgerock/sdk-types';
11+
912
import type { InitFlow, InternalErrorResponse, Updater } from './client.types.js';
10-
import type { GenericError } from './error.types.js';
1113
import type { ErrorNode, FailureNode, ContinueNode, StartNode, SuccessNode } from './node.types.js';
12-
import { PhoneNumberInputValue } from './collector.types.js';
14+
import type { PhoneNumberInputValue } from './collector.types.js';
1315

1416
describe('Client Types', () => {
1517
it('should allow function returning error', async () => {
@@ -189,8 +191,10 @@ describe('Updater', () => {
189191

190192
const withoutError: Updater = () => null;
191193

192-
expectTypeOf(withError).returns.toMatchTypeOf<{ error: GenericError } | null>();
193-
expectTypeOf(withoutError).returns.toMatchTypeOf<{ error: GenericError } | null>();
194+
expectTypeOf(withError).returns.toMatchTypeOf<{ error: Omit<GenericError, 'error'> } | null>();
195+
expectTypeOf(withoutError).returns.toMatchTypeOf<{
196+
error: Omit<GenericError, 'error'>;
197+
} | null>();
194198

195199
// Test both are valid Updater types
196200
expectTypeOf(withError).toMatchTypeOf<Updater>();

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
66
*/
7-
import { PhoneNumberInputValue } from './collector.types.js';
8-
import { GenericError } from './error.types.js';
9-
import { ErrorNode, FailureNode, ContinueNode, StartNode, SuccessNode } from './node.types.js';
7+
import type { GenericError } from '@forgerock/sdk-types';
8+
9+
import type { PhoneNumberInputValue } from './collector.types.js';
10+
import type { ErrorNode, FailureNode, ContinueNode, StartNode, SuccessNode } from './node.types.js';
1011

1112
export type FlowNode = ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode;
1213

1314
export interface InternalErrorResponse {
14-
error: GenericError;
15+
error: Omit<GenericError, 'error'> & { message: string };
1516
type: 'internal_error';
1617
}
1718

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
*/
77
/* eslint-disable @typescript-eslint/no-unused-vars */
88
import { describe, expect, it } from 'vitest';
9-
import type { GenericError } from './error.types.js';
9+
10+
import type { GenericError } from '@forgerock/sdk-types';
1011

1112
describe('GenericError type', () => {
1213
it('should allow valid error objects', () => {
13-
const validErrors: GenericError[] = [
14+
const validErrors: Omit<GenericError, 'error'>[] = [
1415
{
1516
message: 'Something went wrong',
1617
type: 'unknown_error',
@@ -48,8 +49,7 @@ describe('GenericError type', () => {
4849

4950
// This test is just for TypeScript compilation validation
5051
it('should enforce required properties', () => {
51-
// @ts-expect-error - message is required
52-
const missingMessage: GenericError = {
52+
const missingMessage: Omit<GenericError, 'error'> = {
5353
type: 'unknown_error',
5454
};
5555

packages/davinci-client/src/lib/error.types.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

packages/davinci-client/src/lib/node.slice.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { getCollectorErrors } from './node.utils.js';
1919
* Import the types
2020
*/
2121
import type { Draft, PayloadAction } from '@reduxjs/toolkit';
22+
2223
import type { SubmitCollector } from './collector.types.js';
23-
import type { GenericError } from './error.types.js';
2424
import type {
2525
DavinciErrorResponse,
2626
DaVinciFailureResponse,
@@ -294,7 +294,7 @@ export const nodeSlice = createSlice({
294294
code: 'unknown',
295295
type: 'state_error',
296296
message: `\`collectors\` are only available on nodes with \`status\` of ${CONTINUE_STATUS} or ${ERROR_STATUS}`,
297-
} as GenericError,
297+
} as const,
298298
state: [],
299299
};
300300
},
@@ -310,7 +310,7 @@ export const nodeSlice = createSlice({
310310
code: 'unknown',
311311
type: 'state_error',
312312
message: `\`collectors\` are only available on nodes with \`status\` of ${CONTINUE_STATUS} or ${ERROR_STATUS}`,
313-
} as GenericError,
313+
} as const,
314314
state: null,
315315
};
316316
},
@@ -329,7 +329,7 @@ export const nodeSlice = createSlice({
329329
code: 'unknown',
330330
type: 'state_error',
331331
message: `\`errorCollectors\` are only available on nodes with \`status\` of ${ERROR_STATUS}`,
332-
} as GenericError,
332+
} as const,
333333
state: [],
334334
};
335335
},

packages/davinci-client/src/lib/node.types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
66
*/
7+
import { GenericError } from '@forgerock/sdk-types';
8+
79
import type {
810
FlowCollector,
911
PasswordCollector,
@@ -23,7 +25,6 @@ import type {
2325
UnknownCollector,
2426
} from './collector.types.js';
2527
import type { Links } from './davinci.types.js';
26-
import { GenericError } from './error.types.js';
2728

2829
export type Collectors =
2930
| FlowCollector
@@ -74,9 +75,10 @@ export interface ContinueNode {
7475
status: 'continue';
7576
}
7677

77-
export interface DaVinciError extends GenericError {
78+
export interface DaVinciError extends Omit<GenericError, 'error'> {
7879
collectors?: CollectorErrors[];
7980
internalHttpStatus?: number;
81+
message: string;
8082
status: 'error' | 'failure' | 'unknown';
8183
}
8284

packages/oidc-client/src/lib/authorize.request.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ import type { WellKnownResponse } from '@forgerock/sdk-types';
2121
import type { OidcConfig } from './config.types.js';
2222
import { AuthorizeErrorResponse, AuthorizeSuccessResponse } from './authorize.request.types.js';
2323

24+
/**
25+
* @function authorizeµ
26+
* @description Creates an authorization URL for the OIDC client.
27+
* @param {WellKnownResponse} wellknown - The well-known configuration for the OIDC server.
28+
* @param {OidcConfig} config - The OIDC client configuration.
29+
* @param {CustomLogger} log - The logger instance for logging debug information.
30+
* @param {GetAuthorizationUrlOptions} options - Optional parameters for the authorization request.
31+
* @returns {Micro.Micro<AuthorizeSuccessResponse, AuthorizeErrorResponse, never>} - A micro effect that resolves to the authorization response.
32+
*/
2433
export async function authorizeµ(
2534
wellknown: WellKnownResponse,
2635
config: OidcConfig,
@@ -37,13 +46,18 @@ export async function authorizeµ(
3746
* If we support the pi.flow field, this means we are using a PingOne server.
3847
* PingOne servers do not support redirection through iframes because they
3948
* set iframe's to DENY.
49+
*
50+
* We do not use RTK Query for this because we don't want caching, or store
51+
* updates, and want the request to be made similar to the iframe method below.
52+
*
53+
* This returns a Micro that resolves to the parsed response JSON.
4054
*/
4155
return authorizeFetchµ(url).pipe(
4256
Micro.flatMap(
4357
(response): Micro.Micro<AuthorizeSuccessResponse, AuthorizeErrorResponse, never> => {
44-
if ('authorizeResponse' in response) {
45-
log.debug('Received authorize response', response.authorizeResponse);
46-
return Micro.succeed(response.authorizeResponse);
58+
if ('code' in response) {
59+
log.debug('Received code in response', response);
60+
return Micro.succeed(response);
4761
}
4862
log.error('Error in authorize response', response);
4963
// For redirection, we need to remore `pi.flow` from the options
@@ -57,6 +71,9 @@ export async function authorizeµ(
5771
/**
5872
* If the response mode is not pi.flow, then we are likely using a traditional
5973
* redirect based server supporting iframes. An example would be PingAM.
74+
*
75+
* This returns a Micro that's either the success URL parameters or error URL
76+
* parameters.
6077
*/
6178
return authorizeIframeµ(url, config).pipe(
6279
Micro.flatMap(

0 commit comments

Comments
 (0)