Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion e2e/device-client-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
},
"dependencies": {
"@forgerock/device-client": "workspace:*",
"@forgerock/javascript-sdk": "catalog:",
"@forgerock/journey-client": "workspace:*",
"@forgerock/oidc-client": "workspace:*",
"@forgerock/sdk-types": "workspace:*",
"effect": "catalog:effect"
},
"devDependencies": {
Expand Down
165 changes: 103 additions & 62 deletions e2e/device-client-app/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,66 @@
/*
*
* Copyright © 2025 Ping Identity Corporation. All right reserved.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyright should be 2025 - 2026

*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*
*/

import { deviceClient } from '@forgerock/device-client';
import type { ConfigOptions, DeviceClient } from '@forgerock/device-client/types';
import {
CallbackType,
Config,
FRAuth,
FRLoginFailure,
FRLoginSuccess,
FRStep,
callbackType,
journey,
NameCallback,
PasswordCallback,
SessionManager,
TokenManager,
UserManager,
} from '@forgerock/javascript-sdk';
StepType,
} from '@forgerock/journey-client';
import type {
JourneyClient,
JourneyClientConfig,
JourneyResult,
} from '@forgerock/journey-client/types';
import { oidc } from '@forgerock/oidc-client';
import type { OidcClient, OidcConfig } from '@forgerock/oidc-client/types';
import { Console, Effect } from 'effect';

const logout = Effect.ignore(
Effect.tryPromise({
try: () => SessionManager.logout(),
catch: (err) => new Error(`Logout failed: ${err}`),
}),
);
let cachedOidcClient: OidcClient | null = null;

const start = Effect.tryPromise({
try: () => FRAuth.start(),
catch: (err) => new Error(`Authentication start failed: ${err}`),
}).pipe(Effect.tap((step) => Console.log('Called start', step)));
const oidcClientOrThrow = (): OidcClient => {
if (!cachedOidcClient) {
throw new Error('OIDC client not initialized');
}
return cachedOidcClient;
};

const checkFRStep = (step: FRStep | FRLoginFailure | FRLoginSuccess) =>
const checkForStep = (step: JourneyResult) =>
Effect.try({
try: () => {
if (step.type == 'LoginSuccess' || step.type == 'LoginFailure') {
throw new Error(`Unexpected step type: ${step.type}`);
} else {
if (step && 'type' in step && step.type === StepType.Step) {
return step;
}
throw new Error(`Unexpected step type: ${JSON.stringify(step)}`);
},
catch: (err) => new Error(`Failed to start authentication: ${err}`),
});

const callNext = (step: FRStep) =>
const callNext = (client: JourneyClient, step: JourneyResult) =>
Effect.tryPromise({
try: () => FRAuth.next(step),
try: () => client.next(step as Parameters<JourneyClient['next']>[0]),
Comment on lines +48 to +50

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another nitpick but I don't think we need this cast either if you type the step parameter as JourneyStep

catch: (err) => new Error(`Failed to proceed to next step: ${err}`),
}).pipe(Effect.tap((step) => Console.log('Got next step', step)));

const getTokens = Effect.tryPromise({
try: () => TokenManager.getTokens(),
catch: (err) => new Error(`Failed to get tokens: ${err}`),
}).pipe(Effect.tap((tokens) => Console.log('Got Tokens', tokens)));
}).pipe(Effect.tap((next) => Console.log('Got next step', next)));

