Skip to content

Commit 266f7f0

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

11 files changed

Lines changed: 695 additions & 36 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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+
async function setDeviceStatus(
39+
progressStatus: string,
40+
action: () => Promise<string>,
41+
errorPrefix: string,
42+
): Promise<void> {
43+
try {
44+
deviceStatus.innerText = progressStatus;
45+
46+
const successMessage = await action();
47+
deviceStatus.innerText = successMessage;
48+
} catch (error) {
49+
const message = error instanceof Error ? error.message : String(error);
50+
deviceStatus.innerText = `${errorPrefix}: ${message}`;
51+
}
52+
}
53+
54+
getDevicesButton.addEventListener('click', async () => {
55+
await setDeviceStatus(
56+
'Retrieving existing WebAuthn devices...',
57+
storeDevicesBeforeSession,
58+
'Get existing devices failed',
59+
);
60+
});
61+
62+
deleteDevicesButton.addEventListener('click', async () => {
63+
await setDeviceStatus(
64+
'Deleting WebAuthn devices in this session...',
65+
deleteDevicesInSession,
66+
'Delete failed',
67+
);
68+
});
69+
70+
deleteAllDevicesButton.addEventListener('click', async () => {
71+
await setDeviceStatus(
72+
'Deleting all registered WebAuthn devices...',
73+
deleteAllDevices,
74+
'Delete failed',
75+
);
76+
});
77+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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(): Promise<boolean> {
22+
try {
23+
if (webAuthnStepType === WebAuthnStepType.Authentication) {
24+
console.log('trying authentication');
25+
await WebAuthn.authenticate(step);
26+
return true;
27+
} else if (WebAuthnStepType.Registration === webAuthnStepType) {
28+
console.log('trying registration');
29+
await WebAuthn.register(step);
30+
return true;
31+
} else {
32+
return false;
33+
}
34+
} catch {
35+
return false;
36+
}
37+
}
38+
39+
return handleWebAuthn();
40+
}

e2e/journey-app/main.ts

Lines changed: 89 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');
@@ -117,6 +106,7 @@ if (searchParams.get('middleware') === 'true') {
117106
// Represents the main render function for app
118107
async function renderForm() {
119108
journeyEl.innerHTML = '';
109+
errorEl.textContent = '';
120110

121111
if (step?.type !== 'Step') {
122112
throw new Error('Expected step to be defined and of type Step');
@@ -130,6 +120,23 @@ if (searchParams.get('middleware') === 'true') {
130120

131121
const submitForm = () => formEl.requestSubmit();
132122

123+
// Handle WebAuthn steps first so we can hide the Submit button while processing,
124+
// auto-submit on success, and show an error on failure.
125+
const webAuthnStep = WebAuthn.getWebAuthnStepType(step);
126+
const isWebAuthn =
127+
webAuthnStep === WebAuthnStepType.Authentication ||
128+
webAuthnStep === WebAuthnStepType.Registration;
129+
if (isWebAuthn) {
130+
const webauthnSucceeded = await webauthnComponent(journeyEl, step, 0);
131+
if (webauthnSucceeded) {
132+
submitForm();
133+
return;
134+
} else {
135+
errorEl.textContent =
136+
'WebAuthn failed or was cancelled. Please try again or use a different method.';
137+
}
138+
}
139+
133140
const stepRendered =
134141
renderQRCodeStep(journeyEl, step) || renderRecoveryCodesStep(journeyEl, step);
135142

@@ -145,6 +152,57 @@ if (searchParams.get('middleware') === 'true') {
145152
journeyEl.appendChild(submitBtn);
146153
}
147154

155+
function renderComplete() {
156+
if (step?.type !== 'LoginSuccess') {
157+
throw new Error('Expected step to be defined and of type LoginSuccess');
158+
}
159+
160+
const session = step.getSessionToken();
161+
162+
console.log(`Session Token: ${session || 'none'}`);
163+
164+
journeyEl.replaceChildren();
165+
166+
const completeHeader = document.createElement('h2');
167+
completeHeader.id = 'completeHeader';
168+
completeHeader.innerText = 'Complete';
169+
journeyEl.appendChild(completeHeader);
170+
171+
renderDeleteDevicesSection(
172+
journeyEl,
173+
() => storeDevicesBeforeSession(config),
174+
() => deleteDevicesInSession(config),
175+
() => deleteAllDevices(config),
176+
);
177+
178+
const sessionLabelEl = document.createElement('span');
179+
sessionLabelEl.id = 'sessionLabel';
180+
sessionLabelEl.innerText = 'Session:';
181+
182+
const sessionTokenEl = document.createElement('pre');
183+
sessionTokenEl.id = 'sessionToken';
184+
sessionTokenEl.textContent = session || 'none';
185+
186+
const logoutBtn = document.createElement('button');
187+
logoutBtn.type = 'button';
188+
logoutBtn.id = 'logoutButton';
189+
logoutBtn.innerText = 'Logout';
190+
191+
journeyEl.appendChild(sessionLabelEl);
192+
journeyEl.appendChild(sessionTokenEl);
193+
journeyEl.appendChild(logoutBtn);
194+
195+
logoutBtn.addEventListener('click', async () => {
196+
await journeyClient.terminate();
197+
198+
console.log('Logout successful');
199+
200+
step = await journeyClient.start({ journey: journeyName });
201+
202+
renderForm();
203+
});
204+
}
205+
148206
formEl.addEventListener('submit', async (event) => {
149207
event.preventDefault();
150208

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)