Skip to content

Commit 3d60a50

Browse files
Merge pull request #110 from pingidentity/VC-1157-web-verify-adapter-2.0
VC-1157: update js widget for verify adapter 2.0
2 parents 4db5da7 + 479309b commit 3d60a50

File tree

8 files changed

+354
-85
lines changed

8 files changed

+354
-85
lines changed

src/index.js

Lines changed: 159 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ export default class AuthnWidget {
4444
'ONE_TIME_DEVICE_OTP_INPUT_REQUIRED'
4545
]
4646

47+
const idVerificationStates = [
48+
'ID_VERIFICATION_FAILED',
49+
'ID_VERIFICATION_REQUIRED',
50+
'ID_VERIFICATION_TIMED_OUT',
51+
'ID_VERIFICATION_DEVICE',
52+
'ID_VERIFICATION_OPTIONS',
53+
]
54+
4755
return ['USERNAME_PASSWORD_REQUIRED', 'MUST_CHANGE_PASSWORD', 'CHANGE_PASSWORD_EXTERNAL', 'NEW_PASSWORD_RECOMMENDED',
4856
'NEW_PASSWORD_REQUIRED', 'SUCCESSFUL_PASSWORD_CHANGE', 'ACCOUNT_RECOVERY_USERNAME_REQUIRED',
4957
'ACCOUNT_RECOVERY_OTL_VERIFICATION_REQUIRED', 'RECOVERY_CODE_REQUIRED', 'PASSWORD_RESET_REQUIRED',
@@ -52,8 +60,7 @@ export default class AuthnWidget {
5260
'EXTERNAL_AUTHENTICATION_COMPLETED', 'EXTERNAL_AUTHENTICATION_FAILED', 'EXTERNAL_AUTHENTICATION_REQUIRED',
5361
'DEVICE_PROFILE_REQUIRED', 'REGISTRATION_REQUIRED', 'REFERENCE_ID_REQUIRED','CURRENT_CREDENTIALS_REQUIRED',
5462
'DEVICE_SELECTION_REQUIRED', 'MFA_COMPLETED', 'MFA_FAILED', 'OTP_REQUIRED', 'ASSERTION_REQUIRED',
55-
'PUSH_CONFIRMATION_REJECTED', 'PUSH_CONFIRMATION_TIMED_OUT', 'PUSH_CONFIRMATION_WAITING',
56-
'ID_VERIFICATION_FAILED', 'ID_VERIFICATION_REQUIRED', 'ID_VERIFICATION_TIMED_OUT', 'ACCOUNT_LINKING_FAILED',
63+
'PUSH_CONFIRMATION_REJECTED', 'PUSH_CONFIRMATION_TIMED_OUT', 'PUSH_CONFIRMATION_WAITING', 'ACCOUNT_LINKING_FAILED',
5764
'SECURID_CREDENTIAL_REQUIRED', 'SECURID_NEXT_TOKENCODE_REQUIRED', 'SECURID_REAUTHENTICATION_REQUIRED',
5865
'SECURID_SYSTEM_PIN_RESET_REQUIRED', 'SECURID_USER_PIN_RESET_REQUIRED', 'EMAIL_VERIFICATION_REQUIRED', 'EMAIL_VERIFICATION_OTP_REQUIRED',
5966
'MFA_SETUP_REQUIRED', 'DEVICE_PAIRING_METHOD_REQUIRED', 'EMAIL_PAIRING_TARGET_REQUIRED',
@@ -64,7 +71,8 @@ export default class AuthnWidget {
6471
'USER_ID_REQUIRED', 'AUTHENTICATOR_SELECTION_REQUIRED', 'INPUT_REQUIRED', 'ENTRUST_FAILED', 'FRAUD_EVALUATION_CHECK_REQUIRED', 'AUTHENTICATION_CODE_RESPONSE_REQUIRED'
6572
]
6673
.concat(oauthUserAuthorizationStates)
67-
.concat(oneTimeDeviceOtpStates);
74+
.concat(oneTimeDeviceOtpStates)
75+
.concat(idVerificationStates);
6876
}
6977

7078
static get COMMUNICATION_ERROR_MSG() {
@@ -150,7 +158,13 @@ export default class AuthnWidget {
150158
this.registerIdVerificationRequiredEventHandler = this.registerIdVerificationRequiredEventHandler.bind(this);
151159
this.postIdVerificationRequired = this.postIdVerificationRequired.bind(this);
152160
this.handleIdVerificationInProgress = this.handleIdVerificationInProgress.bind(this);
153-
this.handleIdVerificationFailed = this.handleIdVerificationFailed.bind(this);
161+
this.handleIdVerificationDevice = this.handleIdVerificationDevice.bind(this);
162+
this.deviceAuthentication = this.deviceAuthentication.bind(this);
163+
this.handleIdVerificationOptions = this.handleIdVerificationOptions.bind(this);
164+
this.optionsAuthentication = this.optionsAuthentication.bind(this);
165+
this.handleRetryVerification = this.handleRetryVerification.bind(this);
166+
this.handleCancelAuthentication = this.handleCancelAuthentication.bind(this);
167+
this.postIdVerificationTimedOut = this.postIdVerificationTimedOut.bind(this);
154168
this.checkSecurIdPinReset = this.checkSecurIdPinReset.bind(this);
155169
this.postAssertionRequired = this.postAssertionRequired.bind(this);
156170
this.postTOTPActivationRequired = this.postTOTPActivationRequired.bind(this);
@@ -219,7 +233,9 @@ export default class AuthnWidget {
219233
this.addPostRenderCallback('ID_VERIFICATION_REQUIRED', this.postIdVerificationRequired);
220234
this.addPostRenderCallback('ID_VERIFICATION_IN_PROGRESS', this.handleIdVerificationInProgress);
221235
this.addPostRenderCallback('ID_VERIFICATION_COMPLETED', this.postContinueAuthentication);
222-
this.addEventHandler('ID_VERIFICATION_FAILED', this.handleIdVerificationFailed);
236+
this.addPostRenderCallback('ID_VERIFICATION_TIMED_OUT', this.postIdVerificationTimedOut);
237+
this.addEventHandler('ID_VERIFICATION_DEVICE', this.handleIdVerificationDevice);
238+
this.addEventHandler('ID_VERIFICATION_OPTIONS', this.handleIdVerificationOptions);
223239
this.addEventHandler('SECURID_USER_PIN_RESET_REQUIRED', this.checkSecurIdPinReset);
224240
this.addPostRenderCallback('EMAIL_VERIFICATION_REQUIRED', this.postEmailVerificationRequired);
225241
this.addPostRenderCallback('TOTP_ACTIVATION_REQUIRED', this.postTOTPActivationRequired);
@@ -1045,31 +1061,21 @@ export default class AuthnWidget {
10451061
async registerIdVerificationRequiredEventHandler() {
10461062
let data = this.store.getStore();
10471063

1048-
if (data.errorDetails !== undefined)
1049-
{
1050-
document.getElementById("requiredDescription").style.display = "none";
1051-
document.getElementById("errorDescription").style.display = "block";
1052-
} else {
1053-
document.getElementById("requiredDescription").style.display = "block";
1054-
document.getElementById("errorDescription").style.display = "none";
1055-
}
1056-
1057-
const options = {
1058-
method: 'GET'
1059-
}
1060-
let qrUrlRespone = await fetch(data.qrUrl, options);
1061-
let qrCode = await qrUrlRespone.text();
1062-
document.getElementById('qrCode').src = qrCode;
1064+
document.getElementById('qrCode').src = data.qrUrl || "";
10631065
document.getElementById('qrCodeBlock').style.display = 'block';
10641066

1065-
var verificationCodeToken = data.verificationCode.match(/.{1,4}/g);
1066-
document.getElementById('verificationCode').innerHTML = verificationCodeToken.join(' ');
1067+
document.getElementById('verificationCode').innerHTML = data.verificationCode || "";
1068+
1069+
document.getElementById('retryVerification').addEventListener('click', this.handleRetryVerification);
1070+
1071+
if (data.newTab && data.webVerificationUrl !== undefined &&
1072+
(this.store.getPreviousStore().verificationCode !== data.verificationCode)) {
1073+
console.log("web link opens newtab: " + data.webVerificationUrl);
1074+
window.open(data.webVerificationUrl, '_blank');
1075+
}
10671076
}
10681077

10691078
postIdVerificationRequired() {
1070-
document.getElementById('copy')
1071-
.addEventListener('click', this.copyCode);
1072-
10731079
this.pollCheckGet(this.store.getStore().verificationCode, 5000);
10741080
}
10751081

@@ -1079,29 +1085,23 @@ export default class AuthnWidget {
10791085
}, 5000)
10801086
}
10811087

1082-
handleIdVerificationFailed() {
1083-
let data = this.store.getStore();
1084-
1085-
if (data.errorDetails !== undefined) {
1086-
document.getElementById("errorMessage").innerHTML = this.makeIdVerificationErrorMessage(data.errorDetails);
1087-
}
1088-
}
1089-
1090-
copyCode(event) {
1091-
event.preventDefault();
1092-
let range = document.createRange();
1093-
range.selectNode(document.getElementById('verificationCode'));
1094-
window.getSelection().removeAllRanges();
1095-
window.getSelection().addRange(range);
1096-
document.execCommand('copy');
1097-
window.getSelection().removeAllRanges();
1098-
}
1099-
11001088
async pollCheckGet(currentVerificationCode, timeout) {
11011089
let fetchUtil = this.store.fetchUtil;
11021090
let result = await fetchUtil.postFlow(this.store.flowId, 'poll', '{}');
11031091
let newState = await result.json();
11041092

1093+
if (newState.txStatus === 'INITIATED')
1094+
{
1095+
document.getElementById("requiredHeader").style.display = "none";
1096+
document.getElementById("requiredDescription").style.display = "none";
1097+
document.getElementById("startedHeader").style.display = "block";
1098+
document.getElementById("startedDescription").style.display = "block";
1099+
}
1100+
if (newState.code === 'RESOURCE_NOT_FOUND') {
1101+
console.log("resource not found, stop polling flow");
1102+
return;
1103+
}
1104+
11051105
let pollAgain =
11061106
(newState.status === 'ID_VERIFICATION_REQUIRED') &&
11071107
(newState.verificationCode === currentVerificationCode);
@@ -1112,27 +1112,128 @@ export default class AuthnWidget {
11121112
.catch(() => this.generalErrorRenderer(AuthnWidget.COMMUNICATION_ERROR_MSG));
11131113
}
11141114
else {
1115-
setTimeout(() => {
1115+
this.pollCheckGetHandler = setTimeout(() => {
11161116
this.pollCheckGet(currentVerificationCode, timeout);
1117-
}, timeout)
1117+
}, timeout);
11181118
}
11191119
}
11201120

1121-
makeIdVerificationErrorMessage(errorDetails) {
1122-
var errorMessage = '';
1121+
async handleRetryVerification(event, cnt) {
1122+
const maxretries = 10;
1123+
if (cnt === undefined) cnt = 1;
1124+
else if (cnt > maxretries) {
1125+
console.log("exceeded max retries, stop sending retry request");
1126+
return;
1127+
}
11231128

1124-
errorDetails.forEach(errorDetail =>
1125-
{
1126-
if (errorMessage === '') {
1127-
errorMessage = errorDetail.userMessage
1128-
}
1129-
else {
1130-
var append = '<br>' + errorDetail.userMessage
1131-
errorMessage += append
1132-
}
1133-
})
1129+
clearTimeout(this.pollCheckGetHandler);
1130+
// make cancel button non-clickable after one click
1131+
event.target.style.pointerEvents = "none";
1132+
1133+
let actionId = event.target.id;
1134+
let data = this.store;
1135+
data.prevState = data.state;
1136+
// store.reduce is used to prevent notifyListener from being called at the end of store.dispatch
1137+
// when polling is in progress and flow is not available so previous state is returned;
1138+
// don't start extra polling tasks during resubmission of retryVerification request.
1139+
data.state = await this.store.reduce('POST_FLOW', actionId);
1140+
if (data.prevState.username && !data.state.username) {
1141+
data.state.username = data.prevState.username;
1142+
}
1143+
console.log('dispatching retry: ' + actionId+" #"+cnt);
1144+
console.log(data.state);
1145+
1146+
if (data.state.status === 'ID_VERIFICATION_REQUIRED') {
1147+
console.log("not returning to first screen, resubmiting post request in 1 second");
1148+
await new Promise(r => setTimeout(r, 1000));
1149+
this.handleRetryVerification(event, cnt+1);
1150+
} else {
1151+
this.store.notifyListeners();
1152+
}
1153+
}
11341154

1135-
return errorMessage;
1155+
async handleCancelAuthentication(event) {
1156+
await this.store.dispatch('POST_FLOW', event.target.id);
1157+
const state = this.store.state;
1158+
// cancel in login flow goes to login screen
1159+
// cancel in registration flow renders registration_failed error in a separate page
1160+
if (state.status !== 'USERNAME_PASSWORD_REQUIRED') {
1161+
this.generalErrorRenderer(state.userMessages);
1162+
}
1163+
}
1164+
1165+
postIdVerificationTimedOut() {
1166+
document.getElementById('cancelAuthentication').addEventListener('click', this.handleCancelAuthentication);
1167+
}
1168+
1169+
handleIdVerificationDevice() {
1170+
document.getElementById('other').addEventListener('click', this.deviceAuthentication);
1171+
document.getElementById('self').addEventListener('click', this.deviceAuthentication);
1172+
document.getElementById('cancelAuthentication').addEventListener('click', this.handleCancelAuthentication);
1173+
}
1174+
1175+
deviceAuthentication(event) {
1176+
console.log("selected device: " + event.target.id);
1177+
document.getElementById("AuthnWidgetForm").style.pointerEvents = "none";
1178+
const data = { "deviceAuthentication": event.target.id };
1179+
this.store.dispatch('POST_FLOW', "deviceAuthentication", JSON.stringify(data));
1180+
}
1181+
1182+
handleIdVerificationOptions() {
1183+
let data = this.store.getStore();
1184+
if (data.forcePolicy) {
1185+
document.getElementById("description").innerHTML = "Select a method to receive a web link on your mobile device to start the verification process.";
1186+
document.getElementById("qrbtn").style.display = "none";
1187+
1188+
const radios = document.getElementsByName("radioGroup");
1189+
for (let i = 0; i < radios.length; i++) {
1190+
radios[i].checked = true;
1191+
}
1192+
document.getElementById("nextbtn").disabled = false;
1193+
document.getElementById("cancelAuthentication").style.display = "block";
1194+
document.getElementById("retryoptions").style.display = "none";
1195+
}
1196+
if (data.errorMessage) {
1197+
document.getElementById("nextbtn").disabled = true;
1198+
} else {
1199+
if (data.emails.length) {
1200+
document.getElementById('emailRadio').addEventListener('click', this.optionsRadioSelected);
1201+
}
1202+
if (data.phones.length) {
1203+
document.getElementById('mobileRadio').addEventListener('click', this.optionsRadioSelected);
1204+
}
1205+
document.getElementById('qrbtn').addEventListener('click', this.optionsAuthentication);
1206+
document.getElementById('nextbtn').addEventListener('click', this.optionsAuthentication);
1207+
}
1208+
document.getElementById('cancelAuthentication').addEventListener('click', this.handleCancelAuthentication);
1209+
}
1210+
1211+
optionsRadioSelected() {
1212+
document.getElementById("nextbtn").disabled = false;
1213+
}
1214+
1215+
optionsAuthentication(event) {
1216+
console.log("selected option: " + event.target.id);
1217+
document.getElementById("AuthnWidgetForm").style.pointerEvents = "none";
1218+
let email = null;
1219+
let phone = null;
1220+
const qrOnly = event.target.id === "qrbtn";
1221+
if (qrOnly === true) {
1222+
console.log("skip notification, show qr code only");
1223+
} else if (document.querySelector('input[name="radioGroup"]:checked')) {
1224+
const radio = document.querySelector('input[name="radioGroup"]:checked').value;
1225+
if (radio === "emailRadio") {
1226+
const select = document.getElementById("emails");
1227+
email = select.options[select.selectedIndex].value;
1228+
console.log("selected email: "+email);
1229+
} else if (radio === "mobileRadio") {
1230+
const select = document.getElementById("mobiles");
1231+
phone = select.options[select.selectedIndex].value;
1232+
console.log("selected phone: "+phone);
1233+
}
1234+
}
1235+
const data = { "email": email, "phone": phone, "optionsAuthentication": true };
1236+
this.store.dispatch('POST_FLOW', "optionsAuthentication", JSON.stringify(data));
11361237
}
11371238

11381239
checkSecurIdPinReset() {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<div class="card">
2+
<div class="org-logo">
3+
<img class="org-logo__image" src="{{logo}}" alt="Company Logo"/>
4+
</div>
5+
<form class="form form--spacing-lg idv-form" id="AuthnWidgetForm">
6+
<div class="stack stack--small">
7+
<h1 id="header" class="heading" data-id="heading">Verification Required</h1>
8+
<h4 id="description" class="heading heading--4 text-block" data-id="heading">
9+
If you are already on your mobile device, select This Device to open a web link on your mobile device to start the verification process, otherwise select the Other Device button.
10+
</h4>
11+
</div>
12+
<div class="org-logo idv-center">
13+
<div id="icon" class="org-logo__image idv-newtab"></div>
14+
</div>
15+
<div class="idv-btntbl">
16+
<div class="idv-btnrow">
17+
<div class="idv-btncell">
18+
<button id="other" class="button button--tertiary button--primary idv-btn" type="button">
19+
Other Device
20+
</button>
21+
</div>
22+
<div class="idv-btncell">
23+
<button id="self" class="button button--tertiary button--primary idv-btn" type="button">
24+
This Device
25+
</button>
26+
</div>
27+
</div>
28+
</div>
29+
<div class="text-block">
30+
<a tabindex="0" id="cancelAuthentication" target="_self" class="anchor">Cancel</a>
31+
</div>
32+
</form>
33+
</div>

src/partials/id_verification_failed.hbs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,22 @@
22
<div class="org-logo">
33
<img class="org-logo__image" src="{{logo}}" alt="Company Logo" />
44
</div>
5-
<div class="icon-feedback icon-feedback--timeout icon-feedback--error">
6-
<span class="icon-feedback__icon pingicon-ghost-error"></span>
7-
<p class="icon-feedback__label">
8-
<p class="icon-feedback--error">Verification Failed</p>
9-
</p>
5+
<div class="icon-feedback icon-feedback--timeout icon-feedback--error idv-bottom">
6+
<span class="icon-feedback__icon pingicon-ghost-error idv-bottom"></span>
107
</div>
11-
<div class="text-block text-block--small" data-id="textblock">
12-
<div id="errorMessage" class="text-block--overflow-wrap">Error Message</div>
13-
</div>
14-
<div class="text-block" data-id="textblock">
15-
<div class="text-block--overflow-wrap">
16-
<a tabindex="0" data-actionId="cancelAuthentication" target="_self" class="anchor">Cancel</a>
8+
<h4 id="failedDescription" class="heading heading--4 text-block idv-center-small" data-id="heading">
9+
{{#if txId}}
10+
Please contact the support team and provide the information below, or please try again by selecting Cancel.
11+
{{else}}
12+
Please contact the support team for more information, or please try again by selecting Cancel.
13+
{{/if}}
14+
</h4>
15+
{{#if txId}}
16+
<div class="text-block text-block--small" data-id="textblock">
17+
<div id="errorMessage" class="text-block--overflow-wrap">Transaction ID: {{txId}}</div>
1718
</div>
19+
{{/if}}
20+
<div class="text-block" data-id="textblock">
21+
<a tabindex="0" data-actionId="retryVerification" target="_self" class="anchor">Cancel</a>
1822
</div>
1923
</div>

0 commit comments

Comments
 (0)