const checkForLoginSuccess = (step: FRStep | FRLoginSuccess | FRLoginFailure) => {
if (step.type === 'LoginSuccess') {
return Effect.succeed(step);
} else if (step.type === 'LoginFailure') {
const checkForLoginSuccess = (result: JourneyResult) => {
if (result && 'type' in result && result.type === StepType.LoginSuccess) {
return Effect.succeed(result);
} else if (result && 'type' in result && result.type === StepType.LoginFailure) {
return Effect.fail(new Error(`Login failed`));
} else {
return Effect.fail(
new Error(`Unexpected step, expected to be in a LoginSuccess but got ${step.type}`),
new Error(
`Unexpected step, expected to be in a LoginSuccess but got ${JSON.stringify(result)}`,
),
);
}
};
Expand All @@ -66,7 +69,6 @@ export const LoginAndGetClient = Effect.gen(function* () {
const url = new URL(window.location.href);
const amUrl = url.searchParams.get('amUrl') || 'https://openam-sdks.forgeblocks.com/am';
const realmPath = url.searchParams.get('realmPath') || 'alpha';
const platformHeader = url.searchParams.get('platformHeader') === 'true' ? true : false;
const tree = url.searchParams.get('tree') || 'selfservice';

/**
Expand All @@ -77,53 +79,92 @@ export const LoginAndGetClient = Effect.gen(function* () {
const un = url.searchParams.get('un') || 'devicetestuser';
const pw = url.searchParams.get('pw') || 'password';

const config: ConfigOptions = {
const deviceConfig: ConfigOptions = {
realmPath,
serverConfig: {
baseUrl: amUrl,
timeout: 3000,
},
};

yield* Effect.try(() =>
Config.set({
platformHeader,
realmPath,
tree,
clientId: 'WebOAuthClient',
scope: 'profile email me.read openid',
redirectUri: `${window.location.origin}/src/_callback/index.html`,
serverConfig: {
baseUrl: amUrl,
timeout: 3000,
},
}),
);
yield* logout;
const realmSegment = realmPath ? `/realms/root/realms/${realmPath}` : '';
const wellknown = `${amUrl.replace(/\/$/, '')}/oauth2${realmSegment}/.well-known/openid-configuration`;
const redirectUri = `${window.location.origin}/src/_callback/index.html`;

const journeyConfig: JourneyClientConfig = {
serverConfig: {
wellknown,
},
};

const oidcConfig: OidcConfig = {
clientId: 'WebOAuthClient',
scope: 'profile email me.read openid',
redirectUri,
serverConfig: {
wellknown,
},
};

const journeyClient = yield* Effect.tryPromise({
try: () => journey({ config: journeyConfig }),
catch: (err) => new Error(`Failed to initialize journey client: ${err}`),
});

const oidcClient = yield* Effect.tryPromise({
try: () => oidc({ config: oidcConfig }),
catch: (err) => new Error(`Failed to initialize OIDC client: ${err}`),
});

if ('error' in oidcClient) {
return yield* Effect.fail(new Error(`Failed to initialize OIDC client: ${oidcClient.error}`));
}

cachedOidcClient = oidcClient;

yield* Effect.tryPromise({
try: () => oidcClientOrThrow().user.logout(),
catch: (err) => new Error(`Logout failed: ${err}`),
});
Comment on lines +125 to +128

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this? If we are starting the app from a fresh state then won't this always fail preventing you from proceeding?


yield* start.pipe(
Effect.flatMap((step) => checkFRStep(step)),
yield* Effect.tryPromise({
try: () => journeyClient.start({ journey: tree }),
catch: (err) => new Error(`Authentication start failed: ${err}`),
}).pipe(
Effect.tap((step) => Console.log('Called start', step)),
Effect.flatMap((step) => checkForStep(step)),
Effect.map((step) => {
step.getCallbackOfType<NameCallback>(CallbackType.NameCallback).setName(un);
step.getCallbackOfType<PasswordCallback>(CallbackType.PasswordCallback).setPassword(pw);
step.getCallbackOfType<NameCallback>(callbackType.NameCallback).setName(un);
step.getCallbackOfType<PasswordCallback>(callbackType.PasswordCallback).setPassword(pw);

return step;
}),
Effect.flatMap((step) => callNext(step)),
Effect.flatMap((step) => callNext(journeyClient, step)),
/**
* Don't explicitly need this but if the journey changes
* maybe we dont get a LoginSuccess
*/
Effect.flatMap((step) => checkForLoginSuccess(step)),
Effect.flatMap(() => getTokens),
Effect.flatMap(() =>
Effect.tryPromise({
try: () => oidcClientOrThrow().token.get({ backgroundRenew: true }),
catch: (err) => new Error(`Failed to get tokens: ${err}`),
}).pipe(Effect.tap((tokens) => Console.log('Got Tokens', tokens))),
),
);

const client: DeviceClient = deviceClient(config);
const client: DeviceClient = deviceClient(deviceConfig);
return client;
});

export const getUser = Effect.tryPromise({
try: () => UserManager.getCurrentUser() as Promise<Record<string, string>>,
try: async () => {
const response = await oidcClientOrThrow().user.info();
if (response && 'error' in response) {
throw new Error(`Failed to get user info: ${response.error}`);
}
return response as unknown as Record<string, string>;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick but I don't think we need this cast. We have a better typed user response in the new oidc client. The device app only uses user.sub which I think is guaranteed from the new type.

},
catch: (err) => new Error(`Failed to get current user: ${err}`),
});

Expand Down
9 changes: 9 additions & 0 deletions e2e/device-client-app/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@
"references": [
{
"path": "../../packages/device-client/tsconfig.lib.json"
},
{
"path": "../../packages/journey-client/tsconfig.lib.json"
},
{
"path": "../../packages/oidc-client/tsconfig.lib.json"
},
{
"path": "../../packages/sdk-types/tsconfig.lib.json"
}
]
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,15 @@
"rollup": "^4.59.0",
"picomatch@>=4": "^4.0.4",
"picomatch@<3": "^2.3.2",
"fast-uri": "^3.1.6",
"fast-uri": "^3.1.7",
"qs": "^6.16.0",
"@opentelemetry/core": "^2.8.0",
"brace-expansion@<2": "~1.1.15",
"brace-expansion@>=2 <3": "~2.1.1",
"brace-expansion@>=3 <4": "~3.0.2",
"brace-expansion@>=4": "~5.0.8",
"ws": "^8.21.1",
"undici": "^7.29.0",
"undici": "^7.29.1",
"nanoid": "^5.1.16",
"postcss": "^8.5.23"
},
Expand Down
5 changes: 3 additions & 2 deletions packages/device-client/api-report/device-client.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

