Skip to content

Commit 01248e6

Browse files
committed
feat(journey-app): delete webauthn devices using device client
1 parent d119831 commit 01248e6

12 files changed

Lines changed: 608 additions & 39 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
8+
export function renderDeleteDevicesSection(
9+
journeyEl: HTMLDivElement,
10+
storeDevicesBeforeSession: () => Promise<string>,
11+
deleteDevicesInSession: () => Promise<string>,
12+
deleteAllDevices: () => Promise<string>,
13+
): void {
14+
const getDevicesButton = document.createElement('button');
15+
getDevicesButton.type = 'button';
16+
getDevicesButton.id = 'getDevicesButton';
17+
getDevicesButton.innerText = 'Get Registered Devices';
18+
19+
const deleteDevicesButton = document.createElement('button');
20+
deleteDevicesButton.type = 'button';
21+
deleteDevicesButton.id = 'deleteDevicesButton';
22+
deleteDevicesButton.innerText = 'Delete Devices From This Session';
23+
24+
const deleteAllDevicesButton = document.createElement('button');
25+
deleteAllDevicesButton.type = 'button';
26+
deleteAllDevicesButton.id = 'deleteAllDevicesButton';
27+
deleteAllDevicesButton.innerText = 'Delete All Registered Devices';
28+
29+
const deviceStatus = document.createElement('pre');
30+
deviceStatus.id = 'deviceStatus';
31+
deviceStatus.style.minHeight = '1.5em';
32+
33+
journeyEl.appendChild(getDevicesButton);
34+
journeyEl.appendChild(deleteDevicesButton);
35+
journeyEl.appendChild(deleteAllDevicesButton);
36+
journeyEl.appendChild(deviceStatus);
37+
38+
function setButtonsDisabled(disabled: boolean): void {
39+
getDevicesButton.disabled = disabled;
40+
deleteDevicesButton.disabled = disabled;
41+
deleteAllDevicesButton.disabled = disabled;
42+
}
43+
44+
async function setDeviceStatus(
45+
progressStatus: string,
46+
action: () => Promise<string>,
47+
errorPrefix: string,
48+
): Promise<void> {
49+
try {
50+
deviceStatus.innerText = progressStatus;
51+
setButtonsDisabled(true);
52+
53+
const successMessage = await action();
54+
deviceStatus.innerText = successMessage;
55+
} catch (error) {
56+
const message = error instanceof Error ? error.message : String(error);
57+
deviceStatus.innerText = `${errorPrefix}: ${message}`;
58+
} finally {
59+
setButtonsDisabled(false);
60+
}
61+
}
62+
63+
getDevicesButton.addEventListener('click', async () => {
64+
await setDeviceStatus(
65+
'Retrieving existing WebAuthn devices...',
66+
storeDevicesBeforeSession,
67+
'Get existing devices failed',
68+
);
69+
});
70+
71+
deleteDevicesButton.addEventListener('click', async () => {
72+
await setDeviceStatus(
73+
'Deleting WebAuthn devices in this session...',
74+
deleteDevicesInSession,
75+
'Delete failed',
76+
);
77+
});
78+
79+
deleteAllDevicesButton.addEventListener('click', async () => {
80+
await setDeviceStatus(
81+
'Deleting all registered WebAuthn devices...',
82+
deleteAllDevices,
83+
'Delete failed',
84+
);
85+
});
86+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
8+
import { JourneyStep } from '@forgerock/journey-client/types';
9+
import { WebAuthn, WebAuthnStepType } from '@forgerock/journey-client/webauthn';
10+
11+
export function webauthnComponent(journeyEl: HTMLDivElement, step: JourneyStep, idx: number) {
12+
const container = document.createElement('div');
13+
container.id = `webauthn-container-${idx}`;
14+
const info = document.createElement('p');
15+
info.innerText = 'Please complete the WebAuthn challenge using your authenticator.';
16+
container.appendChild(info);
17+
journeyEl.appendChild(container);
18+
19+
const webAuthnStepType = WebAuthn.getWebAuthnStepType(step);
20+
21+
async function handleWebAuthn() {
22+
try {
23+
if (webAuthnStepType === WebAuthnStepType.Authentication) {
24+
console.log('trying authentication');
25+
await WebAuthn.authenticate(step);
26+
} else if (WebAuthnStepType.Registration === webAuthnStepType) {
27+
console.log('trying registration');
28+
await WebAuthn.register(step);
29+
} else {
30+
return Promise.resolve(undefined);
31+
}
32+
} catch (error) {
33+
console.error('WebAuthn error:', error);
34+
}
35+
}
36+
37+
return handleWebAuthn();
38+
}

e2e/journey-app/main.ts

Lines changed: 79 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
11
/*
2-
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
2+
* Copyright (c) 2025-2026 Ping Identity Corporation. All rights reserved.
33
*
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
66
*/
77
import './style.css';
88

99
import { journey } from '@forgerock/journey-client';
10+
import { WebAuthn, WebAuthnStepType } from '@forgerock/journey-client/webauthn';
1011

11-
import type { JourneyClient, RequestMiddleware } from '@forgerock/journey-client/types';
12+
import type {
13+
JourneyClient,
14+
JourneyClientConfig,
15+
RequestMiddleware,
16+
} from '@forgerock/journey-client/types';
1217

1318
import { renderCallbacks } from './callback-map.js';
19+
import { renderDeleteDevicesSection } from './components/webauthn-devices.js';
1420
import { renderQRCodeStep } from './components/qr-code.js';
1521
import { renderRecoveryCodesStep } from './components/recovery-codes.js';
22+
import {
23+
deleteAllDevices,
24+
deleteDevicesInSession,
25+
storeDevicesBeforeSession,
26+
} from './services/delete-webauthn-devices.js';
27+
import { webauthnComponent } from './components/webauthn.js';
1628
import { serverConfigs } from './server-configs.js';
1729

1830
const qs = window.location.search;
@@ -61,7 +73,12 @@ if (searchParams.get('middleware') === 'true') {
6173

6274
let journeyClient: JourneyClient;
6375
try {
64-
journeyClient = await journey({ config: config, requestMiddleware });
76+
const journeyConfig: JourneyClientConfig = {
77+
serverConfig: {
78+
wellknown: config.serverConfig.wellknown,
79+
},
80+
};
81+
journeyClient = await journey({ config: journeyConfig, requestMiddleware });
6582
} catch (error) {
6683
const message = error instanceof Error ? error.message : 'Unknown error';
6784
console.error('Failed to initialize journey client:', message);
@@ -70,34 +87,6 @@ if (searchParams.get('middleware') === 'true') {
7087
}
7188
let step = await journeyClient.start({ journey: journeyName });
7289

73-
function renderComplete() {
74-
if (step?.type !== 'LoginSuccess') {
75-
throw new Error('Expected step to be defined and of type LoginSuccess');
76-
}
77-
78-
const session = step.getSessionToken();
79-
80-
console.log(`Session Token: ${session || 'none'}`);
81-
82-
journeyEl.innerHTML = `
83-
<h2 id="completeHeader">Complete</h2>
84-
<span id="sessionLabel">Session:</span>
85-
<pre id="sessionToken" id="sessionToken">${session}</pre>
86-
<button type="button" id="logoutButton">Logout</button>
87-
`;
88-
89-
const loginBtn = document.getElementById('logoutButton') as HTMLButtonElement;
90-
loginBtn.addEventListener('click', async () => {
91-
await journeyClient.terminate();
92-
93-
console.log('Logout successful');
94-
95-
step = await journeyClient.start({ journey: journeyName });
96-
97-
renderForm();
98-
});
99-
}
100-
10190
function renderError() {
10291
if (step?.type !== 'LoginFailure') {
10392
throw new Error('Expected step to be defined and of type LoginFailure');
@@ -134,6 +123,16 @@ if (searchParams.get('middleware') === 'true') {
134123
renderQRCodeStep(journeyEl, step) || renderRecoveryCodesStep(journeyEl, step);
135124

136125
if (!stepRendered) {
126+
const webAuthnStep = WebAuthn.getWebAuthnStepType(step);
127+
const isWebAuthn =
128+
webAuthnStep === WebAuthnStepType.Authentication ||
129+
webAuthnStep === WebAuthnStepType.Registration;
130+
if (isWebAuthn) {
131+
await webauthnComponent(journeyEl, step, 0);
132+
submitForm();
133+
return; // prevent the rest of the function from running
134+
}
135+
137136
const callbacks = step.callbacks;
138137
renderCallbacks(journeyEl, callbacks, submitForm);
139138
}
@@ -145,6 +144,55 @@ if (searchParams.get('middleware') === 'true') {
145144
journeyEl.appendChild(submitBtn);
146145
}
147146

147+
function renderComplete() {
148+
if (step?.type !== 'LoginSuccess') {
149+
throw new Error('Expected step to be defined and of type LoginSuccess');
150+
}
151+
152+
const session = step.getSessionToken();
153+
154+
console.log(`Session Token: ${session || 'none'}`);
155+
156+
journeyEl.innerHTML = `
157+
<h2 id="completeHeader">Complete</h2>
158+
<span id="sessionLabel">Session:</span>
159+
<pre id="sessionToken" id="sessionToken">${session}</pre>
160+
<button type="button" id="logoutButton">Logout</button>
161+
`;
162+
163+
const logoutBtn = document.getElementById('logoutButton') as HTMLButtonElement;
164+
const sessionLabelEl = document.getElementById('sessionLabel') as HTMLSpanElement;
165+
166+
renderDeleteDevicesSection(
167+
journeyEl,
168+
() => storeDevicesBeforeSession(config),
169+
() => deleteDevicesInSession(config),
170+
() => deleteAllDevices(config),
171+
);
172+
173+
const getDevicesButton = document.getElementById('getDevicesButton') as HTMLButtonElement;
174+
const deleteDevicesButton = document.getElementById('deleteDevicesButton') as HTMLButtonElement;
175+
const deleteAllDevicesButton = document.getElementById(
176+
'deleteAllDevicesButton',
177+
) as HTMLButtonElement;
178+
const deviceStatus = document.getElementById('deviceStatus') as HTMLPreElement;
179+
180+
journeyEl.insertBefore(getDevicesButton, sessionLabelEl);
181+
journeyEl.insertBefore(deleteDevicesButton, sessionLabelEl);
182+
journeyEl.insertBefore(deleteAllDevicesButton, sessionLabelEl);
183+
journeyEl.insertBefore(deviceStatus, sessionLabelEl);
184+
185+
logoutBtn.addEventListener('click', async () => {
186+
await journeyClient.terminate();
187+
188+
console.log('Logout successful');
189+
190+
step = await journeyClient.start({ journey: journeyName });
191+
192+
renderForm();
193+
});
194+
}
195+
148196
formEl.addEventListener('submit', async (event) => {
149197
event.preventDefault();
150198

e2e/journey-app/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"@forgerock/journey-client": "workspace:*",
1515
"@forgerock/oidc-client": "workspace:*",
1616
"@forgerock/protect": "workspace:*",
17-
"@forgerock/sdk-logger": "workspace:*"
17+
"@forgerock/sdk-logger": "workspace:*",
18+
"@forgerock/device-client": "workspace:*"
1819
},
1920
"nx": {
2021
"tags": ["scope:e2e"]

e2e/journey-app/server-configs.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,32 @@
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 type { JourneyClientConfig } from '@forgerock/journey-client/types';
7+
import type { OidcConfig } from '@forgerock/oidc-client/types';
88

99
/**
1010
* Server configurations for E2E tests.
1111
*
1212
* All configuration (baseUrl, authenticate/sessions paths) is automatically
1313
* derived from the well-known response via `convertWellknown()`.
1414
*/
15-
export const serverConfigs: Record<string, JourneyClientConfig> = {
15+
export const serverConfigs: Record<string, OidcConfig> = {
1616
basic: {
17+
clientId: 'WebOAuthClient',
18+
redirectUri: '',
19+
scope: 'openid profile email',
1720
serverConfig: {
1821
wellknown: 'http://localhost:9443/am/oauth2/realms/root/.well-known/openid-configuration',
1922
},
23+
responseType: 'code',
2024
},
2125
tenant: {
26+
clientId: 'WebOAuthClient',
27+
redirectUri: '',
28+
scope: 'openid profile email',
2229
serverConfig: {
2330
wellknown:
2431
'https://openam-sdks.forgeblocks.com/am/oauth2/realms/root/realms/alpha/.well-known/openid-configuration',
2532
},
33+
responseType: 'code',
2634
},
2735
};

0 commit comments

Comments
 (0)