```ts

import { ConfigOptions } from '@forgerock/javascript-sdk';
import type { LegacyConfigOptions } from '@forgerock/sdk-types';

// @public (undocumented)
export type Bluetooth = {
Expand All @@ -21,7 +21,8 @@ export type Browser = {
userAgent: string;
};

export { ConfigOptions }
// @public (undocumented)
export type ConfigOptions = LegacyConfigOptions;

// @public (undocumented)
export type DeleteDeviceQuery = {
Expand Down
5 changes: 3 additions & 2 deletions packages/device-client/api-report/device-client.types.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

```ts

import { ConfigOptions } from '@forgerock/javascript-sdk';
import type { LegacyConfigOptions } from '@forgerock/sdk-types';

// @public (undocumented)
export type Bluetooth = {
Expand All @@ -21,7 +21,8 @@ export type Browser = {
userAgent: string;
};

export { ConfigOptions }
// @public (undocumented)
export type ConfigOptions = LegacyConfigOptions;

// @public (undocumented)
export type DeleteDeviceQuery = {
Expand Down
2 changes: 1 addition & 1 deletion packages/device-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"test:watch": "pnpm nx nxTest --watch"
},
"dependencies": {
"@forgerock/javascript-sdk": "catalog:",
"@forgerock/sdk-types": "workspace:*",
"@reduxjs/toolkit": "catalog:"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions packages/device-client/src/lib/device.store.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/*
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
* Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
import { type ConfigOptions } from '@forgerock/javascript-sdk';
import { type ConfigOptions } from './types/index.js';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a circular dependency? Should we import it from sdk-types package instead?

import type { LegacyConfigOptions as ConfigOptions } from '@forgerock/sdk-types';

import { configureStore } from '@reduxjs/toolkit';
import { deviceService } from './services/index.js';
import type { OathDevice, RetrieveOathQuery } from './types/oath.types.js';
Expand Down
6 changes: 4 additions & 2 deletions packages/device-client/src/lib/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
* Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
Expand All @@ -9,7 +9,9 @@ import { deviceClient } from '../device.store.js';
export type DeviceClient = ReturnType<typeof deviceClient>;

// Re-export types from external dependencies that consumers need
export type { ConfigOptions } from '@forgerock/javascript-sdk';
import type { LegacyConfigOptions } from '@forgerock/sdk-types';

export type ConfigOptions = LegacyConfigOptions;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this technically a breaking change we need to be concerned about? The tokenStore option now accepts CustomStorageObject instead of TokenStoreObject (legacy). This change makes sense and I know most of the properties in this config object are ignored by device client, including tokenStore, but if someone was previously passing a TokenStoreObject then this breaks for them.


// Re-export local types
export * from './oath.types.js';
Expand Down
5 changes: 5 additions & 0 deletions packages/device-client/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,10 @@
"src/**/*.test.ts",
"src/**/*.test.utils.ts",
"src/lib/mock-data/*"
],
"references": [
{
"path": "../sdk-types/tsconfig.lib.json"
}
]
}
Loading
Loading