1// Common classes used both in adaptive and regular SDKs. 2 3// Create global object if needed. 4window.nnl = window.nnl || {}; 5 6// The flag indicates that this is a Cordova App (in cordova document URL starts with FILE) 7nnl.isCordovaApp = (document.URL.indexOf('http://') === -1) && (document.URL.indexOf('https://') === -1); 8 9// DefaultUIConfig represents the default configuration for the UI. 10const DefaultUIConfig = Object.freeze({ 11 reasonCodeMap: { 12 passkey_on_new_device: "New device detected!", 13 key_protection_software: "Additional verification is required." 14 } 15}); 16 17/** 18 * @classdesc Promise wrapper that can be externally setteled. 19 * @class 20 * @ignore 21 */ 22const PromiseWrapper = function() { 23 const _this = this; 24 /** 25 * Resolves the actual Promise object. 26 * @method PromiseWrapper#resolve 27 * @param {any} value The value to resolve the Promise with. 28 */ 29 this.resolve = null; 30 31 /** 32 * Rejects the actual Promise object. 33 * @method PromiseWrapper#reject 34 * @param {any} value The value to reject the Promise with. 35 */ 36 this.reject = null; 37 38 const mPromise = new Promise(function(resolve, reject) { 39 _this.resolve = resolve; 40 _this.reject = reject; 41 }); 42 43 /** 44 * Returns the actual Promise object. 45 */ 46 this.get = function() { 47 return mPromise; 48 } 49 50 Object.freeze(this); 51} 52 53/** 54 * Defines type of fido operation 55 * @enum {string} 56 */ 57const FidoOperationType = { 58 REG: 'REG', 59 AUTH: 'AUTH', 60 LIST_REG: 'LIST_REG', 61 RENAME_REG: 'RENAME_REG', 62 DELETE_REG: 'DELETE_REG', 63 LIST_OOB_AUTH: 'LIST_OOB_AUTH' 64} 65Object.freeze(FidoOperationType); 66 67/** 68 * Defines state of ongoing adaptive operation which should be used to determine next step on adaptive process. 69 * @enum {string} 70 */ 71const OperationState = { 72 /** 73 * should continue with init. 74 */ 75 OP_INIT: 'OP_INIT', 76 /** 77 * should continue with update. 78 */ 79 OP_UPDATE: 'OP_UPDATE', 80 /** 81 * should complete with success result. 82 */ 83 OP_CANCEL: 'OP_CANCEL', 84 /** 85 * Should cancel entire adaptive process. 86 */ 87 OP_SUCCESS: 'OP_SUCCESS', 88 /** 89 * Should lock the method until lock period is passed. 90 */ 91 OP_LOCKED: 'OP_LOCKED' 92} 93Object.freeze(OperationState); 94 95/** 96 * Defines type of ongoing adaptive operation. {@link MethodUI} implementations use this to show appropriate UI. 97 * @enum {string} 98 */ 99const OperationType = { 100 /** 101 * Indicates registration operation. 102 */ 103 REGISTRATION: 'REGISTRATION', 104 /** 105 * Indicates authentication operation. 106 */ 107 AUTHENTICATION: 'AUTHENTICATION' 108} 109Object.freeze(OperationType); 110 111/** 112 * Specifies the type of QR code to generate. 113 * @enum {string} 114 */ 115const QRType = { 116 /** 117 * The QR code will be generated for mobile apps only. 118 * The QR code will be specific for the relying party (the OOB data will not include the operation endpoint). 119 */ 120 APP_RP_SPECIFIC: 'APP_RP_SPECIFIC', 121 /** 122 * The QR code will be generated for mobile apps only. 123 * The QR code will be generated for any relying party (the OOB data will include the operation endpoint). 124 */ 125 APP_ANY_RP: 'APP_ANY_RP', 126 /** 127 * The QR code will be generated for mobile apps and browsers. 128 * The QR code will be specific for the relying party (the OOB data will not include the operation endpoint). 129 */ 130 UNIVERSAL_RP_SPECIFIC: 'UNIVERSAL_RP_SPECIFIC', 131 /** 132 * The QR code will be generated for mobile apps and browsers. 133 * The QR code will be generated for any relying party (the OOB data will include the operation endpoint). 134 */ 135 UNIVERSAL_ANY_RP: 'UNIVERSAL_ANY_RP' 136} 137Object.freeze(QRType); 138 139/** 140 * Adaptive method types. 141 * @type {Object} 142 * @name AdaptiveType 143 * @property {string} EMAIL Indicates method that uses Email OTP. 144 * @property {string} SMS Indicates method that uses SMS OTP. 145 * @property {string} FIDO Indicates method that uses FIDO protocol. 146 * @property {string} FIDO_OOB Indicates method that uses OOB. 147 * @property {string} EXTERNAL_AUTH Indicates method that uses External Authentication. 148 */ 149const AdaptiveType = new (function() { 150 // Adaptive types. 151 this.EMAIL = 'Email OTP'; 152 this.SMS = 'SMS OTP'; 153 this.FIDO = 'FIDO Auth';
154 this.FIDO_OOB = 'FIDO OOB Auth'; 155 this.EXTERNAL_AUTH = 'External Auth'; 156 this.PHOTO_ID = 'Photo ID'; 157 158 // Adaptive names. 159 this.names = {}; 160 this.names[this.EMAIL] = 'OTP Using Email'; 161 this.names[this.SMS] = 'OTP Using SMS'; 162 this.names[this.FIDO] = ''; 163 this.names[this.FIDO_OOB] = ''; 164 this.names[this.EXTERNAL_AUTH] = ''; 165 this.names[this.PHOTO_ID] = 'Using Photo ID'; 166 167 Object.freeze(this); 168})(); 169 170 171/** 172 * Adaptive method types. 173 * @type {Object} 174 * @name AuthMethod 175 */ 176const AuthMethod = { 177 FIDO2_AUTH:'FIDO2-AUTH', 178 UAF_AUTH: 'UAF-AUTH', 179 OOB_PUSH: 'OOB-PUSH' 180} 181Object.freeze(AuthMethod); 182 183/** 184 * @enum {string} 185 */ 186const MethodState = { 187 SUCCEEDED:"SUCCEEDED", 188 AWAITING_USER_ACTION:"AWAITING_USER_ACTION", 189 PENDING:"PENDING", 190 IN_PROGRESS:"IN_PROGRESS", 191 FAILED:"FAILED" 192} 193Object.freeze(MethodState); 194 195/** 196 * @enum {string} 197 * Indicates which subsystem of the SDK has produced the error. 198 */ 199const SubSystem = { 200 /** The error was produced by the UAF subsystem */ 201 UAF: 'UAF', 202 /** The error was produced by the FIDO2 subsystem */ 203 FIDO2: 'FIDO2', 204 /** The error was produced by a non FIDO subsystem */ 205 NON_FIDO: 'NON_FIDO', 206 /** The error was produced by the Adaptive Engine subsystem */ 207 ADAPTIVE_ENGINE: 'ADAPTIVE_ENGINE' 208} 209Object.freeze(SubSystem); 210 211 212const QUICK_AUTH_DATA_FIDO2_KEY = 'dataFido2'; 213const SECURITY_KEY_DEFAULT_NAME = 'Security Key'; 214 215/** 216 * @enum {string} 217 * Indicates adaptive operation type in {@link AdaptiveResult}. 218 */ 219const AdaptiveOperation = { 220 /** The result is for authentication operation */ 221 AUTH: 'AUTH', 222 /** The result is for suggest register operation */ 223 SUGGEST_REG: 'SUGGEST_REG', 224 /** The result is for sign up operation */ 225 SIGN_UP: 'SIGN_UP' 226 227} 228Object.freeze(AdaptiveOperation); 229 230// Levels and tags definitions for the UI configuration. 231const UIConfigTags = Object.freeze({ 232 TAG_LEVEL_SIGN_IN_VIEW: "sign_in_view", 233 TAG_LEVEL_USER_NAME_VIEW: "user_name_view", 234 TAG_LEVEL_METHODS_VIEW: "methods_view", 235 TAG_LEVEL_REGISTRATION_VIEW: "registration_view", 236 TAG_LEVEL_SIGN_UP: "sign_up_view", 237 TAG_LEVEL_MANAGE_REGISTRATION_VIEW: "manage_regs_view", 238 TAG_LEVEL_FIDO_VIEW: "fido_view", 239 TAG_LEVEL_ADAPTIVE_VIEW: "adaptive_view", 240 TAG_LEVEL_SUGGEST_REG_VIEW: "suggest_reg_view", 241 TAG_LEVEL_PENDING_AUTHS_VIEW: "pending_auths_view", 242 TAG_LEVEL_OOB_VIEW: "oob_view", 243 244 TAG_LABEL_METHOD_TITLE_MOBILE_DEVICE: "method_title_mobile_device", 245 TAG_LABEL_METHOD_TITLE_FIDO: "method_title_fido", 246 TAG_LABEL_METHOD_TITLE_EMAIL: "method_title_email", 247 TAG_LABEL_METHOD_TITLE_SMS: "method_title_sms", 248 TAG_LABEL_METHOD_TITLE_EMAIL_OR_SMS: "method_title_email_or_sms", 249 250 TAG_LABEL_SIGN_IN_EMPTY_USERNAME: "sign_in_empty_username", 251 TAG_LABEL_SIGN_IN_EMPTY_PASSWORD: "sign_in_empty_password", 252 253 TAG_LABEL_METHOD_SIGN_IN_TITLE_PREFIX: "method_sign_in_title_prefix", 254 TAG_LABEL_METHOD_TRANSACTION_TITLE_PREFIX: "method_transaction_title_prefix", 255 256 TAG_LABEL_METHOD_REGISTER_TITLE_PREFIX: "method_register_title_prefix", 257 258 TAG_LABEL_REGISTER_EMAIL_TITLE: "register_email_title", 259 TAG_LABEL_REGISTER_PHONE_TITLE: "register_phone_title", 260 261 TAG_LABEL_PENDING_AUTH_REMAINING_TIME_PREFIX: "pending_auth_remaining_time_prefix", 262 TAG_LABEL_PENDING_AUTH_PUSH_STATUS_PREFIX: "pending_auth_push_status_prefix", 263 264 TAG_LABEL_REG_PLATFORM_AUTH_TITLE: "register_platform_auth_title", 265 TAG_LABEL_REG_SECURITY_KEY_TITLE: "register_security_key_title", 266 TAG_LABEL_PASSKEY_NAME_ON_ICLOUD_KEYCHAIN: "passkey_name_on_icloud_keychain", 267 TAG_LABEL_PASSKEY_NAME_ON_CHROME_ON_MAC: "passkey_name_on_chrome_on_mac", 268 TAG_LABEL_PASSKEY_NAME_WINDOWS_HELLO: "passkey_name_windows_hello", 269 TAG_LABEL_PASSKEY_NAME_ON_GOOGLE_PW_MANAGER: "passkey_name_on_google_password_manager", 270 TAG_LABEL_PASSKEY_NAME_LOCAL_ANDROID: "passkey_name_on_local_android", 271 TAG_LABEL_REG_SECURITY_KEY_SUCCESS_MESSAGE: "register_security_key_success_message", 272 TAG_LABEL_SECURITY_KEY_NAME: "security_key_name", 273 274 TAG_IMAGE_PLATFORM_AUTHENTICATOR_REG_ICON: "platform_authenticator_reg_icon", 275 TAG_IMAGE_SECURITY_KEY_REG_ICON: "security_key_reg_icon", 276 277 TAG_LABEL_AUTHENTICATOR_USED_JUST_NOW: "used_just_now_label", 278 TAG_LABEL_AUTHENTICATOR_LAST_USED_FORMAT: "last_used_authenticator_format", 279 TAG_LABEL_AUTHENTICATOR_USED_SECOND: "second", 280 TAG_LABEL_AUTHENTICATOR_USED_SECONDS: "seconds", 281 TAG_LABEL_AUTHENTICATOR_USED_MINUTE: "minute", 282 TAG_LABEL_AUTHENTICATOR_USED_MINUTES: "minutes", 283 TAG_LABEL_AUTHENTICATOR_USED_HOUR: "hour", 284 TAG_LABEL_AUTHENTICATOR_USED_HOURS: "hours", 285 TAG_LABEL_AUTHENTICATOR_USED_DAY: "day", 286 TAG_LABEL_AUTHENTICATOR_USED_DAYS: "days", 287 TAG_LABEL_AUTHENTICATOR_USED_WEEK: "week", 288 TAG_LABEL_AUTHENTICATOR_USED_WEEKS: "weeks", 289 TAG_LABEL_AUTHENTICATOR_USED_MONTH: "month", 290 TAG_LABEL_AUTHENTICATOR_USED_MONTHS: "months", 291 TAG_LABEL_AUTHENTICATOR_USED_YEAR: "year", 292 TAG_LABEL_AUTHENTICATOR_USED_YEARS: "years", 293 294 TAG_LABEL_OOB_TIME_REMAINING: "oob_time_remaining", 295 296 // Error tags. 297 TAG_ERROR_SIGN_IN_INCORRECT_IDENTIFIER: "sign_in_incorrect_identifier", 298 TAG_ERROR_NOT_ENROLLED: "error_not_enrolled", 299 TAG_ERROR_REG_NO_MATCH: "error_reg_no_match", 300 TAG_ERROR_AUTH_NO_MATCH: "error_auth_no_match", 301 TAG_ERROR_CONNECTION: "error_connection", 302 TAG_ERROR_WRONG_ATTEMPTS: "error_wrong_attempts", 303 TAG_ERROR_SESSION_EXPIRED: "error_session_expired", 304 TAG_ERROR_KEY_DISAPPEARED_PERMANENTLY: "error_key_disappeared_permanently", 305 TAG_ERROR_NOT_INSTALLED: "error_not_installed", 306 TAG_ERROR_ALREADY_REGISTERED: "error_already_registered", 307 TAG_ERROR_DEFAULT: "error_default", 308 TAG_ERROR_SIGN_UP_SESSION_EXPIRED: "sign_up_session_expired", 309 TAG_ERROR_USER_NOT_RESPONSIVE: "error_user_not_responsive" 310}); 311 312/** 313 * @enum {string} 314 * Defines the dark mode behavior for built in SDK views. 315 */ 316const NNLDarkMode = { 317 /** Dark mode is always disabled. */ 318 OFF: 'off', 319 /** Dark mode is always enabled. */ 320 ON: 'on', 321 /** Dark mode is enabled or disabled based on browser/platform settings. */ 322 AUTOMATIC: 'automatic' 323} 324 325/** 326 * Defines keys for UI customization configuration JSON object. 327 * @class 328 * @ignore 329 */ 330const JsonWrapperKey = Object.freeze({ 331 level: 'level', 332 styles: 'styles', 333 strings: 'strings', 334 images: 'images', 335 views: 'views' 336}); 337 338/** 339 * Wraps the UI customization configuration and provides methods for searching for configuration values. 340 * @class 341 * @ignore 342 * @param {any} value - The JSON value to wrap. 343 * @param {JsonWrapper} [parent] - The parent wrapper when wrapping a child item. 344 */ 345const JsonWrapper = function(value, parent) { 346 const _this = this; 347 const mValue = typeof value === 'object' ? NNLUtils.cloneObject(value) : value; 348 const mParent = parent; 349 350 /** 351 * Recursively merges the provided wrapper into this one. 352 * @param {JsonWrapper} wrapperToBeApplied the wrapper to merge items from. 353 */ 354 this.mergeJsonObjectsRecursively = function(wrapperToBeApplied) {
355 wrapperToBeApplied.getAllKeys().forEach(function(key) { 356 const subWrapperExisting = _this.get(key); 357 const subWrapperToBeApplied = wrapperToBeApplied.get(key); 358 359 if(subWrapperExisting != null && subWrapperToBeApplied != null && subWrapperToBeApplied.isJsonObject()) { 360 subWrapperExisting.mergeJsonObjectsRecursively(subWrapperToBeApplied); 361 addValue(key, subWrapperExisting); 362 } else { 363 addValue(key, subWrapperToBeApplied); 364 } 365 }); 366 } 367 368 /** 369 * Returns wrapper item for given key. 370 * @param {string} key the key to get wrapper for. 371 * @returns {JsonWrapper} the wrapper item for given key or null if item for given key does not exist. 372 */ 373 this.get = function(key) { 374 if(mValue[key] != null) { 375 return new JsonWrapper(mValue[key], _this); 376 } 377 378 return null; 379 } 380 381 /** 382 * Returns the current wrapped value. 383 * @returns {any} the wrapped value. 384 */ 385 this.getValue = function() { 386 return mValue; 387 } 388 389 /** 390 * Returns the list of keys of the current wrapepd object. 391 * @returns {string[]} list of keys. 392 */ 393 this.getAllKeys = function() { 394 return Object.keys(mValue); 395 } 396 397 /** 398 * Checks whether the current wrapped value is a JSON object or not. 399 * @returns {boolean} true if the wrapped value is JSON object, false otherwise. 400 */ 401 this.isJsonObject = function() { 402 return typeof mValue === 'object'; 403 } 404 405 /** 406 * Inserts the wrapped value of the provided wrapper into the current wrapepd object for given key. 407 * @param {string} key the key of the value being inserted 408 * @param {JsonWrapper} wrapper the wrapper to get the value from to insert. 409 */ 410 function addValue(key, wrapper) { 411 mValue[key] = wrapper.getValue(); 412 } 413 414 /** 415 * Searches for configuration object for specified sub property inside the specified property starting from this level and going up. 416 * @param {string} property - The property to search configuration in. 417 * @param {string} subProperty - The sub property to search configuration for. 418 * @returns {object} The configuration object if found, null otherwise. 419 * @private 420 */ 421 this.searchUp = function(property, subProperty) { 422 if(subProperty == null) { 423 return null; 424 } 425 426 const element = searchLevel(property, subProperty); 427 if(element == null && mParent != null) { 428 return mParent.searchUp(property, subProperty); 429 } 430 431 return element; 432 } 433 434 /** 435 * Searches for configuration object for specified sub property inside the specified property starting from this level 436 * and going down by the specified list of levels. Returns the configuration found in the lowest level. 437 * @param {string[]} levels - The list of levels where the configuration should be searched. 438 * @param {string} property - The property to search configuration in. 439 * @param {string} subProperty - The sub property to search configuration for. 440 * @returns {object} The configuration object if found, null otherwise. 441 * @private 442 */ 443 this.searchDown = function(levels, property, subProperty) { 444 if(levels != null && levels.length > 0) { 445 const wrapper = _this.getLevel(levels[0]); 446 if(wrapper != null) { 447 const lowerElement = wrapper.searchDown(levels.slice(1), property, subProperty); 448 if(lowerElement != null) { 449 return lowerElement; 450 } 451 } 452 } 453 454 return searchLevel(property, subProperty); 455 } 456 457 /** 458 * Returns the configuration wrapper for the specified level. 459 * @param {string} level - The level to get the configuration wrapper for. 460 * @returns {JsonWrapper} The configuration wrapper if found, null otherwise. 461 * @private 462 */ 463 this.getLevel = function(level) { 464 if(level == null) { 465 return null; 466 } 467 468 const views = mValue[JsonWrapperKey.views]; 469 // If the "views" prop is not found, return null to continue the search. 470 if (views == null) { 471 return null; 472 } 473 474 const keys = Object.keys(views); 475 for(let index in keys) { 476 const key = keys[index]; 477 if(typeof views[key] === 'object') { 478 let levelConfig = new JsonWrapper(views[key], _this); 479 if(key === level) { 480 return levelConfig; 481 } else { 482 levelConfig = levelConfig.getLevel(level); 483 if(levelConfig != null) { 484 return levelConfig; 485 } 486 } 487 } 488 } 489 } 490 491 // Searches configuration on the current level. 492 function searchLevel(property, subProperty) { 493 // Get the object for specified property. 494 let element = mValue[property]; 495 496 // If a configuration for specified property was found, 497 // retrieve configuration for specified sub property. 498 if(element != null) { 499 element = element[subProperty]; 500 } 501 502 return element; 503 } 504 505 Object.freeze(this); 506} 507Object.freeze(JsonWrapper); 508 509/** 510 * Wraps an HTML element to apply customization according the "data-tag" attribute. 511 * @class 512 * @param {HTMLElement} view - The view to apply the customization to. 513 */ 514const ViewWrapper = function(view) { 515 const _this = this; 516 const mView = view; 517 const mTags = {}; 518 519 // Parse the "data-tag" attribute value if specified. 520 if(mView.dataset.tag != null) { 521 mView.dataset.tag.split(';').forEach(function(tag) { 522 const parts = tag.split(':'); 523 let key = ''; 524 let value = ''; 525 if(parts.length === 2) { 526 key = parts[0].trim(); 527 value = parts[1].trim(); 528 } 529 530 if(key !== '' && value !== '') { 531 mTags[key] = value; 532 } else {
533 console.error('The tag value "' + tag + '" is not valid for view', mView); 534 } 535 }); 536 } 537 538 /** 539 * Loads the UI customization configuration and starts applying it. 540 */ 541 this.onShow = function() { 542 const configJsonWrapper = NNLUiConfig.getJsonWrapper(); 543 if(configJsonWrapper != null) { 544 _this.customize(configJsonWrapper); 545 } 546 547 // Enable or disable dark mode based on configuration (called after customize() to be able to apply images). 548 NNLUiConfig.setDarkMode(); 549 } 550 551 /** 552 * Starts applying customization from the specified configuration to this view and its children 553 * @private 554 * @param {JsonWrapper} wrapper - The customization configuration 555 */ 556 this.customize = function(wrapper) { 557 // If a level tag is specified, try to retrieve and use configuration for that level. 558 const levelWrapper = wrapper.getLevel(mTags[JsonWrapperKey.level]); 559 if(levelWrapper != null) { 560 wrapper = levelWrapper; 561 } 562 563 const styleJson = wrapper.searchUp(JsonWrapperKey.styles, mTags[JsonWrapperKey.styles]); 564 if(styleJson != null) { 565 applyStyles(styleJson); 566 } 567 568 const text = wrapper.searchUp(JsonWrapperKey.strings, mTags[JsonWrapperKey.strings]); 569 if(text != null) { 570 applyText(text); 571 } 572 573 const lightImage = wrapper.searchUp(JsonWrapperKey.images, mTags[JsonWrapperKey.images]); 574 const darkImage = wrapper.searchUp(JsonWrapperKey.images, 'dark_' + mTags[JsonWrapperKey.images]); 575 applyImage(lightImage, darkImage); 576 577 // Apply configuration to the child elements too. 578 for(let index = 0; index < mView.children.length; ++index) { 579 (new ViewWrapper(mView.children[index])).customize(wrapper); 580 } 581 } 582 583 function applyStyles(styleJson) { 584 // The the current view is the SDK defined custom toggle, apply the the values as CSS properties. 585 if(mTags[JsonWrapperKey.styles] === 'toggle') { 586 if(styleJson.thumb_enable_color != null) { 587 mView.style.setProperty('--light-thumb-enabled-color', convertColor(styleJson.thumb_enable_color)); 588 } 589 if(styleJson.dark_thumb_enable_color != null) { 590 mView.style.setProperty('--dark-thumb-enabled-color', convertColor(styleJson.dark_thumb_enable_color)); 591 } 592 593 if(styleJson.thumb_disable_color != null) { 594 mView.style.setProperty('--light-thumb-disabled-color', convertColor(styleJson.thumb_disable_color)); 595 } 596 if(styleJson.dark_thumb_disable_color != null) { 597 mView.style.setProperty('--dark-thumb-disabled-color', convertColor(styleJson.dark_thumb_disable_color)); 598 } 599 600 if(styleJson.track_enable_color != null) { 601 mView.style.setProperty('--light-track-enabled-color', convertColor(styleJson.track_enable_color)); 602 } 603 if(styleJson.dark_track_enable_color != null) { 604 mView.style.setProperty('--dark-track-enabled-color', convertColor(styleJson.dark_track_enable_color)); 605 } 606 607 if(styleJson.track_disable_color != null) { 608 mView.style.setProperty('--light-track-disabled-color', convertColor(styleJson.track_disable_color)); 609 } 610 if(styleJson.dark_track_disable_color != null) { 611 mView.style.setProperty('--dark-track-disabled-color', convertColor(styleJson.dark_track_disable_color)); 612 } 613 614 if(styleJson.border_color != null) { 615 mView.style.setProperty('--light-border-color', convertColor(styleJson.border_color)); 616 } 617 if(styleJson.dark_border_color != null) { 618 mView.style.setProperty('--dark-border-color', convertColor(styleJson.dark_border_color)); 619 } 620 621 if(styleJson.border_size != null) { 622 mView.style.setProperty('--toggle-border-width', styleJson.border_size + 'px'); 623 } 624 625 if(styleJson.corner_radius != null) { 626 mView.style.setProperty('--toggle-border-radius', styleJson.corner_radius + 'px'); 627 } 628 } else if (mTags[JsonWrapperKey.styles] === 'progress_bar') { 629 if(styleJson.progress_bar_color != null) { 630 mView.style.setProperty('--light-progress-color', convertColor(styleJson.progress_bar_color)); 631 } 632 if(styleJson.dark_progress_bar_color != null) { 633 mView.style.setProperty('--dark-progress-color', convertColor(styleJson.dark_progress_bar_color)); 634 } 635 } else { 636 if(styleJson.background_color != null) { 637 mView.style.setProperty('--light-background-color', convertColor(styleJson.background_color)); 638 } 639 if(styleJson.dark_background_color != null) { 640 mView.style.setProperty('--dark-background-color', convertColor(styleJson.dark_background_color)); 641 } 642 643 if(styleJson.disable_color != null) { 644 const disabledState = getColorAndOpacity(styleJson.disable_color); 645 mView.style.setProperty('--light-disabled-background-color', disabledState.color); 646 mView.style.setProperty('--light-disabled-opacity', disabledState.o
646pacity); 647 } 648 if(styleJson.dark_disable_color != null) { 649 const disabledState = getColorAndOpacity(styleJson.dark_disable_color); 650 mView.style.setProperty('--dark-disabled-background-color', disabledState.color); 651 mView.style.setProperty('--dark-disabled-opacity', disabledState.opacity); 652 } 653 654 if(styleJson.text_color != null) { 655 mView.style.setProperty('--light-text-color', convertColor(styleJson.text_color)); 656 } 657 if(styleJson.dark_text_color != null) { 658 mView.style.setProperty('--dark-text-color', convertColor(styleJson.dark_text_color)); 659 } 660 661 if(styleJson.border_color != null) { 662 mView.style.setProperty('--light-border-color', convertColor(styleJson.border_color)); 663 } 664 if(styleJson.dark_border_color != null) { 665 mView.style.setProperty('--dark-border-color', convertColor(styleJson.dark_border_color)); 666 } 667 668 if(styleJson.border_size != null) { 669 mView.style.borderWidth = styleJson.border_size + 'px'; 670 } 671 672 if(styleJson.text_alignment != null) { 673 mView.style.textAlign = styleJson.text_alignment; 674 mView.dataset.textAlign = styleJson.text_alignment; 675 } 676 677 if(styleJson.corner_radius != null) { 678 mView.style.borderRadius = styleJson.corner_radius + 'px'; 679 } 680 } 681 } 682 683 // Sets the given text as a placeholder for input elements and as display text for other elements. 684 function applyText(text) { 685 if(mView instanceof HTMLInputElement) { 686 mView.placeholder = text; 687 } else { 688 mView.innerText = text; 689 } 690 } 691 692 // Sets the given light and dark images as custom properties to the view. 693 function applyImage(lightImage, darkImage) { 694 if(lightImage != null) { 695 if(lightImage !== '') { 696 mView.dataset.lightImage = NNLUtils.getAbsoluteImageURL(lightImage); 697 mView.dataset.darkImage = mView.dataset.lightImage; 698 } else { 699 // If image source is set to empty value, it should be hidden. 700 mView.style.display = 'none'; 701 // In that case ignore dark image as well. 702 darkImage = null; 703 } 704 } 705 if(darkImage != null) { 706 mView.dataset.darkImage = NNLUtils.getAbsoluteImageURL(darkImage); 707 } 708 } 709 710 function convertColor(colorString) { 711 // The #AARRGGBB should be converted to #RRGGBBAA 712 if(colorString.length === 9) { 713 return '#' + colorString.slice(3) + colorString.slice(1, 3); 714 } 715 716 // #ARGB should be converted to #RGBA 717 if(colorString.length === 5) { 718 return '#' + colorString.slice(2) + colorString.slice(1, 2); 719 } 720 721 return colorString; 722 } 723 724 function getColorAndOpacity(colorString) { 725 let opacity = null; 726 727 if(colorString.length == 9) { 728 // Parse from double digit hex. 729 opacity = parseInt(colorString.slice(1, 3), 16); 730 colorString = '#' + colorString.slice(3); 731 } else if(colorString.length == 5) { 732 opacity = colorString.slice(1, 2); 733 // Make a double digit hex, then parse. 734 opacity = parseInt(opacity + opacity, 16); 735 colorString = '#' + colorString.slice(2); 736 } 737 738 if(opacity == null || isNaN(opacity)) { 739 opacity = 1; 740 } else { 741 opacity = (opacity/255).toFixed(2); 742 } 743 744 return { 745 color: colorString, 746 opacity: opacity 747 } 748 } 749 750 function getImageURL(imageURL) { 751 return 'url("' + NNLUtils.getAbsoluteImageURL(imageURL) + '")'; 752 } 753 754 Object.freeze(this); 755} 756 757/** 758* Defines the WebAuthn user mediation for Conditional UI. 759* @type {string} 760*/ 761const USER_MEDIATION_CONDITIONAL = "conditional"; 762 763/** 764 * @classdesc A singleton factory class to create and provide {@link MethodUI} implementations for particular method types. 765 * @interface 766 */ 767const MethodUIFactory = (function() { 768 var mInstance = null; 769 /** 770 * This method implementation should create {@link MethodUI} instance for the given type. 771 * <br/>The implementation can leverage the default implementation of {@link MethodUI} for the following types. 772 * Method type | Default implementation 773 * ------------------------------- | ---------------------- 774 * {@link AdaptiveType FIDO_OOB} | OOBMethodUI({@link Extras extras}) 775 * {@link AdaptiveType EMAIL, SMS} | OtpMethodUI({@link Extras extras}) 776 * See the example below for details. 777 * 778 * @example <caption>Using custom UI with default implementation of {@link MethodUI}.</caption> 779 * // Get the default factory in order to forward calls to it when needed. 780 * var defaultFactory = MethodUIFactory.getInstance(); 781 * // Create instance of custom factory and set it as current. 782 * MethodUIFactory.setInstance(new CustomMethodUIFactory()); 783 * var CustomMethodUIFactory = function() { 784 * this.createMethodUI = function(methodType, extras, sessionData) { 785 * // For OOB, E-mail OTP and SMS OTP method types create instance of default MethodUI with custom UI implementation. 786 * if(methodType === AdaptiveType.FIDO_OOB) { 787 * // CustomOobUI implements {@link IOobLiveData} interface. 788 * var oobMethodUI = new OOBMethodUI(extras, sessionData); 789 * var oobLiveData = new CustomOobUI(oobMethodUI); 790 * oobMethodUI.setLiveData(oobLiveData); 791 * return oobMethodUI; 792 * } 793 * 794 * if(methodType === AdaptiveType.EMAIL || methodType === AdaptiveType.SMS) { 795 * // CustomOtpUI implements {@link IOtpLiveData} interface. 796 * var otpMethodUI = new OtpMethodUI(extras, sessionData); 797 * var otpLiveData = new CustomOtpUI(otpMethodUI); 798 * otpMethodUI.setLiveData(otpLiveData); 799 * return otpMethodUI; 800 * } 801 * 802 * // Forward call to default factory for other method types. 803 * return defaultFactory.createMethodUI(methodType, extras, sessionData); 804 * } 805 * 806 * // Forward call to the default factory for the other functions. 807 * this.isSupported = function(methodType) { 808 * return defaultFactory.isSupported(methodType); 809 * } 810 * 811 * this.getInstructions = function(methodType) { 812 * return defaultFactory.getInstructions(methodType); 813 * } 814 * 815 * this.getGroups = function() { 816 * return defaultFactory.getGroups(); 817 * } 818 * 819 * this.getTitle = function(methodGroup, opType) { 820 * return defaultFactory.getTitle(methodGroup, opType); 821 * } 822 * 823 * this.getIcon = function(methodGroup) { 824 * return defaultFactory.getIcon(methodGroup); 825 * } 826 * } 827 * 828 * @method MethodUIFactory#createMethodUI 829 * @param {AdaptiveType} methodType The type of desired MethodUI. 830 * @param {Extras} [extras] Extra options. 831 * @param {SessionData} [sessionData] Object containing session information as well as custom properties set by API Server plugins. 832 * @returns {MethodUI} MethouUI class instance of given type. 833 */ 834 835 /** 836 * This method implementation should return a boolean value indicating whether 837 * the factory supports the given method type or not. 838 * @method MethodUIFactory#isSupported 839 * @param {AdaptiveType} methodType Method type to check. 840 * @returns {boolean} true if method is supported or false otherwise. 841 */ 842 843 /** 844 * This method implementation should return an instruction message for given method type. 845 * @method MethodUIFactory#getInstructions 846 * @param {AdaptiveType} methodType Method type to get instruction for. 847 * @returns {string}
847 Instructions string for method type. 848 */ 849 850 /** 851 * This method implementation should return the list of supported authentication method groups. 852 * Authentication methods can be grouped if they utilize the same UI. 853 * @method MethodUIFactory#getGroups 854 * @returns {AdaptiveMethod[][]} The list of method groups. 855 */ 856 857 /** 858 * This method implementation should return the title of a group of authentication methods 859 * for given operation. 860 * @param {AdaptiveMethod[]} methodGroup The group of authentication methods to get title for. 861 * @param {OpType} opType The operation to get title for. 862 * @method MethodUIFactory#getTitle 863 * @returns {string} The authentication methods' group title. 864 */ 865 866 /** 867 * This method implementation should return the icon URL for a group of authentication methods. 868 * The URL can be either an http URL or a data URL. 869 * @param {AdaptiveMethod[]} methodGroup The group of authentication methods to get icon URL for. 870 * @method MethodUIFactory#getIcon 871 * @returns {string} The authentication method group's icon URL, null if not specified. 872 */ 873 874 return { 875 /** 876 * Returns the MethodUIFactory instance. 877 * If no instance was set using setInstance method, creates and returns {@link DefaultMethodUIFactory} instance 878 * @returns {MethodUIFactory} Stored instance of MethodUIFactory. 879 * @memberof MethodUIFactory 880 */ 881 getInstance: function() { 882 if(mInstance === null) { 883 mInstance = new DefaultMethodUIFactory(); 884 } 885 886 return mInstance; 887 }, 888 /** 889 * Sets the MethodUIFactory instance. 890 * @param {MethodUIFactory} instance The new instance. 891 * @memberof MethodUIFactory 892 */ 893 setInstance: function(instance) { 894 NNLUtils.validateArgument('instance', instance, 'object', null, false); 895 mInstance = instance; 896 } 897 }; 898})(); 899 900/** 901 * @classdesc Default implementation of MethodUIFactory. 902 * @class 903 * @implements MethodUIFactory 904 */ 905var DefaultMethodUIFactory = function() { 906 /** 907 * Creates and returns default {@link MethodUI} instance for given type. 908 * @param {AdaptiveType} methodType The type of desired MethodUI. 909 * @param {Extras} [extras] Extra options. 910 * @param {SessionData} [sessionData] Object containing session information as well as custom properties set by API Server plugins. 911 * @returns {MethodUI} MethodUI class instance of given type. 912 */ 913 this.createMethodUI = function(methodType, extras, sessionData) { 914 if(methodType === AdaptiveType.EMAIL || methodType === AdaptiveType.SMS) { 915 var otpMethodUI = new OtpMethodUI(extras, sessionData); 916 var otpLiveData = new DefaultOtpUI(otpMethodUI); 917 otpMethodUI.setLiveData(otpLiveData); 918 return otpMethodUI; 919 } 920 921 if(methodType === AdaptiveType.FIDO) { 922 return new FIDOMethodUI(extras, sessionData); 923 } 924 925 if(methodType === AdaptiveType.FIDO_OOB) { 926 var oobMethodUI = new OOBMethodUI(extras, sessionData); 927 var oobLiveData = new DefaultOobUI(oobMethodUI); 928 oobMethodUI.setLiveData(oobLiveData); 929 return oobMethodUI; 930 } 931 932 throw {outcome: Outcome.NO_MATCH}; 933 } 934 935 /** 936 * Indicates whether the given method type is supported by the default factory or not. 937 * @param {AdaptiveType} methodType Method type to check. 938 * @returns {boolean} true if method is supported or false otherwise. 939 */ 940 this.isSupported = function(methodType) { 941 switch(methodType) { 942 case AdaptiveType.EMAIL: 943 case AdaptiveType.SMS: 944 case AdaptiveType.FIDO_OOB: 945 return true; 946 947 case AdaptiveType.FIDO: 948 // If FIDO2 is not supported in a web browser, then "FIDO Auth" method is not supported. 949 return (nnl.isCordovaApp || window.PublicKeyCredential != null); 950 951 default: 952 return false; 953 } 954 } 955 956 /** 957 * Returns instruction message for given method type. 958 * @param {AdaptiveType} methodType Method type to get instruction for. 959 * @returns {string} Instructions string for method type. 960 */ 961 this.getInstructions = function(methodType) { 962 switch(methodType) { 963 case AdaptiveType.EMAIL: 964 return NNLUiConfig.getText( 965 [UIConfigTags.TAG_LEVEL_REGISTRATION_VIEW, UIConfigTags.TAG_LEVEL_ADAPTIVE_VIEW],
966 UIConfigTags.TAG_LABEL_REGISTER_EMAIL_TITLE, 967 'Add your email address to help secure your account' 968 ); 969 case AdaptiveType.SMS: 970 return NNLUiConfig.getText( 971 [UIConfigTags.TAG_LEVEL_REGISTRATION_VIEW, UIConfigTags.TAG_LEVEL_ADAPTIVE_VIEW], 972 UIConfigTags.TAG_LABEL_REGISTER_PHONE_TITLE, 973 'Add your phone number to help secure your account' 974 ); 975 976 default: 977 return ''; 978 } 979 } 980 981 /** 982 * The function returns the list of supported authentication method groups. 983 * Authentication methods can be grouped if they utilize the same UI. 984 * @returns {AdaptiveMethod[][]} The list of method groups. 985 */ 986 this.getGroups = function() { 987 return [ 988 [ 989 {type: AdaptiveType.EMAIL, name: AdaptiveType.names[AdaptiveType.EMAIL]}, 990 {type: AdaptiveType.SMS, name: AdaptiveType.names[AdaptiveType.SMS]}, 991 ] 992 ]; 993 } 994 995 /** 996 * This method returns the title of a group of authentication methods for given operation type. 997 * @param {AdaptiveMethod[]} methodGroup The group of authentication methods to get title for. 998 * @param {OpType} opType The operation type to get title for. 999 * @returns {string} The authentication methods' group title. 1000 */ 1001 this.getTitle = function(methodGroup, opType) { 1002 if(methodGroup == null || methodGroup.length === 0) { 1003 console.error('Empty method group' + opType); 1004 throw {outcome: Outcome.FAILURE}; 1005 } 1006 1007 if(methodGroup[0].type === AdaptiveType.FIDO) { 1008 const policyConfig = NNLUtils.getPolicyConfig(methodGroup[0].name); 1009 if(policyConfig != null) { 1010 return policyConfig.title; 1011 } 1012 } 1013 1014 // First set prefix based on operation type. 1015 let defaultMethodFormat = null; 1016 let methodFormatTag = null; 1017 let methodFormatLevels = null; 1018 let methodsLevel = [UIConfigTags.TAG_LEVEL_SIGN_IN_VIEW]; 1019 switch(opType) { 1020 case OpType.REG: 1021 methodsLevel = [UIConfigTags.TAG_LEVEL_SUGGEST_REG_VIEW]; 1022 break; 1023 1024 case OpType.AUTH: 1025 defaultMethodFormat = 'Sign in with {0}'; 1026 methodFormatTag = UIConfigTags.TAG_LABEL_METHOD_SIGN_IN_TITLE_PREFIX; 1027 methodFormatLevels = [UIConfigTags.TAG_LEVEL_SIGN_IN_VIEW, UIConfigTags.TAG_LEVEL_METHODS_VIEW]; 1028 break; 1029 1030 case OpType.TRANS: 1031 defaultMethodFormat = 'Authorize with {0}'; 1032 methodFormatTag = UIConfigTags.TAG_LABEL_METHOD_TRANSACTION_TITLE_PREFIX; 1033 methodFormatLevels = [UIConfigTags.TAG_LEVEL_SIGN_IN_VIEW, UIConfigTags.TAG_LEVEL_METHODS_VIEW]; 1034 break; 1035 1036 default: 1037 console.error('Unsupported operation type' + opType); 1038 throw {outcome: Outcome.FAILURE}; 1039 } 1040 1041 // Then set title based on group type and size. 1042 var groupTitle = ''; 1043 // Check the type of the first method in the group. 1044 switch(methodGroup[0].type) { 1045 // As grouping it not supported for FIDO and FIDO_OOB methods, they should be the only 1046 // method in the group, so checking the type of only the first method is enough. 1047 case AdaptiveType.FIDO: 1048 groupTitle = NNLUiConfig.getText( 1049 methodsLevel, 1050 UIConfigTags.TAG_LABEL_METHOD_TITLE_FIDO, 1051 'FIDO Auth' 1052 ); 1053 break; 1054 1055 case AdaptiveType.FIDO_OOB: 1056 groupTitle = NNLUiConfig.getText( 1057 methodsLevel, 1058 UIConfigTags.TAG_LABEL_METHOD_TITLE_MOBILE_DEVICE, 1059 'Mobile Device' 1060 ); 1061 break; 1062 1063 // The SMS and EMAIL methods can be grouped together, so for both cases we should check if 1064 // the other one is in the group too (group size is greater than 1). 1065 case AdaptiveType.EMAIL: 1066 case AdaptiveType.SMS: 1067 if(methodGroup.length > 1) { 1068 groupTitle = NNLUiConfig.getText( 1069 methodsLevel, 1070 UIConfigTags.TAG_LABEL_METHOD_TITLE_EMAIL_OR_SMS, 1071 'Email or SMS' 1072 ); 1073 } else if(methodGroup[0].type === AdaptiveType.EMAIL) { 1074 groupTitle = NNLUiConfig.getText( 1075 methodsLevel, 1076 UIConfigTags.TAG_LABEL_METHOD_TITLE_EMAIL, 1077 'Email' 1078 ); 1079 } else { 1080 groupTitle = NNLUiConfig.getText( 1081 methodsLevel, 1082 UIConfigTags.TAG_LABEL_METHOD_TITLE_SMS, 1083 'SMS' 1084 ); 1085 } 1086 break; 1087 1088 default: 1089 console.error('Unsupported method type ' + methodGroup[0].type); 1090 throw {outcome: Outcome.FAILURE}; 1091 } 1092 1093 if (defaultMethodFormat == null && 1094 methodFormatLevels == null && 1095 methodFormatTag == null) { 1096 1097 return groupTitle; 1098 } 1099 1100 return NNLUiConfig.getText( 1101 methodFormatLevels, 1102 methodFormatTag, 1103 defaultMethodFormat, 1104 groupTitle 1105 ); 1106 } 1107 1108 /** 1109 * This method returns the icon URL for a group of authentication methods. 1110 * Looks for the icon in the {@link AppSdkConfig|UI customization configuration or AppSdkConfig.policyMap}. 1111 * @param {AdaptiveMethod[]} methodGroup The group of authentication methods to get icon URL for. 1112 * @returns {string} The authentication method group's icon URL, null if not specified. 1113 */ 1114 this.getIcon = function(methodGroup) { 1115 1116 if(methodGroup == null || methodGroup.length === 0) { 1117 console.error('Empty method group'); 1118 throw {outcome: Outcome.FAILURE}; 1119 } 1120 1121 if(methodGroup[0].type === AdaptiveType.FIDO) { 1122 const policyConfig = NNLUtils.getPolicyConfig(methodGroup[0].name); 1123 if(policyConfig != null) { 1124 return policyConfig.icon; 1125 } 1126 } 1127 1128 return null; 1129 } 1130 1131 Object.freeze(this); 1132} 1133 1134/** 1135 * This interface defines methods for displaying OOB operation related UI. 1136 * @interface IOobLiveData 1137 */ 1138 1139/** 1140 * This class contains information about started OOB operation. 1141 * @property {QRType} qrType Type of the QR code contained in the qrImage property. 1142 * @property {string} qrImage Base64 PNG image data of QR code. 1143 * @property {boolean}
1143 pushSent Indicates whether the operation was started by sending a push notification or not. 1144 * @property {number} remainingTimeMillis The operation expiration time in milliseconds. 1145 * @property {string} rawData The raw QR data received from the server. 1146 */ 1147 var OobInformation = function() { 1148 this.qrType = ''; 1149 this.qrImage = ''; 1150 this.pushSent = false; 1151 this.remainingTimeMillis = 0; 1152 this.rawData = null; 1153} 1154 1155/** 1156 * This method will be called when an OOB operation has started. 1157 * @method IOobLiveData#onOperationStarted 1158 * @param {OperationType} operation The OOB operation type. 1159 * @param {OobInformation} oobInformation Structure containing operation related information. 1160 */ 1161 1162/** 1163 * This method will be called when the user has scanned the QR code or has opened the push notification. 1164 * @method IOobLiveData#onOobReceived 1165 * @param {object} deviceInfo Object containing information about the user's device 1166 * @param {string} deviceInfo.info The name of the device. 1167 */ 1168 1169/** 1170 * This method will be called when the OOB operation is completed. 1171 * @method IOobLiveData#onOperationCompleted 1172 * @param {Outcome} outcome The operation outcome. 1173 * @param {string} [message] Error message. 1174 */ 1175 1176/** 1177 * This method will be called to notify that the UI should be closed 1178 * @method IOobLiveData#onFinish 1179 */ 1180 1181/** 1182 * This interface defines a method for displaying transaction UI 1183 * @interface ITransactionUI 1184 */ 1185 1186/** 1187 * This method implementation should display a Transaction Authorization UI 1188 * @method ITransactionUI#openUI 1189 * @param {string} transactionContent base64 encoded transaction content 1190 * @param {string} contentType The MIME-Type of the content, e.g. "text/plain" 1191 * @returns {Promise} A promise that will be resolved with {Outcome:SUCCESS} or {Outcome:CANCELED} indicating user's choice. 1192 */ 1193 1194/** 1195 * Default implementation of ITransactionUI. 1196 * @implements {ITransactionUI} 1197 */ 1198const DefaultTransactionUI = function() { 1199 // The stored timer for expiration 1200 let mTimer = null; 1201 1202 // The amount of time with milliseconds for the "mTimer" expiration 1203 let mExpirationTime = 3 * 60 * 1000; 1204 1205 var popup, authorizeBtn, declineBtn; 1206 1207 var isReady = NNLUtils.loadHTML('transaction-ui.html', 'nnl-transaction-ui-section') 1208 .then(function() { 1209 document.querySelector('#nnl-transaction-text').innerHTML = ''; 1210 popup = document.getElementById('nnl-transaction-ui'); 1211 authorizeBtn = document.getElementById('nnl-transaction-authorize'); 1212 declineBtn = document.getElementById('nnl-transaction-decline'); 1213 }); 1214 1215 // Reference to the "resolve" function of the Promise returned by openUI. 1216 var returnResult; 1217 1218 // Authorize button click handler. 1219 function onAuthorize() { 1220 if (mTimer) { 1221 clearTimeout(mTimer); 1222 mTimer = null; 1223 } 1224 1225 hideUI(); 1226 returnResult(Outcome.SUCCESS); 1227 } 1228 1229 // Decline button click handler. 1230 function onDecline() { 1231 if (mTimer) { 1232 clearTimeout(mTimer); 1233 mTimer = null; 1234 } 1235 1236 hideUI(); 1237 returnResult(Outcome.CANCELED); 1238 } 1239 1240 // Timer expiration handler. 1241 function onTimeOut() { 1242 hideUI(); 1243 returnResult(Outcome.CANCELED); 1244 } 1245 1246 function hideUI() { 1247 // Hide the UI and remove click handlers. 1248 NNLUtils.showModal(popup, false); 1249 authorizeBtn.removeEventListener('click', onAuthorize); 1250 declineBtn.removeEventListener('click', onDecline); 1251 } 1252 1253 this.openUI = function(transactionText) { 1254 return isReady.then(function() { 1255 // Add click handlers 1256 authorizeBtn.addEventListener('click', onAuthorize); 1257 declineBtn.addEventListener('click', onDecline); 1258 // Setting the expiration timer 1259 mTimer = setTimeout(onTimeOut, mExpirationTime); 1260 // Display UI 1261 var decodedText = NNLBase64.toString(transactionText); 1262 document.querySelector('#nnl-transaction-text').innerText = decodedText; 1263 1264 new ViewWrapper(popup).onShow(); 1265 NNLUtils.showModal(popup, true); 1266 1267 return new Promise(function(resolve) { 1268 returnResult = resolve; 1269 }); 1270 }); 1271 } 1272 1273 Object.freeze(this); 1274} 1275 1276/** 1277 * @classdesc A singleton factory class to create and provide {@link ITransactionUI} implementations. 1278 * @interface 1279 */ 1280const TransactionUIFactory = (function() { 1281 var mInstance = null; 1282 1283 return { 1284 /** 1285 * Returns the TransactionUIFactory instance. 1286 * If no instance was set using setInstance method, creates and returns DefaultTransactionUIFactory instance 1287 * @returns {TransactionUIFactory} Stored instance of TransactionUIFactory. 1288 * @memberof TransactionUIFactory 1289 */ 1290 getInstance: function() { 1291 if(mInstance === null) { 1292 mInstance = new DefaultTransactionUIFactory(); 1293 } 1294 1295 return mInstance; 1296 }, 1297 /** 1298 * Sets the TransactionUIFactory instance. 1299 * @param {TransactionUIFactory} instance The new instance. 1300 * @memberof TransactionUIFactory 1301 */ 1302 setInstance: function(instance) { 1303 NNLUtils.validateArgument('instance', instance, 'object', null, false); 1304 mInstance = instance; 1305 } 1306 }; 1307})(); 1308Object.freeze(TransactionUIFactory); 1309 1310/** 1311 * This method implementation should create and return {@link ITransactionUI} implementation. 1312 * @method TransactionUIFactory#createTransactionUIInstance 1313 * @returns {ITransactionUI} The created instance. 1314 */ 1315 1316/** 1317 * @classdesc Default implementation of TransactionUIFactory. 1318 * @class 1319 * @implements TransactionUIFactory 1320 */ 1321const DefaultTransactionUIFactory = function() { 1322 this.createTransactionUIInstance = function() { 1323 return new DefaultTransactionUI(); 1324 } 1325 1326 Object.freeze(this); 1327} 1328 1329/** 1330 * This interface defines methods for displaying a prompt to ask the user to try 1331 * FIDO authentication in cases when the user has a registration but it is unclear if it is 1332 * registered on the same device where the authentication is being performed. 1333 * @interface ITryFidoUi 1334 */ 1335 1336/** 1337 * The implementation should ask the user to try FIDO authentication as it may be available on the current device. 1338 * @method ITryFidoUi#openUI 1339 * @returns {Promise} A promise that should always resolve either with {@link Outcome|Outcome.SUCCESS} when the user 1340 * agrees to try FIDO authentication or with {@link Outcome|Outcome.CANCELED} when the user declines the prompt. 1341 */ 1342 1343/** 1344 * This interface defines a method for creating {@link ITryFidoUi} instance. 1345 * @interface ITryFidoUiFactory 1346 */ 1347 1348/** 1349 * The implementation should create and return {@link ITryFidoUi} instance. 1350 * This function will be called in case when the user's registration is most likely 1351 * available on the current device for authentication (e.g. a synced passkey). 1352 * @method ITryFidoUiFactory#createTryFidoUi 1353 * @returns {ITryFidoUi} The {@link ITryFidoUi} instance. 1354 */ 1355 1356/** 1357 * The implementation should create and return {@link ITryFidoUi} instance. 1358 * This function will be called in case when the user's registration is most likely 1359 * available on another device (e.g. a passkey created on mobile device by scanning a QR code). 1360 * @method ITryFidoUiFactory#createTryHybridUi 1361 * @returns {ITryFidoUi} The {@link ITryFidoUi} instance. 1362 */ 1363 1364/** 1365 * A singleton factory class to create and provide {@link ITryFidoUiFactory} implementations. 1366 * @class 1367 * @hideconstructor 1368 */ 1369const TryFidoUiFactory = new (function() { 1370 var mInstance = null; 1371 1372 /** 1373 * Returns {@link ITryFidoUiFactory} instance. 1374 * If no instance was set using the setInstance method, creates and returns {@link DefaultTryFidoUiFactory} instance. 1375 * @alias TryFidoUiFactory.getInstance 1376 * @returns {ITryFidoUiFactory} The factory instance to use. 1377 */ 1378 this.getInstance = function() { 1379 if(mInstance == null) { 1380 mInstance = new DefaultTryFidoUiFactory(); 1381 } 1382 1383 return mInstance; 1384 } 1385 1386 /** 1387 * Sets the {@link ITryFidoUiFactory} instance. 1388 * @alias TryFidoUiFactory.setInstance 1389 * @param {ITryFidoUiFactory} instance The new instance. 1390 */ 1391 this.setInstance = function(instance) { 1392 NNLUtils.validateArgument('instance', instance, 'object', null, false); 1393 mInstance = instance; 1394 } 1395 1396 Object.freeze(this); 1397})(); 1398 1399/** 1400 * Default implementation of {@link ITryFidoUiFactory}. 1401 * @class 1402 * @implements ITryFidoUiFactory 1403 */ 1404function DefaultTryFidoUiFactory() { 1405 /** 1406 * Creates an instance of {@link DefaultTryFidoUi} for the case when the user's 1407 * registration is most likely available on the current device for 1408 * authentication (e.g. a synced passkey). 1409 */ 1410 this.createTryFidoUi = function() { 1411 return new DefaultTryFidoUi(false); 1412 } 1413 1414 /** 1415 * Creates an instance of {@link DefaultTryFidoUi} for the case when the user's 1416 * registration is most likely available on another device (e.g. a passkey created 1417 * on mobile device by scanning a QR code). 1418 */ 1419 this.createTryHybridUi = function() { 1420 return new DefaultTryFidoUi(true); 1421 } 1422} 1423 1424/** 1425 * Default implementation of {@link ITryFidoUi}. 1426 * Handles both Try FIDO and Try Hybrid cases. 1427 * @class 1428 * @implements {ITryFidoUi} 1429 * @param {boolean} isHybridMode indicates whether the prompt should be 1430 * for the current device (`false`) or another device (`true`). 1431 */ 1432const DefaultTryFidoUi = function(isHybridMode) { 1433 const mIsReady = NNLUtils.loadHTML('try-fido-ui.html', 'nnl-try-fido-ui-section'); 1434 let mDialog, mAuthorizeBtn, mDeclineBtn, mPromise; 1435 1436 // Authorize button click handler. 1437 function onAuthorize() { 1438 hideUI(); 1439 mPromise.resolve(Outcome.SUCCESS); 1440 } 1441 1442 // Decline button click handler. 1443 function onDecline() { 1444 hideUI(); 1445 mPromise.resolve(Outcome.CANCELED); 1446 } 1447 1448 function hideUI() { 1449 // Hide the UI and remove click handlers. 1450 NNLUtils.showModal(mDialog, false); 1451 mAuthorizeBtn.removeEventListener('click', onAuthorize); 1452 mDeclineBtn.removeEventListener('click', onDecline); 1453 } 1454 1455 /** 1456 * Asks the user to try FIDO authentication indicating that a registration 1457 * may be available on the current or another device. 1458 * @returns {Promise} A promise that resolves with {@link Outcome|Outcome.SUCCESS} when the user 1459 * agrees to try FIDO authentication or with {@link Outcome|Outcome.CANCELED} when the user declines the prompt. 1460 */ 1461 this.openUI = function() { 1462 mPromise = new PromiseWrapper(); 1463 1464 mIsReady.then(function() {
1465 mDialog = document.querySelector(isHybridMode ? '#nnl-try-hybrid-ui' : '#nnl-try-fido-ui'); 1466 mAuthorizeBtn = mDialog.querySelector('.nnl-try-fido-authorize'); 1467 mDeclineBtn = mDialog.querySelector('.nnl-try-fido-decline'); 1468 1469 // Add click handlers 1470 mAuthorizeBtn.addEventListener('click', onAuthorize); 1471 mDeclineBtn.addEventListener('click', onDecline); 1472 1473 new ViewWrapper(mDialog).onShow(); 1474 NNLUtils.showModal(mDialog, true); 1475 }); 1476 1477 return mPromise.get(); 1478 } 1479 1480 Object.freeze(this); 1481} 1482 1483/** 1484 * This interface defines methods to get operation results information 1485 * @interface IOperationResultListener 1486 */ 1487 1488/** 1489 * This method implementation handles operations result on complete 1490 * @method IOperationResultListener#onComplete 1491 * @param {FidoOperationType} operationType The type of operation 1492 * @param {OperationData} operationData The operation data object that contains the result data of completed operation 1493 * @param {Extras} extras Extra options specified for the operation 1494 * @returns {Promise|undefined} If the implementation is doing an asynchronous job, it can return a Promise that should 1495 * be resolved when the job is done (it should not be rejected). 1496 */ 1497 1498/** 1499 * OperationData object provided in {@link IOperationResultListener#onComplete|IOperationResultListener.onComplete} method. 1500 * <br>The following table illustrates which properties will be set depending on operation. 1501 * {@link FidoOperationType} | Properties 1502 * ---------------------------- | -------------------------------------------- 1503 * FidoOperationType.REG | protocolFamily, additionalInfo, authenticatorAttachment 1504 * FidoOperationType.AUTH | userName, protocolFamily, additionalInfo 1505 * FidoOperationType.LIST_REG | registrations 1506 * FidoOperationType.DELETE_REG | handle 1507 * FidoOperationType.RENAME_REG | handle 1508 * 1509 * @type {object} 1510 * @name OperationData 1511 * @property {string} [protocolFamily] Either "uaf" or "web". 1512 * @property {object} [additionalInfo] Additional information from Server. 1513 * @property {string} [authenticatorAttachment] Either "platform", "cross-platform" or "unknown". Present only when protocolFamily is "web". 1514 * @property {FidoRegistration[]} [registrations] A list of user's active registered FIDO authenticators. 1515 * @property {object} [handle] Handle of the deleted or renamed authenticator. Will be null when all authenticators were deleted. 1516 * @property {string} [userName] The userName of the user that performed the operation. 1517 * @property {object} [quickData] Data returned from server for quick Authentication. 1518 */ 1519 1520/** 1521 * This method implementation handles operations result on failure 1522 * @method IOperationResultListener#onFailure 1523 * @param {FidoOperationType} operationType The operation type 1524 * @param {Error} error The result object containing failure information 1525 * @returns {Promise|undefined} If the implementation is doing an asynchronous job, it can return a Promise that should 1526 * be resolved when the job is done (it should not be rejected). 1527 */ 1528 1529/** 1530 * The error object provided in {@link IOperationResultListener#onFailure|IOperationResultListener.onFailure} method. 1531 * @type {object} 1532 * @name Error 1533 * @property {Outcome} outcome Result code returned after processing a FIDO request (see {@link Outcome} object for possible values). 1534 * @property {Object} [additionalInfo] Additional info returned from the server 1535 * @property {Object} [additionalErrorInfo] Additional error information from API Server. 1536 * @property {number} [serverStatusCode] The statusCode from the server. 1537 */ 1538 1539/** 1540 * This method implementation handles session data updates. 1541 * @method IOperationResultListener#onSessionDataUpdated 1542 * @param {SessionData} sessionData The updated session data. Should be null if session is expired. 1543 */ 1544 1545/** 1546 * @class 1547 * @hideconstructor 1548 * @classdesc 1549 * A singleton that implements {@link IOperationResultListener} interface to cache data about users and their 1550 * registrations after successful operations. Also provides methods for accessing the cached data. 1551 * @implements {IOperationResultListener} 1552 */ 1553const UserDataCache = new (function () { 1554 var _this = this; 1555 const USER_DATA_KEY_PREFIX = 'nnl.userDataCache.'; 1556 const ACTIVE_USER_KEY = 'nnl.activeUser'; 1557 1558 /** 1559 * After each successful operation updates cached user data. 1560 * @override 1561 */ 1562 this.onComplete = function(operationType, operationData, extras) { 1563 return AppSdkInfo.getDeviceId().then(function(thisDeviceID) { 1564 switch(operationType) { 1565 case FidoOperationType.REG: 1566 case FidoOperationType.AUTH: 1567 return addRegistration(operationData, thisDeviceID, extras); 1568 case FidoOperationType.LIST_REG: 1569 return updateRegistrations(operationData, thisDeviceID); 1570 case FidoOperationType.DELETE_REG: 1571 return removeRegistrations(operationData); 1572 default: 1573 break; 1574 } 1575 }); 1576 } 1577 1578 /** 1579 * Does nothing. 1580 * @override 1581 */ 1582 this.onFailure = function(operationType, error) {} 1583 1584 /** 1585 * Returns the authenticated user's userName previously stored by calling {@link UserDataCache#setActiveUser|setActiveUser}. 1586 * @returns {string} active user's userName 1587 */ 1588 this.getActiveUser = function() { 1589 return NNLStorage.getItem(ACTIVE_USER_KEY); 1590 } 1591 1592 /** 1593 * Stores currently active user's userName in localStorage and loads the cached data for that user. 1594 * If the userName parameter is provided, stores it as the authenticated user's userName. 1595 * <br>If the userName parameter is not provided or is null, removes previously stored authenticated user's userName. 1596 * @param {string} [userName] Authenticated user's userName. 1597 * @returns {Promise}
1597 A promise that will be resolved when the active user's cached data is loaded. 1598 */ 1599 this.setActiveUser = function(userName) { 1600 if(userName == null) { 1601 NNLStorage.removeItem(ACTIVE_USER_KEY); 1602 return Promise.resolve(); 1603 } 1604 1605 // Get existing active user before updating it. 1606 var currentUserName = _this.getActiveUser(); 1607 1608 // If the same user was active before, no need to do anything. 1609 if(currentUserName === userName) { 1610 return Promise.resolve(_this.getUserData()); 1611 } 1612 1613 // If the user was changed, set the new user as active and fetch its cached data. 1614 NNLStorage.setItem(ACTIVE_USER_KEY, userName); 1615 return _this.fetchUserData(userName); 1616 } 1617 1618 /** 1619 * Retrieves the cached data from cookie storage for the specified user. 1620 * @param {string} userName User name to retrieve data for. 1621 * @returns {Promise} A promise that will be resolved with the cached user data (see the description for {@link UserDataCache#getUserData|getUserData}). 1622 */ 1623 this.fetchUserData = function(userName) { 1624 return NNLStorage.fetchItem(USER_DATA_KEY_PREFIX + userName).then(parseData); 1625 } 1626 1627 /** 1628 * Returns a JSON object containing cached data for the currently active user (specified by {@link UserDataCache#setActiveUser|setActiveUser} call). 1629 * <br>The built in properties in the returned object are listed below. 1630 * <br>The object also will contain any custom properties added using the {@link UserDataCache#setUserData|setUserData} method. 1631 * 1632 * Property | Type | Description 1633 * ----------------------- | ------ | ------------- 1634 * fido2CredentialIds | Array | List of objects with handle and attachment type of local FIDO2 registrations. 1635 * fido2CredentialIds[].id | String | The handle for FIDO2 registration. 1636 * fido2CredentialIds[].authenticatorAttachment | String | The attachment type of FIDO2 registration. Either "platform", "cross-platform" or "unknown". 1637 * uafKeyIds | Array | List of handles of local UAF registrations. 1638 * secondDeviceId | String | The device ID of the second device used during OOB registration. 1639 * pushHandle | String | The push handle for the second device. 1640 * 1641 * @returns {object} JSON object with user's data. 1642 */ 1643 this.getUserData = function() { 1644 var userName = _this.getActiveUser(); 1645 if(userName == null) { 1646 return null; 1647 } 1648 1649 return parseData(NNLStorage.getItem(USER_DATA_KEY_PREFIX + userName)); 1650 } 1651 1652 this.getQuickAuthData = function (userName, keyName) { 1653 let data = NNLStorage.getItem(USER_DATA_KEY_PREFIX + userName); 1654 if (data == null) { 1655 return null; 1656 } 1657 1658 let userData = parseData(data); 1659 if (userData.quickAuth == null) { 1660 return null; 1661 } 1662 1663 return userData.quickAuth[keyName != null ? keyName : QUICK_AUTH_DATA_FIDO2_KEY]; 1664 } 1665 1666 /** 1667 * Stores the specified data in the cache for the specified user. 1668 * <br>Note that the data will be stored as is, and the next call to {@link UserDataCache#getUserData|getUserData} 1669 * will return this object. In order to add a custom property to the cached data, 1670 * first call {@link UserDataCache#getUserData|getUserData}, add the new property to the returned data, then call this 1671 * method to store it. 1672 * <br> If the data parameter is not specified, is null or is empty, then the cached data for the specified user will be deleted. 1673 * @param {string} userName User name to set data for. 1674 * @param {Object} [data] user's data to be cached. 1675 */ 1676 this.setUserData = function(userName, data) { 1677 if(userName == null) { 1678 return null; 1679 } 1680 1681 // If data is not provided or is empty object, delete the existing data. 1682 // Otherwise override the existing data. 1683 if(NNLUtils.isObjectEmpty(data)) { 1684 NNLStorage.removeItem(USER_DATA_KEY_PREFIX + userName); 1685 } else { 1686 NNLStorage.setItem(USER_DATA_KEY_PREFIX + userName, JSON.stringify(data), true); 1687 } 1688 } 1689 1690 /** 1691 * Removes the quickData (or quickData related to the credentialId of the provided handle) from the userData. 1692 * 1693 * @param userData The userData from where to delete the quickData. 1694 * @param quickData The quick data to optional if the handle is provided. 1695 * @param handle 1696 */ 1697 function removeQuickData(userData, quickData, handle) { 1698 let currUserQuickCredId = getUserQuickCredentialId(userData); 1699 if (handle != null && handle !== '') { 1700 let credId = extractCredentialIdFromHandle(handle); 1701 if (credId === currUserQuickCredId) { 1702 delete userData.quickAuth; 1703 } 1704 } else if (quickData != null && quickData.credentialId === currUserQuickCredId ) { 1705 delete userData.quickAuth; 1706 } 1707 } 1708 1709 function getUserQuickCredentialId(userData) { 1710 if (userData != null && 1711 userData.quickAuth != null && 1712 userData.quickAuth.dataFido2 != null && 1713 userData.quickAuth.dataFido2.credentialId != null && 1714 userData.quickAuth.dataFido2.credentialId !== "") { 1715 1716 return userData.quickAuth.dataFido2.credentialId; 1717 } 1718 1719 return null; 1720 } 1721 1722 /** 1723 * Extracts the credentialId from the provided handle. 1724 * 1725 * @param handle the handle from which to extract the credentialId. 1726 * @returns {string} The extracted credentialId. 1727 * @private 1728 */ 1729 function extractCredentialIdFromHandle(handle) { 1730 let handleParts = JSON.parse(NNLBase64.toString(handle)); 1731 return handleParts[2]; 1732 } 1733 1734 function parseData(userData) { 1735 // Parse existing user data or create a default empty object. 1736 userData = userData || '{}'; 1737 userData = JSON.parse(userData); 1738 // Make sure that "fido2CredentialIds" and "uafKeyIds" are defined. 1739 userData.fido2CredentialIds = userData.fido2CredentialIds || []; 1740 userData.uafKeyIds = userData.uafKeyIds || []; 1741 1742 return userData; 1743 } 1744 1745 function addRegistration(operationData, thisDeviceID, extras) { 1746 // In case of authentication the userName is supplied in the operationData. 1747 // In case of registration the app is expected to set the active user name before starting the registration. 1748 var userName = operationData.userName; 1749 var activeUser = _this.getActiveUser(); 1750 // If user is missing or data is not available, nothing can be added in cache 1751 if((userName == null && activeUser == null) || NNLUtils.isObjectEmpty(operationData.additionalInfo)) { 1752 return; 1753 } 1754 1755 return Promise.resolve() 1756 .then(function() { 1757 // If active user was not set, fetch the cached data for the user specified in operationData. 1758 if(activeUser == null) { 1759 return _this.fetchUserData(userName); 1760 } 1761 1762 // Otherwise use active user's userName and get already updated data. 1763 userName = activeUser; 1764 return _this.getUserData(); 1765 }) 1766 .then(function(userData) { 1767 // If the operation was done on another device, then it is an OOB operation 1768 if(operationData.additionalInfo.device != null && operationData.additionalInfo.device.id !== thisDeviceID) { 1769 userData.secondDeviceId = operationData.additionalInfo.device.id; 1770 if(operationData.additionalInfo.push != null && operationData.additionalInfo.push.pushHandle != null) { 1771 userData.pushHandle = operationData.additionalInfo.push.pushHandle; 1772 } 1773 } else { 1774 // Get the list of authenticators used during the operation. 1775 var authenticatorsResult = operationData.additionalInfo.authenticatorsResult || []; 1776 if(authenticatorsResult.length > 0) { 1777 if(operationData.protocolFamily === 'web') { 1778 // Add the new credentials ID to the list if it does not exist. 1779 var found = userData.fido2CredentialIds.some(function(item) { 1780 if(item.id === authenticatorsResult[0].handle) { 1781 // If the attachment type of the existing credential is not known, override it with 1782 // attachment type from operation data which is either the same or has the correct value. 1783 if(item.authenticatorAttachment == null || item.authenticatorAttachment === 'unknown') { 1784 item.authenticatorAttachment = operationData.authenticatorAttachment; 1785 } 1786 return true; 1787 } 1788 1789 return false; 1790 });
1791 1792 if(!found) { 1793 userData.fido2CredentialIds.push({ 1794 id: authenticatorsResult[0].handle, 1795 authenticatorAttachment: operationData.authenticatorAttachment 1796 }); 1797 } 1798 1799 if (operationData.quickAuthData != null) { 1800 updateQuickData(userData, operationData.quickAuthData, extras[AppSdk.EXTRA_KEY_QUICK_AUTH_DATA_KEY_NAME]); 1801 } 1802 } else if(operationData.protocolFamily === 'uaf') { 1803 // Add the new keyID to the list if it does not exist. 1804 if(userData.uafKeyIds.indexOf(authenticatorsResult[0].handle) === -1) { 1805 userData.uafKeyIds.push(authenticatorsResult[0].handle); 1806 } 1807 } 1808 } 1809 } 1810 1811 _this.setUserData(userName, userData); 1812 }); 1813 } 1814 1815 function updateQuickData(userData, quickData, customKey) { 1816 if (quickData == null) { 1817 return; 1818 } 1819 let handle = userData.fido2CredentialIds[userData.fido2CredentialIds.length - 1].id; 1820 quickData.credentialId = extractCredentialIdFromHandle(handle); 1821 1822 let quickDataKey = QUICK_AUTH_DATA_FIDO2_KEY; 1823 if (customKey != null) { 1824 quickDataKey = customKey; 1825 } 1826 1827 userData.quickAuth = {}; 1828 userData.quickAuth[quickDataKey] = quickData; 1829 } 1830 1831 function removeRegistrations(operationData) { 1832 var userName = _this.getActiveUser(); 1833 // If the operation data contains a "userName" and a "quickData", 1834 // then this is a request to remove the quick data. 1835 let isQuickDataRemoval = operationData.userName != null && operationData.quickData; 1836 if (isQuickDataRemoval) { 1837 userName = operationData.userName; 1838 } 1839 1840 if(userName === null) { 1841 return; 1842 } 1843 1844 // Load data for active user. 1845 var userData = _this.getUserData(); 1846 if (isQuickDataRemoval && userData == null) { 1847 // If the user data is not set but this is request for quickData removal, then get the data from the cache. 1848 userData = parseData(NNLStorage.getItem(USER_DATA_KEY_PREFIX + userName)); 1849 } 1850 1851 // In case of deleting single registration remove the deleted handle from list of cached handles. 1852 if(operationData.handle != null) { 1853 // As the protocol is not available, filter lists for both protocols. 1854 userData.fido2CredentialIds = userData.fido2CredentialIds.filter(function(item) { 1855 return item.id !== operationData.handle; 1856 }); 1857 userData.uafKeyIds = userData.uafKeyIds.filter(function(handle) { 1858 return handle !== operationData.handle; 1859 }); 1860 1861 removeQuickData(userData, null, operationData.handle); 1862 } else if (isQuickDataRemoval) { 1863 removeQuickData(userData, operationData.quickData, null); 1864 } else { 1865 // In case of deleting all registration or performing purge, just delete user data. 1866 userData = null; 1867 } 1868 1869 // Save the data for the user. 1870 _this.setUserData(userName, userData); 1871 } 1872 1873 function updateRegistrations(operationData, thisDeviceID) { 1874 var userName = _this.getActiveUser(); 1875 if(userName === null) { 1876 return; 1877 } 1878 1879 // Load data for active user. 1880 var userData = _this.getUserData(); 1881 1882 // In case of list registrations operations update the lists of cached handles 1883 // by intersecting them with available handles. 1884 var credentialIds = []; 1885 var keyIds = []; 1886 // delete oob information. 1887 delete userData.secondDeviceId; 1888 delete userData.pushHandle; 1889 var lastUsedOOBTimeStamp = 0; 1890 1891 // Loop over the registrations. 1892 operationData.registrations.forEach(function(registration) { 1893 if(registration.protocol === 'fido2') { 1894 var found = credentialIds.some(function(item) { 1895 return item.id === registration.handle; 1896 }); 1897 1898 // Try to add the registration if it is not already in the new list. 1899 if(!found) { 1900 var existingHandles = userData.fido2CredentialIds.map(function(item) { 1901 return item.id; 1902 }); 1903 1904 // If the registration info is in the current cache, keep it. 1905 var index = existingHandles.indexOf(registration.handle); 1906 if(index !== -1) { 1907 credentialIds.push(userData.fido2CredentialIds[index]); 1908 } else if(registration.device === thisDeviceID) { 1909 // If a fido2 registration is for this device, add it. 1910 credentialIds.push({ 1911 id: registration.handle, 1912 authenticatorAttachment: 'unknown' 1913 });
1914 } 1915 } 1916 } else if(registration.protocol === 'uaf') { 1917 if(userData.uafKeyIds.indexOf(registration.handle) !== -1) { 1918 keyIds.push(registration.handle); 1919 } 1920 // If a fido2 registration is for this device and is not in the list, add it. 1921 if(registration.device === thisDeviceID && keyIds.indexOf(registration.handle) === -1) { 1922 keyIds.push(registration.handle); 1923 } 1924 } 1925 1926 // Save the OOB information of the last used registered mobile device other than this device. 1927 // Check for mobile device is needed because after authenticating with FIDO2 on a new browser 1928 // a new registration entry is added in server with new device id. Because of that this condition 1929 // without check for mobile device becomes true, and incorrect OOB information will be saved. 1930 if(registration.device !== thisDeviceID && isMobileDevice(registration.deviceType) 1931 && registration.lastUsedTimeStamp > lastUsedOOBTimeStamp) { 1932 // remember timestamp and store device id. 1933 lastUsedOOBTimeStamp = registration.lastUsedTimeStamp; 1934 userData.secondDeviceId = registration.device; 1935 // If this registration has push handle store it, 1936 // otherwise delete push handle stored from previous device. 1937 if(registration.push != null) { 1938 userData.pushHandle = registration.push.pushHandle; 1939 } else { 1940 delete userData.pushHandle; 1941 } 1942 } 1943 }); 1944 1945 // Override localCredentials with filtered list. 1946 userData.fido2CredentialIds = credentialIds; 1947 userData.uafKeyIds = keyIds; 1948 // Save data for user. 1949 _this.setUserData(userName, userData); 1950 } 1951 1952 function isMobileDevice(deviceType) { 1953 return deviceType != null && deviceType.match(/^(?:ios|android)/i) != null; 1954 } 1955 1956 function convertStorageFormatIfNeeded() { 1957 // If the old data does not exist, just return. 1958 if(localStorage.getItem('userDataCache') == null) { 1959 return; 1960 } 1961 1962 // Parse the old data and delete it and related cookies before adding new data 1963 // in order not to excess the cookie size limit. 1964 var oldUserData = JSON.parse(localStorage.getItem('userDataCache')); 1965 NNLStorage.removeItem('userDataCache'); 1966 1967 // Loop over the stored users and create separate entry for each one. 1968 Object.keys(oldUserData).forEach(function(userName) { 1969 var oldData = oldUserData[userName]; 1970 var newData = {}; 1971 1972 // If there is a stored "fido2" object, convert it to "fido2CredentialIds" array 1973 // while also converting the array items' format. 1974 if(oldData.fido2 != null && oldData.fido2.credentialIds != null) { 1975 newData.fido2CredentialIds = oldData.fido2.credentialIds.map(function(item) { 1976 // If an item is already object, return it as is. 1977 if(typeof item === 'object') { 1978 return item; 1979 } 1980 1981 // Otherwise return object with unknown attachment type. 1982 return { 1983 id: item, 1984 authenticatorAttachment: 'unknown' 1985 } 1986 }); 1987 } 1988 // Delete the "fido2" object from the old data. 1989 delete oldData.fido2; 1990 1991 // If there is a stored "uaf" object, convert it to "uafKeyIds" array. 1992 if(oldData.uaf != null && oldData.uaf.keyIds != null) { 1993 newData.uafKeyIds = oldData.uaf.keyIds; 1994 } 1995 // Delete the "uaf" object from the old data. 1996 delete oldData.uaf; 1997 1998 // If there is a stored "oob" object, copy its content to "secondDeviceId" and "pushHandle" fields. 1999 if(oldData.oob != null) { 2000 if(oldData.oob.device != null) { 2001 newData.secondDeviceId = oldData.oob.device; 2002 } 2003 if(oldData.oob.pushHandle != null) { 2004 newData.pushHandle = oldData.oob.pushHandle; 2005 } 2006 } 2007 // Delete the "oob" object from the old data. 2008 delete oldData.oob; 2009 2010 // Delete timestamp as it is not needed anymore. 2011 delete oldData.lastUpdatedTimestamp; 2012 2013 // If there are other keys remaining in the old data 2014 // (custom items stored by the app) copy them as is. 2015 Object.keys(oldData).forEach(function(key) { 2016 newData[key] = oldData[key]; 2017 }); 2018 2019 // Store the new data with new key in the storage. 2020 NNLStorage.setItem(USER_DATA_KEY_PREFIX + userName, JSON.stringify(newData), true); 2021 }); 2022 } 2023 2024 return { 2025 /** 2026 * Returns the single instance. 2027 * @memberof UserDataCache 2028 * @returns {UserDataCache} 2029 */ 2030 getInstance: function() { 2031 convertStorageFormatIfNeeded(); 2032 return _this; 2033 } 2034 } 2035})(); 2036Object.freeze(UserDataCache); 2037 2038/** 2039 * @class 2040 * @hideconstructor 2041 * @classdesc A singleton factory class to create and provide {@link IOperationResultListener} implementations. 2042 */ 2043const OperationResultListener = (function() { 2044 var mInstance = null; 2045 var mListeners = []; 2046 2047 function impl() { 2048 this.onComplete = function(operationType, operationData, extras) { 2049 // Chain of promises to wait on before completing the operation. 2050 var chain = Promise.resolve();
2051 mListeners.forEach(function(listener) { 2052 if(typeof listener.onComplete === 'function') { 2053 chain = chain.then(function() { 2054 // If an implementation does not return a Promise, than it is assumed 2055 // to be synchronous and finished its job and the next listener can be called, 2056 // otherwise the returned Promise will be added to the chain and the next listener 2057 // will be called when that Promise is resolved. 2058 return listener.onComplete(operationType, operationData, extras); 2059 }); 2060 } 2061 }); 2062 2063 // Complete operation after all implementations are finished their job. 2064 return chain; 2065 } 2066 2067 this.onFailure = function(operationType, error) { 2068 // Chain of promises to wait on before completing the operation. 2069 var chain = Promise.resolve(); 2070 mListeners.forEach(function(listener) { 2071 if(typeof listener.onFailure === 'function') { 2072 chain = chain.then(function() { 2073 // If an implementation does not return a Promise, than it is assumed 2074 // to be synchronous and finished its job and the next listener can be called, 2075 // otherwise the returned Promise will be added to the chain and the next listener 2076 // will be called when that Promise is resolved. 2077 return listener.onFailure(operationType, error); 2078 }); 2079 } 2080 }); 2081 2082 // Complete operation after all implementations are finished their job. 2083 return chain; 2084 } 2085 2086 this.onSessionDataUpdated = function(sessionData) { 2087 mListeners.forEach(function(listener) { 2088 if(typeof listener.onSessionDataUpdated === 'function') { 2089 listener.onSessionDataUpdated(NNLUtils.cloneObject(sessionData)); 2090 } 2091 }); 2092 } 2093 } 2094 2095 return { 2096 /** 2097 * Returns the IOperationResultListener instance. 2098 * @returns {IOperationResultListener} Stored instance of IOperationResultListener. 2099 * @memberof OperationResultListener 2100 */ 2101 getInstance: function() { 2102 if(mInstance === null) { 2103 mInstance = new impl(); 2104 OperationResultListener.addInstance(UserDataCache.getInstance()); 2105 } 2106 2107 return mInstance; 2108 }, 2109 /** 2110 * Adds a new IOperationResultListener instance. 2111 * @param {IOperationResultListener} instance The new instance. 2112 * @memberof OperationResultListener 2113 */ 2114 addInstance: function(instance) { 2115 mListeners.push(instance); 2116 }, 2117 /** 2118 * Removes a previously added IOperationResultListener instance. 2119 * @param {IOperationResultListener} instance The instance to remove. 2120 * @memberof OperationResultListener 2121 */ 2122 removeInstance: function(instance) { 2123 var index = mListeners.indexOf(instance); 2124 if(index !== -1) { 2125 mListeners.splice(index, 1); 2126 } 2127 } 2128 }; 2129})(); 2130Object.freeze(OperationResultListener); 2131 2132const NNLUtils = new (function() { 2133 const IFRAME_ID = 'nnl_iframe'; 2134 2135 /** 2136 * ID of the "location" extension. 2137 */ 2138 const LOCATION_EXT_ID = 'noknok.uaf.location'; 2139 2140 /** 2141 * ID of the "metrics" extension. 2142 */ 2143 const METRICS_EXT_ID = 'noknok.metrics'; 2144 2145 // References to the NNL iframe tag and its origin. 2146 var nnlIframe; 2147 var nnlIframeOrigin; 2148 2149 var SDK_URL = null; 2150 if(document.currentScript) { 2151 SDK_URL = document.currentScript.src; 2152 } else { 2153 // document.currentScript is not supported on IE, try to get url from a stack trace. 2154 try { 2155 throw new Error(); 2156 } catch(err) { 2157 // Find the second match for the "at" string to get file src url from stack. 2158 // Specifically works with the format of stack traces in IE. 2159 var stackDetails = (/.*at [^(]*\((.*):(.+):(.+)\)$/ig).exec(err.stack); 2160 SDK_URL = stackDetails && stackDetails[1]; 2161 } 2162 } 2163 SDK_URL = SDK_URL && SDK_URL.replace('js/utils.js', ''); 2164 2165 // Returns a promise that will be resolved when document loading is complete. 2166 function documentReady() { 2167 return new Promise(function(resolve) { 2168 if(document.readyState === 'loading') { 2169 // If the document is still loading, wait for it to complete. 2170 document.addEventListener('DOMContentLoaded', resolve); 2171 } else { 2172 // Otherwise resolve right away. 2173 resolve(); 2174 } 2175 }); 2176 } 2177 2178 // Returns a promise that will be resolved when iframe is loaded and rejected when loading fails. 2179 function loadIframe() { 2180 return new Promise(function(resolve, reject) { 2181 nnlIframe = document.getElementById(IFRAME_ID); 2182 // If the iframe is not present in the document, then create a new element and load the iframe. 2183 // The promise will be resolved on successful load and rejected on error. 2184 if(nnlIframe === null) { 2185 nnlIframe = document.createElement('iframe'); 2186// nnlIframe.src = NNLUtils.getResourceURL('', 'iframe.jsp'); 2187 nnlIframe.src = window.location.origin + '/iss-pc/jsp/passkey/nnlappsdk-9.2.0/iframe.jsp'; 2188 nnlIframe.id = IFRAME_ID; 2189 nnlIframe.name = IFRAME_ID; 2190 nnlIframe.style = 'display:none'; 2191 nnlIframe.onload = resolve; 2192 nnlIframe.onerror = reject; 2193 document.body.appendChild(nnlIframe); 2194 } else { 2195 // If the iframe is already in the page, resolve the promise. 2196 resolve(); 2197 } 2198 nnlIframeOrigin = NNLUtils.getOrigin(nnlIframe.src); 2199 }); 2200 } 2201 2202 // Returns a promose that will be resolved when a ready event is received from the iframe. 2203 function connectToIframe() { 2204 return new Promise(function(resolve) { 2205 function readyCallback(event) { 2206 // Once iframe has reported for duty, remove event listener and resolve the promise. 2207 if(event.origin === nnlIframeOrigin && event.data && event.data.type === 'nnl.iframeReady') { 2208 console.log('Connection with the NNL iframe is established.'); 2209 window.removeEventListener('message', readyCallback); 2210 2211 nnlIframe.contentWindow.postMessage({type: 'nnl.ready'}, nnlIframeOrigin); 2212 resolve(); 2213 } 2214 } 2215 2216 // Start listening from message. 2217 window.addEventListener('message', readyCallback, false); 2218 }); 2219 } 2220 2221 // Load and connect to the iframe and store the resulting promise. 2222 var nnlIframeLoaded = documentReady() 2223 .then(loadIframe) 2224 .then(connectToIframe); 2225 2226 // This promise will be created first time initialize is called, and just returned in subsequent calls 2227 // without doing the initialization calls (to prevent simultaneous loadScript calls for the same script). 2228 var initPromise = null; 2229 2230 // Returns a promise that will be resolved when the iframe is loaded and ready for communication 2231 // and all common dependencies are loaded. Will be rejected otherwise. 2232 this.initialize = function() { 2233 if(initPromise == null) { 2234 initPromise = nnlIframeLoaded.then(function() { 2235 // The resources loaded here are not referenced globally (before an SDK API call) 2236 // so it is safe to load them asynchronously in parallel. 2237 return Promise.all([ 2238 NNLUtils.loadCSS('nnlsdk.css'), 2239 NNLUtils.loadHTML('utils.html', 'nnl-utils-section'), 2240 NNLUtils.loadScript('metrics.js'), 2241 NNLUtils.loadScript('fido-client.js'), 2242 NNLUtils.loadScript('outcome.js'), 2243 NNLUtils.loadScript('common-utils.js') 2244 ]); 2245 }); 2246 } 2247 2248 return initPromise; 2249 } 2250 2251 this.loadScript = function(scriptName, scriptFolder) { 2252 scriptFolder = scriptFolder || 'js'; 2253 return new Promise(function(resolve, reject) { 2254 var existing = document.querySelector('script[src*="' + scriptName + '"]'); 2255 if(existing === null) { 2256 var script = document.createElement('script'); 2257 script.src = NNLUtils.getResourceURL(scriptFolder, scriptName); 2258 script.onload = resolve; 2259 script.onerror = function() { 2260 // If the script fails to load, remove the element to try again next time. 2261 script.remove(); 2262 reject(); 2263 } 2264 document.head.appendChild(script); 2265 } else { 2266 resolve(); 2267 } 2268 }); 2269 } 2270 2271 this.loadCSS = function(cssName) { 2272 return new Promise(function(resolve, reject) { 2273 var existing = document.querySelector('link[href*="' + cssName + '"]'); 2274 if(existing === null) { 2275 var link = document.createElement('link'); 2276 link.rel = 'stylesheet'; 2277 link.type = 'text/css'; 2278 link.href = NNLUtils.getResourceURL('css', cssName); 2279 link.onload = resolve; 2280 link.onerror = function() { 2281 // If the CSS fails to load, remove the element to try again next time. 2282 link.remove(); 2283 reject(); 2284 } 2285 document.head.appendChild(link); 2286 } else { 2287 resolve(); 2288 } 2289 }); 2290 } 2291 2292 this.loadHTML = function(htmlName, elementID) { 2293 return NNLUtils.loadResource(NNLUtils.getResourceURL('html', htmlName), 'html', true) 2294 .then(function(content) { 2295 let div = document.querySelector('#' + elementID); 2296 if(div == null) { 2297 div = document.createElement('div'); 2298 div.id = elementID; 2299 document.body.appendChild(div); 2300 } 2301 2302 div.innerHTML = content; 2303 }); 2304 } 2305 2306 this.loadResource = function(resourceURL, resourceType, useIframe) { 2307 return Promise.resolve() 2308 .then(function() { 2309 if(useIframe) { 2310 return loadResourceInIframe(resourceURL, resourceType); 2311 } 2312 2313 return loadResourceDirect(resourceURL, resourceType); 2314 }) 2315 .then(function(httpResponse) { 2316 // on cordova the status is 0, so need to check if there is an actual response 2317 if(httpResponse.status === 200 || (nnl.isCordovaApp && httpResponse.status === 0 && !isResourceEmpty(httpResponse.content, resourceType))) { 2318 return httpResponse.content; 2319 } 2320 2321 return Promise.reject(new Error('Request to "' + resourceURL + '" has failed with status code ' + httpResponse.status)); 2322 }); 2323 } 2324 2325 // Loads resource through iframe.
2326 // Returns a Promise that always resolves with an object containing status and content fields. 2327 function loadResourceInIframe(resourceURL, resourceType) { 2328 return new Promise(function(resolve) { 2329 var data = { 2330 type: 'nnl.loadresource', 2331 resourceURL: resourceURL, 2332 resourceType: resourceType 2333 } 2334 2335 // Create a message channel to receive the response through it. 2336 var channel = new MessageChannel(); 2337 // Listen for response on this end of the channel. 2338 channel.port1.onmessage = function(event) { 2339 resolve(event.data); 2340 } 2341 2342 nnlIframeLoaded.then(function() { 2343 // Transfer the other end of the channel to iframe. 2344 nnlIframe.contentWindow.postMessage(data, nnlIframeOrigin, [channel.port2]); 2345 }); 2346 }); 2347 } 2348 2349 // Loads resource with XMLHttpRequest in top page context. 2350 // Returns a Promise that always resolves with an object containing status and content fields. 2351 function loadResourceDirect(resourceURL, resourceType) { 2352 return new Promise(function(resolve) { 2353 const req = new XMLHttpRequest(); 2354 if(resourceType != null) { 2355 req.responseType = resourceType; 2356 } 2357 req.open('GET', resourceURL, true); 2358 req.onload = function() { 2359 resolve({ 2360 status: req.status, 2361 content: req.response 2362 }); 2363 } 2364 req.onerror = req.onload; 2365 req.send(); 2366 }); 2367 } 2368 2369 function isResourceEmpty(content, resourceType) { 2370 if(content == null) { 2371 return true; 2372 } 2373 2374 if(resourceType === 'json') { 2375 return NNLUtils.isObjectEmpty(content); 2376 } 2377 2378 return content === ''; 2379 } 2380 2381 this.sendRestMessage = function(endPoint, message) { 2382 return new Promise(function(resolve) { 2383 var data = { 2384 type: 'nnl.restmessage', 2385 endPoint: endPoint, 2386 message: message, 2387 ajaxCallConnectionTimeout: AppSdkConfig.ajaxCallConnectionTimeout 2388 } 2389 2390 // Create a message channel to receive the response through it. 2391 var channel = new MessageChannel(); 2392 // Listen for response on this end of the channel. 2393 channel.port1.onmessage = function(event) { 2394 resolve(event.data); 2395 } 2396 2397 NNLUtils.initialize().then(function() { 2398 // Transfer the other end of the channel to iframe. 2399 nnlIframe.contentWindow.postMessage(data, nnlIframeOrigin, [channel.port2]); 2400 }); 2401 }); 2402 } 2403 2404 this.getOrigin = function(url) { 2405 if(url === undefined || url === null) { 2406 return "*"; 2407 } 2408 2409 var parts = url.split("/"); 2410 return parts[0] + "//" + parts[2]; 2411 } 2412 2413 /** 2414 * This method returns location extension object in following format: 2415 { 2416 "id" : "noknok.uaf.location", 2417 "data" : "<location_params>", 2418 "fail_if_unknown" : false 2419 } 2420 @param opType extension operation type 2421 @param extensions requested extensions list 2422 */ 2423 this.createLocationExtension = function(opType, extensions) { 2424 var sendExtension = false; 2425 2426 // Get extension mode value for given opType from AppSdkConfig config object 2427 switch(AppSdkConfig.sendLocation[opType]) { 2428 case ExtensionMode.NEVER: 2429 // never send extension 2430 break; 2431 case ExtensionMode.REQUESTED: 2432 // send extension only if it is included in extensions list (exists in server response) 2433 sendExtension = isExtRequested(LOCATION_EXT_ID, extensions); 2434 break; 2435 case ExtensionMode.ALWAYS: 2436 // always send extension 2437 sendExtension = true; 2438 break; 2439 default: 2440 break; 2441 } 2442 2443 if (sendExtension) { 2444 // get location params from local storage if available return location extension. 2445 const locationParams = NNLStorage.getItem("locationParams"); 2446 // update local storage 2447 getAndUpdateLocationParams(); 2448 2449 if(locationParams) { 2450 return Promise.resolve({ 2451 "id" : LOCATION_EXT_ID, 2452 "data" : locationParams, 2453 "fail_if_unknown" : false 2454 }); 2455 } 2456 } 2457 return Promise.reject(); 2458 }; 2459 2460 this.createMetricsExtension = function(opType, extensions) { 2461 var sendExtension = false; 2462 2463 // Get extension mode value for given opType from AppSdkConfig config object 2464 switch(AppSdkConfig.sendMetrics[opType]) { 2465 case ExtensionMode.NEVER: 2466 // never send extension 2467 break; 2468 case ExtensionMode.REQUESTED: 2469 // send extension only if it is included in extensions list (exists in server response) 2470 sendExtension = isExtRequested(METRICS_EXT_ID, extensions); 2471 break; 2472 case ExtensionMode.ALWAYS: 2473 // always send extension 2474 sendExtension = true; 2475 break; 2476 default: 2477 break; 2478 } 2479 2480 if (sendExtension) { 2481 // get the metrics report 2482 const metrics = Tabulator.getInstance().getReport(); 2483 if(metrics) { 2484 return Promise.resolve({ 2485 "id" : METRICS_EXT_ID, 2486 "data" : metrics, 2487 "fail_if_unknown" : false 2488 }); 2489 } 2490 } 2491 return Promise.reject(); 2492 }; 2493 2494 /** 2495 * Checks whether the extension is requested by the server.
2496 * @param {string} extId the extension ID to check. 2497 * @param {Array} extensions the array of extensions. 2498 * @returns {boolean} is extension requested by the server. 2499 */ 2500 function isExtRequested(extId, extensions) { 2501 return extensions && extensions.reduce(function(acc, ext) {return ext.id === extId}, false) 2502 } 2503 2504 /** 2505 * Creates a deep clone of the specified object. 2506 */ 2507 this.cloneObject = function(obj) { 2508 // Object.assign() does not do a deep clone, so use stringify/parse approach. 2509 return JSON.parse(JSON.stringify(obj || {})); 2510 } 2511 2512 // Check if the given object is empty 2513 this.isObjectEmpty = function(obj){ 2514 if(obj == null) { 2515 return true; 2516 } 2517 2518 for(var prop in obj) { 2519 if(Object.prototype.hasOwnProperty.call(obj, prop)) { 2520 return false; 2521 } 2522 } 2523 2524 return JSON.stringify(obj) === JSON.stringify({}); 2525 } 2526 2527 // Removes event listeners of given element by recreating it. 2528 // Returns reference to the newly created element. 2529 this.removeEventListeners = function(element) { 2530 const copy = element.cloneNode(true); 2531 element.parentNode.replaceChild(copy, element); 2532 return copy; 2533 } 2534 2535 // Checks whether provided data is email or not. 2536 this.isEmail = function(data) { 2537 return (/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/).test(data); 2538 } 2539 2540 // Checks whether provided data is phone number or not. 2541 this.isPhoneNumber = function(data) { 2542 return (/^\+?[0-9]+$/).test(data); 2543 } 2544 2545 this.getInitMessage = function(opType, extras, supportedProtocols) { 2546 var rpData = NNLUtils.createRpData(extras); 2547 return nnl.FIDOClient.init(rpData, opType, supportedProtocols); 2548 } 2549 2550 // Creates RP data structure from extras. 2551 this.createRpData = function(extras) { 2552 var rpData = { 2553 checkPolicy: false 2554 } 2555 2556 // Copy available data from extras 2557 if(extras != null) { 2558 // If this is a web OOB operation, copy the OOB data. 2559 if(extras.webOob) { 2560 rpData.oobData = extras.oobData; 2561 } 2562 2563 // If application has specified support for QR code scanning, copy that flag. 2564 if(extras.qrSupported != null) { 2565 rpData.qrSupported = extras.qrSupported; 2566 } 2567 2568 // If application has specified a custom username, copy it. 2569 if(extras.userName != null) { 2570 rpData.userName = extras.userName; 2571 } 2572 2573 // If application has specified a custom user display name, copy it. 2574 if(extras.userDisplayName != null) { 2575 rpData.userDisplayName = extras.userDisplayName; 2576 } 2577 2578 // If the application has specified the SPC requirement level, copy it. 2579 if (extras[AppSdk.EXTRA_KEY_PAYMENT_SPC] != null) { 2580 rpData[AppSdk.EXTRA_KEY_PAYMENT_SPC] = extras[AppSdk.EXTRA_KEY_PAYMENT_SPC]; 2581 } 2582 2583 // If the application has specified the user mediation, copy it. 2584 if (extras[AppSdk.EXTRA_KEY_USER_MEDIATION] != null) { 2585 rpData[AppSdk.EXTRA_KEY_USER_MEDIATION] = extras[AppSdk.EXTRA_KEY_USER_MEDIATION]; 2586 } 2587 2588 // If the application has specified the flag for enabilng the quick auth, copy it. 2589 if (extras[AppSdk.EXTRA_KEY_QUICK_AUTH_ENABLED]) { 2590 rpData[AppSdk.EXTRA_KEY_QUICK_AUTH_ENABLED] = extras[AppSdk.EXTRA_KEY_QUICK_AUTH_ENABLED]; 2591 } 2592 2593 // Copy extensions if availalbe. 2594 if(extras.extensions != null) { 2595 rpData.extensions = extras.extensions; 2596 } 2597 } 2598 2599 return rpData; 2600 } 2601 2602 // Creates OperationData structure based on the operation result. 2603 this.createOperationData = function(result, userName) { 2604 var operationData = {}; 2605 operationData.additionalInfo = NNLUtils.cloneObject(result.additionalInfo); 2606 2607 // Extract the protocol from authenticator handle. 2608 if(operationData.additionalInfo.authenticatorsResult != null && operationData.additionalInfo.authenticatorsResult.length > 0) { 2609 var handle = operationData.additionalInfo.authenticatorsResult[0].handle; 2610 handle = NNLBase64.toString(handle); 2611 handle = JSON.parse(handle); 2612 // Strip version from protocol family. 2613 operationData.protocolFamily = handle[0].split('_')[0]; 2614 2615 if(operationData.protocolFamily === 'web') { 2616 // The attachmentHints array can be returned from server after authentication 2617 var hints = operationData.additionalInfo.authenticatorsResult[0].attachmentHints; 2618 // The isPlatformAuthenticator flag can be set by FIDOClient after registration or authentication. 2619 var isPlatformAuthenticator = result.isPlatformAuthenticator; 2620 // If the flag is not set, but server indicates that the "internal" transport was used, 2621 // then this was an authentication with platform authenticator. 2622 if(isPlatformAuthenticator == null && hints != null && hints.length > 0) { 2623 isPlatformAuthenticator = (hints.indexOf('internal') !== -1); 2624 } 2625 2626 if(isPlatformAuthenticator != null) { 2627 operationData.authenticatorAttachment = isPlatformAuthenticator ? 'platform' : 'cross-platform'; 2628 } else { 2629 operationData.authenticatorAttachment = 'unknown'; 2630 } 2631 } 2632 } 2633 2634 if(result.push != null) { 2635 operationData.additionalInfo.push = result.push; 2636 } 2637 2638 if(result.message != null) { 2639 let messageDecoded = JSON.parse(NNLBase64.toString(result.message)); 2640 if (messageDecoded.server && messageDecoded.server.quickAuthData && messageDecoded.server.appID) { 2641 operationData.quickAuthData = messageDecoded.server.quickAuthData;
2642 operationData.quickAuthData.appId = messageDecoded.server.appID; 2643 } 2644 } 2645 2646 if(userName != null) { 2647 operationData.userName = userName; 2648 } 2649 2650 return operationData; 2651 } 2652 2653 // Creates Error object for OperationResultListener.onFailure from a Result or AdaptiveResult object. 2654 this.createErrorObject = function(result) { 2655 // Outcome is always present in a Result or AdaptiveResult object 2656 const error = { 2657 outcome: result.outcome 2658 } 2659 2660 // Result object contains statusCode, AdaptiveResult object contains serverStatusCode. 2661 if(result.statusCode != null) { 2662 error.serverStatusCode = result.statusCode; 2663 } else { 2664 error.serverStatusCode = result.serverStatusCode; 2665 } 2666 2667 // Copy optional fields if they are present. 2668 if(result.additionalInfo != null) { 2669 error.additionalInfo = result.additionalInfo; 2670 } 2671 2672 if(result.additionalErrorInfo != null) { 2673 error.additionalErrorInfo = result.additionalErrorInfo; 2674 } 2675 2676 return error; 2677 } 2678 2679 // Returns a policy configuration if a mapping for the given policy name exists. 2680 this.getPolicyConfig = function(policyName) { 2681 let policyConfig = null; 2682 // First look if UI config contains a policy map. 2683 const configJsonWrapper = NNLUiConfig.getJsonWrapper(); 2684 if(configJsonWrapper != null) { 2685 policyConfig = configJsonWrapper.searchUp('policyMap', policyName); 2686 } 2687 2688 // For backward compatibility, also look up in the AppSdkConfig. 2689 if(policyConfig == null && AppSdkConfig.policyMap != null) { 2690 policyConfig = AppSdkConfig.policyMap[policyName]; 2691 } 2692 2693 return policyConfig; 2694 } 2695 2696 this.getResourceURL = function(resourceFolder, resourceName) { 2697 return SDK_URL + resourceFolder + (resourceFolder !== '' ? '/' : '') + resourceName; 2698 } 2699 2700 this.getAbsoluteImageURL = function(imageURL) { 2701 // Construct an absolute URL from current page as base URL. 2702 let baseURL = document.location.href; 2703 // If base URL is specified by the app (by calling NNLUiConfig.downloadUiConfig), use that one. 2704 if(nnl.uiConfigBaseUrl != null) { 2705 baseURL = nnl.uiConfigBaseUrl + 'web/'; 2706 } 2707 const absoluteURL = new URL(imageURL, baseURL); 2708 return absoluteURL.href; 2709 } 2710 2711 // The modal dialog built-in in the SDK that is currently open. 2712 // The showModal function below will set this to the modal when it is opened, 2713 // and set this to null when the modal is closed. 2714 var mActiveModal = null; 2715 2716 // This event listener will be called anytime an element in the document gets the input focus. 2717 // If there is an opened dialog and it does not contain the element that got focus 2718 // then it will move focus to that dialog. This prevents selecting buttons on the background 2719 // by repeatedly pressing the "Tab" key. 2720 document.addEventListener('focusin', function(event) { 2721 if(mActiveModal != null && !mActiveModal.contains(event.target)) { 2722 mActiveModal.focus(); 2723 } 2724 }, true); 2725 2726 /** 2727 * Show or hides the given modal dialog (built-in in the SDK). 2728 */ 2729 this.showModal = function(modal, bShow) { 2730 if(bShow) { 2731 // Set the active modal. 2732 mActiveModal = modal; 2733 // Make the dialog focusable in order to keep focus on it. 2734 modal.setAttribute('tabindex', '-1'); 2735 2736 modal.style.display = 'block'; 2737 document.body.classList.add('nnl-dialog-opened'); 2738 } else { 2739 modal.style.display = 'none'; 2740 mActiveModal = null; 2741 document.body.classList.remove('nnl-dialog-opened'); 2742 } 2743 } 2744 2745 /** 2746 * Performs Try FIDO operation if the server request indicates that there is a 2747 * registration which may be available on the current or another device 2748 * via the "credentialAvailability" list. 2749 * After user's confirmation sets "credentialAvailability" to an empty list 2750 * to not show this prompt again for the current processing flow. 2751 */ 2752 this.tryFido = function(request) { 2753 const credentialAvailability = request.credentialAvailability || []; 2754 // For backward compatibility check the "regUsedOnThisDevice" flag. 2755 if(request.regUsedOnThisDevice != null) { 2756 credentialAvailability.push(request.regUsedOnThisDevice ? 'OnThisDevice ' : 'MaybeOnThisDevice'); 2757 } 2758 2759 let tryFidoUI = null; 2760 // Check for credential availability by priority. 2761 // If the credential is available on the current device, then do not show a prompt. 2762 if(credentialAvailability.indexOf('OnThisDevice') !== -1) { 2763 return Promise.resolve(); 2764 } 2765 2766 // If the credential may be available on the current device, 2767 // then show the Try FIDO prompt.
2768 if(credentialAvailability.indexOf('MaybeOnThisDevice') !== -1) { 2769 tryFidoUI = TryFidoUiFactory.getInstance().createTryFidoUi(); 2770 } else { 2771 // If the credential is not available on the current device. 2772 // If the credential is a security key, do not show a prompt. 2773 if(credentialAvailability.indexOf('SecurityKey') !== -1) { 2774 return Promise.resolve(); 2775 } 2776 2777 // If the credential is available on another device, 2778 // then show the Try Hybrid prompt. 2779 if(credentialAvailability.indexOf('Hybrid') !== -1) { 2780 tryFidoUI = TryFidoUiFactory.getInstance().createTryHybridUi(); 2781 } 2782 } 2783 2784 // Show the prompt if needed. 2785 if(tryFidoUI != null) { 2786 ProgressIndicator.getInstance().suspend(); 2787 return tryFidoUI.openUI() 2788 .then(function(outcome) { 2789 request.credentialAvailability = []; 2790 2791 ProgressIndicator.getInstance().resume(); 2792 if(outcome !== Outcome.SUCCESS) { 2793 return Promise.reject({outcome: outcome}); 2794 } 2795 }); 2796 } 2797 2798 return Promise.resolve(); 2799 } 2800 2801 /** 2802 * Validates the provided value against the type and the instance type, 2803 * using javascript typeof and instanceof operators respectively. 2804 * @param {string} name The name of the parameter. 2805 * @param {any} value The value to be validated. 2806 * @param {string} type the string representative of the type of value. 2807 * @param {any} instanceType the instance type that the value should be assignable to. 2808 * @param {boolean} nullable the flag indicating whether the value can be null or undefined. 2809 * @throws {INVALID_ARGUMENT_EXCEPTION} if the value is not of expected type, instance, or is null (if not nullable). 2810 */ 2811 this.validateArgument = function (name, value, type, instanceType, nullable) { 2812 // If the value is required and is null or undefined - throw an exception. 2813 if (!nullable && value == null) { 2814 throw new AppSdk.INVALID_ARGUMENT_EXCEPTION(name); 2815 } 2816 2817 // This is the case if the parameter is optional and is provided. 2818 if (value != null) { 2819 // Check the type of value against required one (if provided). 2820 if (type != null && typeof value !== type) { 2821 throw new AppSdk.INVALID_ARGUMENT_EXCEPTION(name); 2822 } 2823 2824 // Check the instance type of value against the required one (if provided). 2825 if (instanceType != null && !(value instanceof instanceType)) { 2826 throw new AppSdk.INVALID_ARGUMENT_EXCEPTION(name); 2827 } 2828 } 2829 } 2830 2831 /** 2832 * This method gets location params using Geolocation API and updates local storage 2833 */ 2834 var getAndUpdateLocationParams = function() { 2835 2836 if(!navigator.geolocation) { 2837 NNLStorage.setItem('locationParams', JSON.stringify({status:LocationStatus.SENSOR_UNAVAILABLE})); 2838 return; 2839 } 2840 2841 navigator.geolocation.getCurrentPosition ( 2842 function(position) { 2843 NNLStorage.setItem('locationParams', JSON.stringify({ 2844 status : LocationStatus.OK, 2845 longitude : position.coords.longitude, 2846 latitude : position.coords.latitude, 2847 accuracy : position.coords.accuracy 2848 })); 2849 }, 2850 function(err) { 2851 console.log("ERROR: ", err.code, err.message); 2852 var statusCode = { 2853 // map browser geolocation API error codes to AppSDK codes 2854 1: LocationStatus.NO_APP_PERMISSION, 2855 2: LocationStatus.SIGNAL_UNAVAILABLE, 2856 3: LocationStatus.SIGNAL_UNAVAILABLE 2857 }[err.code] || LocationStatus.UNKNOWN; 2858 2859 NNLStorage.setItem('locationParams', JSON.stringify({status : statusCode})); 2860 }, 2861 {timeout:5000} 2862 ); 2863 } 2864 2865 /** 2866 * Defines the location fetching status codes 2867 */ 2868 var LocationStatus = { 2869 OK : 0, 2870 UNKNOWN : 1, 2871 SENSOR_UNAVAILABLE : 2, 2872 SERVICES_NOT_INSTALLED : 3, 2873 SIGNAL_UNAVAILABLE : 4, 2874 NO_APP_PERMISSION : 5 2875 } 2876 2877 // Helper methods for loading libphonenumber and parsing with it. 2878 // It is the caller's responsibility to call load() and wait for it to resolve before calling parse(). 2879 this.libphonenumber = Object.freeze({ 2880 load: function() { 2881 return NNLUtils.loadScript('libphonenumber-max.js', 'webjars/libphonenumber-js/1.9.18/bundle'); 2882 }, 2883 parse: function(data, extras) { 2884 // Format the phone number according to current locale.
2885 const locale = (extras != null ? extras.currentlocale : null); 2886 const phoneNumber = libphonenumber.parsePhoneNumberFromString(data, locale); 2887 if(phoneNumber != null && phoneNumber.isValid()) { 2888 return phoneNumber.number; 2889 } 2890 2891 return null; 2892 } 2893 }); 2894})(); 2895Object.freeze(NNLUtils); 2896 2897// Parses a list of method groups to identify if there are methods with same type but different names. 2898// Provides a method to retrieve a list of objects containing a method group, its constructed title and icon. 2899const MethodInfoHelper = function(methodGroups, opType) { 2900 // Check if there are FIDO and FIDO_OOB methods with different names 2901 // in order to determine whether to show the names in the list or not. 2902 let hasDifferentFidoMethods = false; 2903 let hasDifferentFidoOOBMethods = false; 2904 let firstFidoMethodName = null; 2905 let firstFidoOOBMethodName = null; 2906 2907 // Loop over all groups and methods. 2908 methodGroups.forEach(function(group) { 2909 group.forEach(function(method) { 2910 // If difference is not found yet, check the method. 2911 if(method.type === AdaptiveType.FIDO && !hasDifferentFidoMethods) { 2912 // For the first occurrence just save the name. 2913 if(firstFidoMethodName == null) { 2914 firstFidoMethodName = method.name; 2915 } else { 2916 // On subsequent occurrence set the flag to true if the names do not match. 2917 hasDifferentFidoMethods = (firstFidoMethodName !== method.name); 2918 } 2919 } 2920 2921 // Repeat the same for OOB method. 2922 if(method.type === AdaptiveType.FIDO_OOB && !hasDifferentFidoOOBMethods) { 2923 if(firstFidoOOBMethodName == null) { 2924 firstFidoOOBMethodName = method.name; 2925 } else { 2926 hasDifferentFidoOOBMethods = (firstFidoOOBMethodName !== method.name); 2927 } 2928 } 2929 }); 2930 }); 2931 2932 // Returns a list of objects containing a method group, its constructed title and icon. 2933 this.getMethodInfoList = function() { 2934 const list = []; 2935 methodGroups.forEach(function(group) { 2936 let title = MethodUIFactory.getInstance().getTitle(group, opType); 2937 const icon = MethodUIFactory.getInstance().getIcon(group); 2938 2939 // Append method name for FIDO methods if there are multiple FIDO methods 2940 // of the same type but with different names. 2941 if(group[0].type === AdaptiveType.FIDO && !hasPolicyMap(group) && hasDifferentFidoMethods || 2942 group[0].type === AdaptiveType.FIDO_OOB && hasDifferentFidoOOBMethods) { 2943 title += ' (' + group[0].name + ')'; 2944 } 2945 2946 list.push({ 2947 title: title, 2948 icon: icon, 2949 group: group 2950 }); 2951 }); 2952 2953 return list; 2954 } 2955 2956 function hasPolicyMap(methodGroup) { 2957 if(methodGroup[0].type === AdaptiveType.FIDO) { 2958 const policyConfig = NNLUtils.getPolicyConfig(methodGroup[0].name); 2959 return policyConfig != null && policyConfig.title != null; 2960 } 2961 return false; 2962 } 2963 2964 Object.freeze(this); 2965} 2966 2967/** 2968 * Controls how extensions are processed. 2969 * @property {string} NEVER never produce the extension 2970 * @property {string} ALWAYS always produce the extension 2971 * @property {string} REQUESTED produce the extension if requested by the server. 2972 */ 2973const ExtensionMode = { 2974 NEVER: "never", 2975 ALWAYS: "always", 2976 REQUESTED: "requested" 2977} 2978Object.freeze(ExtensionMode); 2979 2980/** 2981 * Defines operation type where the extensions should be included 2982 * @property {string} INIT include extension in INIT requests payload 2983 * @property {string} FINISH include extension in FINISH requests payload 2984 * @property {string} PROTOCOL include in protocol message 2985 */ 2986const ExtensionOpType = { 2987 INIT : "init", 2988 FINISH : "finish", 2989 PROTOCOL : "protocol" 2990} 2991Object.freeze(ExtensionOpType); 2992 2993/** 2994 * ID of the "show transaction" extension. 2995 */ 2996const SHOW_TRANSACTION_EXT_ID = 'noknok.showTransaction'; 2997 2998/** 2999 * Defines functions for applying UI customization configurations. 3000 * @class 3001 * @hideconstructor 3002 */ 3003const NNLUiConfig = new (function() { 3004 let mWrapper = null; 3005 const langtag = document.documentElement.lang || navigator.language; 3006 let mLocale; 3007 // If Intl.Locale is available, parse language and region with it. 3008 if(typeof Intl.Locale === 'function') { 3009 mLocale = new Intl.Locale(langtag); 3010 } else { 3011 // Fallback for old browsers. 3012 const langtagParts = langtag.split('-'); 3013 // The language is always the first part. Remove it from array and store in the object. 3014 mLocale = { 3015 language: langtagParts.shift() 3016 } 3017 // The region code is the fist occurrence of a two letter part in the rest of the langtag. 3018 langtagParts.some(function(langtagPart) { 3019 if(/^\w{2}$/.test(langtagPart)) { 3020 // If a 2 letter part is found, store it in the object and stop iteration. 3021 mLocale.region = langtagPart; 3022 return true; 3023 } 3024 }); 3025 } 3026 3027 // Contains the list of configuration files eligible for download, in the order then should be applied. 3028 const mAllowedFiles = []; 3029 ['default', 'web'].forEach(function(configType) { 3030 mAllowedFiles.push(configType + '/ui_cfg.json'); 3031 mAllowedFiles.push(configType + '/ui_cfg_' + mLocale.language + '.json'); 3032 if(mLocale.region != null) { 3033 mAllowedFiles.push(configType + '/ui_cfg_' + mLocale.language + '-' + mLocale.region.toLowerCase() + '.json'); 3034 } 3035 }); 3036 3037 /** 3038 * Applies a UI customization configuration. This function can be called multiple times, every subsequent call 3039 * will merge the provided configuration with existing one. 3040 * @param {(object|string|URL)} config The UI customization configuration source. 3041 * It can be the configuration object itself, a JSON string of the configuration object or a URL 3042 * to load the configuration object from. 3043 * @param {boolean} localized Flag indicating whether to automatically load configurations for current language/region or not. 3044 * Used only when `config` is a URL ending with a file name. When this parameter is true, the function will first load 3045 * the configuration from the URL, then will try to load language and region specific files.<br/> 3046 * For example when the URL is `https://example.com/path/to/ui_configuration.json`, it will try to load the following two URLs as well:<br/> 3047 * <code> 3048 * https://example.com/path/to/ui_configuration<b>_languageCode</b>.json<br/> 3049 * https://example.com/path/to/ui_configuration<b>_languageCode-regionCode</b>.json</br> 3050 * </code> 3051 * where `languageCode` and `regionCode` are two letter lower case language and region codes. 3052 * @returns {Promise} A Promise that will be resolved when the configuration is successfully loaded 3053 * and rejected if the provided string is not a valid JSON object or network error happens when loading the 3054 * provided URL. 3055 */ 3056 this.addUiConfig = function(config, localized) { 3057 let isUrl = false; 3058 return Promise.resolve() 3059 .then(function() { 3060 // If an object is provided, use it as is. 3061 if(typeof config === 'object') { 3062 return Promise.resolve(NNLUtils.cloneObject(config)); 3063 } 3064 3065 // Try to parse the provided string as JSON object. 3066 try { 3067 return Promise.resolve(JSON.parse(config)); 3068 } catch(e) {} 3069 3070 // If parsing fails, treat the parameter as URL. 3071 isUrl = true; 3072 return NNLUtils.loadResource(config, 'json', false); 3073 }) 3074 .then(applyConfiguration) 3075 .then(function() { 3076 // If localized versions are requested with URL, try to load them 3077 // if there is a filename at the end of the URL. 3078 if(isUrl && localized) { 3079 const lastSlash = config.lastIndexOf('/'); 3080 if(lastSlash !== -1) { 3081 const baseUrl = config.substring(0, lastSlash + 1); 3082 const fileName = config.substring(lastSlash + 1); 3083 if(fileName !== '') { 3084 return loadLocalizations(baseUrl, fileName); 3085 } 3086 } 3087 } 3088 }); 3089 } 3090 3091 /** 3092 * Downloads UI configuration files from the specified URL and applies the configurations. 3093 * @param {URL} url Base URL from which to download UI configuration files. 3094 * @returns {Promise} A Promise that will be resolved when all configuration files are successfully downloaded and 3095 * applied, and rejected if a network error happens when downloading the files. 3096 */ 3097 this.downloadUiConfig = function(url) { 3098 // Store the base URL to use it relative image paths when rendering them. 3099 // Also add a trailing slash if it is not there. 3100 nnl.uiConfigBaseUrl = url + (url.lastIndexOf('/') === url.length - 1 ? '' : '/'); 3101 3102 return Promise.resolve() 3103 .then(function() { 3104 return NNLUtils.loadResource(nnl.uiConfigBaseUrl + 'toc.json', 'json', true); 3105 }) 3106 .then(function(toc) { 3107 const downloads = []; 3108 // Loop over the allowed config files in order they should be applied.
3109 mAllowedFiles.forEach(function(configFile) { 3110 // If TOC contains an allowed file, download it and add to array to apply. 3111 if(toc.indexOf(configFile) !== -1) { 3112 downloads.push(NNLUtils.loadResource(nnl.uiConfigBaseUrl + configFile, 'json', true)); 3113 } 3114 }); 3115 3116 // Wait for all downloads to complete. 3117 return Promise.all(downloads); 3118 }) 3119 .then(function(uiConfigs) { 3120 uiConfigs.forEach(applyConfiguration); 3121 }); 3122 } 3123 3124 /** 3125 * Returns the JsonWrapper instance. 3126 * @private 3127 * @returns {JsonWrapper} the wrapper instance. 3128 */ 3129 this.getJsonWrapper = function() { 3130 return mWrapper; 3131 } 3132 3133 /** 3134 * Retrieves a string based on UI customization settings or from app resources. 3135 * 3136 * @param {array} levels An array of levels specifying the hierarchy to search for the property. 3137 * @param {string} tag The tag identifying the string within the configuration file. 3138 * @param {string} defaultValue The default value if the configuration was not found. 3139 * @param {...any} formatArgs Values that will be substituted into the return value. 3140 * @returns {string} retrieved string value, either from UI configuration or default. 3141 * @private 3142 */ 3143 this.getText = function () { 3144 const args = [].slice.call(arguments); 3145 let levels = args.shift(); 3146 let tag = args.shift(); 3147 let defaultValue = args.shift(); 3148 let formatArgs = []; 3149 if (args.length > 0) { 3150 formatArgs = args; 3151 } 3152 3153 let text = defaultValue; 3154 // If json wrapper is null, return the default value. 3155 if (mWrapper != null) { 3156 let configString = mWrapper.searchDown(levels, JsonWrapperKey.strings, tag); 3157 // If configured string is null or empty, return the default value.
3158 if (configString != null && configString.length !== 0) { 3159 text = configString; 3160 } 3161 } 3162 3163 // Format the text with provided arguments. 3164 return formatString(text, formatArgs); 3165 } 3166 3167 function formatString(format, args) { 3168 let result = format; 3169 args.forEach(function (elem, i) { 3170 result = result.replace("{" + i + "}", elem); 3171 }); 3172 return result; 3173 } 3174 3175 function loadLocalizations(baseUrl, fileName) { 3176 let fileNameBase = fileName; 3177 let extension = ''; 3178 3179 // Parse file name and extension. 3180 const lastDot = fileName.lastIndexOf('.'); 3181 if(lastDot !== -1) { 3182 fileNameBase = fileName.substring(0, lastDot); 3183 extension = fileName.substring(lastDot); 3184 } 3185 3186 // Language code is always available, try to load configuration for that language. 3187 fileNameBase += ('_' + mLocale.language); 3188 return loadLocalization(baseUrl + fileNameBase + extension) 3189 .then(function() { 3190 // If region code is also available, try to load configuration for that region as well. 3191 if(mLocale.region != null) { 3192 fileNameBase += ('-' + mLocale.region.toLowerCase()); 3193 return loadLocalization(baseUrl + fileNameBase + extension) 3194 } 3195 }); 3196 } 3197 3198 function loadLocalization(url) { 3199 return NNLUtils.loadResource(url, 'json', false) 3200 .then(applyConfiguration) 3201 .catch(function(e){ 3202 // Ignore errors when loading localized configurations 3203 // as the constructed URL may not exist. 3204 console.log(e); 3205 }); 3206 } 3207 3208 function applyConfiguration(uiConfig) { 3209 const wrapper = new JsonWrapper(uiConfig); 3210 if(mWrapper == null) { 3211 mWrapper = wrapper; 3212 } else { 3213 mWrapper.mergeJsonObjectsRecursively(wrapper); 3214 } 3215 } 3216 3217 applyConfiguration(DefaultUIConfig); 3218 3219 // A query to check if dark mode is enabled. 3220 const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)'); 3221 3222 // Enables or disables dark mode based on configured behavior and system mode. 3223 this.setDarkMode = function() { 3224 let propName = 'lightImage'; 3225 // If automatic handling is configured, enable or disable dark mode based on the query match. 3226 // If explicit mode is configured, enable or disable based on the configured value. 3227 if((AppSdkConfig.darkMode === NNLDarkMode.AUTOMATIC && darkModeQuery.matches) || AppSdkConfig.darkMode === NNLDarkMode.ON) { 3228 document.body.classList.add('nnl-dark-mode'); 3229 propName = 'darkImage'; 3230 } else { 3231 document.body.classList.remove('nnl-dark-mode'); 3232 } 3233 3234 document.querySelectorAll('[data-tag*="images:"]').forEach(function(image) { 3235 if(image.dataset[propName] != null) { 3236 if(image instanceof HTMLImageElement) { 3237 image.src = image.dataset[propName]; 3238 } else if(image instanceof HTMLLinkElement && image.rel === 'icon') { 3239 image.href = image.dataset[propName]; 3240 } else { 3241 image.style.content = 'url("' + image.dataset[propName] + '")'; 3242 } 3243 } 3244 }); 3245 } 3246 3247 // Listen for dark mode change events and enable or disable it accordingly. 3248 if(typeof darkModeQuery.addEventListener === 'function') { 3249 darkModeQuery.addEventListener('change', this.setDarkMode); 3250 } 3251})(); 3252 3253/**
3254 * This object contains many configurable properties that determine some behaviors of the SDK. 3255 * @property {number} adaptiveTimeout=70 - Adaptive operation timeout in seconds. 3256 * @property {number} ajaxCallConnectionTimeout=15 - ajax call connection timeout in seconds. 3257 * @property {boolean} serverNamingEnabled=true - Enables / Disables server-side default naming for passkeys and security keys. 3258 * @property {boolean} sendCredentialProviderInfo=true - Enables / Disables sending the credential provider information to the server. 3259 * @property {Object} sendLocation - Controls the generation of the location extension. 3260 * @property {ExtensionMode} sendLocation.init=NEVER - to generate in init request payload. 3261 * @property {ExtensionMode} sendLocation.finish=REQUESTED - to generate in finish request payload. 3262 * @property {ExtensionMode} sendLocation.protocol=NEVER - to generate in protocol message. 3263 * @property {Object} sendMetrics - Controls the generation of the metrics extension. 3264 * @property {ExtensionMode} sendMetrics.init=NEVER - to generate in init request payload. 3265 * @property {ExtensionMode} sendMetrics.finish=ALWAYS - to generate in finish request payload. 3266 * @property {ExtensionMode} sendMetrics.protocol=NEVER - to generate in protocol message. 3267 * @property {Object} oob - Contains parameters for OOB operations. 3268 * @property {number} oob.pollingInterval=1000 - Polling interval for OOB operation in milliseconds. 3269 * @property {Object} otp - OTP method anti hammering configuration. 3270 * @property {number} otp.maxFalseAttempts=3 - Maximum number of failed attempts to start OTP authentication. 3271 * @property {number} otp.probationPeriod=0 - The time period in seconds during which failed attempts are allowed. 3272 * @property {number} otp.lockoutPeriod=10 - The time period in seconds during which the OTP authentication method will be locked. 3273 * @property {Object} metricConfig - Configures the metrics unit and range for historical data in JS App SDK calls. 3274 * @property {number} metricConfig.range=2 - The total count of metrics to be stored. 3275 * @property {MetricTimeUnit} metricConfig.unit=DAY - The data capacity to be stored in each metric. 3276 * @property {Object} applink - Contains parameters for App Linking. 3277 * @property {string} applink.linkUrlBase - A URL used to launch the native mobile app. 3278 * @property {string} applink.hmsDomain - Domain name (without protocol) for HMS App Linking. 3279 * @property {string} applink.hmsAndroidPackage - The package name of the Android application to open with the HMS App Link. 3280 * @property {boolean} applink.enableHmsAppLink=conditional - Enables / Disables HMS App Linking. By default HMS App Linking is enabled on HMS devices. 3281 * @property {NNLDarkMode} darkMode=OFF - Specifies how the built in SDK views should handle dark mode. Should be set before triggering any view. 3282 */ 3283var AppSdkConfig = { 3284 adaptiveTimeout: 70, 3285 ajaxCallConnectionTimeout: 15, 3286 serverNamingEnabled: true, 3287 sendCredentialProviderInfo: true, 3288 3289 sendLocation: { 3290 init: ExtensionMode.NEVER, 3291 finish: ExtensionMode.REQUESTED, 3292 protocol: ExtensionMode.NEVER 3293 }, 3294 3295 sendMetrics: { 3296 init: ExtensionMode.NEVER, 3297 finish: ExtensionMode.ALWAYS, 3298 protocol: ExtensionMode.NEVER 3299 }, 3300 3301 oob: { 3302 regBaseUrl: "", 3303 authBaseUrl: "", 3304 pollingInterval: 1000 3305 }, 3306 3307 otp: { 3308 maxFalseAttempts: 3, 3309 probationPeriod: 0, 3310 lockoutPeriod: 10 3311 }, 3312 3313 metricConfig: { 3314 range: 2, 3315 unit: 'DAY' 3316 }, 3317 3318 applink: { 3319 linkUrlBase: "", 3320 hmsDomain: "", 3321 hmsAndroidPackage: "", 3322 enableHmsAppLink: (navigator.userAgent.match('HMSCore') && !navigator.userAgent.match('GMSCore')) 3323 }, 3324 3325 darkMode: NNLDarkMode.OFF 3326} 3327 3328/** 3329 * @hideconstructor 3330 * @class 3331 * Allows to specify retrieve various AppSdk related properties. 3332 * <br/>Supported properties are: 3333 * 3334 * Property | Description 3335 * -------------- | ----------- 3336 * AppName | A unique name identifying the web application. 3337 * AppDisplayName | User friendly application name. 3338 * DeviceID | Device ID associated with the Browser. 3339 * SdkVersion | NNL SDK version. Read only. 3340 */ 3341const AppSdkInfo = new (function() { 3342 var appName = window.location.origin; 3343 var appDisplayName = window.location.origin; 3344 3345 /** 3346 * Returns the version of App SDK. 3347 * @return {string} NNL SDK version. 3348 * @method AppSdkInfo.getSdkVersion 3349 * @static 3350 */
3351 this.getSdkVersion = function() { 3352 return '9.2.0.281'; 3353 } 3354 3355 /** 3356 * Sets custom application name. 3357 * Note: The custom application name provided by the Web App is unique across all AppSdk instances 3358 * and does not persist when page is closed or reloaded. 3359 * @param {string} customAppName - The custom application name. 3360 * @method AppSdkInfo.setAppName 3361 * @static 3362 */ 3363 this.setAppName = function(customAppName) { 3364 appName = customAppName; 3365 } 3366 3367 /** 3368 * Sets custom application display name. 3369 * Note: The custom application display name provided by the Web App is unique across all AppSdk instances 3370 * and does not persist when page is closed or reloaded. 3371 * @param {string} customAppDisplayName - The custom application display name. 3372 * @method AppSdkInfo.setAppDisplayName 3373 * @static 3374 */ 3375 this.setAppDisplayName = function(customAppDisplayName) { 3376 appDisplayName = customAppDisplayName; 3377 } 3378 3379 /** 3380 * Sets the custom deviceId to be used instead of the default one. 3381 * Note: The custom deviceId provided by the Web App is unique across all AppSdk instances 3382 * and does not persist when page is closed or reloaded. 3383 * @param {string} deviceId Custom deviceId which uniquely identifies the device. 3384 * @method AppSdkInfo.setDeviceId 3385 * @static 3386 */ 3387 this.setDeviceId = function(deviceId) { 3388 return NNLUtils.initialize().then(function() { 3389 return nnl.FIDOClient.setDeviceId(deviceId); 3390 }); 3391 } 3392 3393 /** 3394 * Retrieves the stored application name. 3395 * Either default, or custom application name (if set by [setAppName]{@link AppSdkInfo.setAppName}) is returned. 3396 * @returns {string} application name. 3397 * @method AppSdkInfo.getAppName 3398 * @static 3399 */ 3400 this.getAppName = function() { 3401 return appName; 3402 } 3403 3404 /** 3405 * Retrieves the stored application display name. 3406 * Either default, or custom application display name (if set by [setAppDisplayName]{@link AppSdkInfo.setAppDisplayName}) is returned. 3407 * @returns {string} application display name. 3408 * @method AppSdkInfo.getAppDisplayName 3409 * @static 3410 */ 3411 this.getAppDisplayName = function() { 3412 return appDisplayName; 3413 } 3414 3415 /** 3416 * Returns the Device ID associated with the Browser. 3417 * Either default, or custom Device ID (if set by [setDeviceId]{@link AppSdkInfo.setDeviceId}) is returned. 3418 * @returns {Promise} A Promise that will be resolved with the device ID associated with the Browser. 3419 * @method AppSdkInfo.getDeviceId 3420 * @static 3421 */ 3422 this.getDeviceId = function() { 3423 return NNLUtils.initialize().then(function() { 3424 return nnl.FIDOClient.getDeviceId(); 3425 }); 3426 } 3427 3428 Object.freeze(this); 3429})(); 3430 3431/** 3432 * Defines method for changing progress indicator visibility. 3433 * @interface IndicatorListener 3434 */ 3435 3436/** 3437 * Called when progress indicator visibility needs to be changed. 3438 * @method IndicatorListener#changeIndicatorVisibility 3439 * @param {boolean} show - Flag indicating whether progress indicator should be visible or not. 3440 */ 3441 3442 3443/** 3444 * @hideconstructor 3445 * @class 3446 * Singleton for controlling progress indicator. 3447 */ 3448const ProgressIndicator = (function() { 3449 let instance = null; 3450 3451 // Default implementation of IndicatorListener 3452 const NNLIndicatorListener = function() { 3453 this.changeIndicatorVisibility = function(show) { 3454 const progress = document.querySelector('#nnl-progress-wrapper'); 3455 if(show) { 3456 new ViewWrapper(document.querySelector('#nnl-progress')).onShow(); 3457 progress.style.display = 'block'; 3458 } else { 3459 progress.style.display = 'none'; 3460 } 3461 } 3462 } 3463 3464 function ProgressIndicatorImpl() { 3465 let counter = 0; 3466 let suspended = false; 3467 let listener = new NNLIndicatorListener(); 3468 3469 /** 3470 * Increments the progress indicator count. 3471 * Shows the progress indicator if the counter was 0 and indicator is not suspended. 3472 * @method ProgressIndicator#incCounter 3473 */ 3474 this.incCounter = function() { 3475 if(++counter === 1 && !suspended) { 3476 listener.changeIndicatorVisibility(true); 3477 } 3478 } 3479 3480 /** 3481 * Decrements the progress indicator count. 3482 * Hides the progress indicator if the counter reaches 0 and indicator is not suspended. 3483 * @method ProgressIndicator#decCounter 3484 */ 3485 this.decCounter = function() { 3486 if(counter === 0) { 3487 console.warn('Attempt to decrement ProgressIndicator counter when it is 0.'); 3488 return; 3489 } 3490 3491 if(--counter === 0 && !suspended) { 3492 listener.changeIndicatorVisibility(false); 3493 } 3494 } 3495 3496 /** 3497 * Suspends the progress indicator and hides it if it is visible. 3498 * @method ProgressIndicator#suspend 3499 */ 3500 this.suspend = function() { 3501 if(suspended) { 3502 console.warn('Attempt to suspend already suspended ProgressIndicator.'); 3503 return; 3504 } 3505 3506 suspended = true; 3507 if(counter > 0) { 3508 listener.changeIndicatorVisibility(false); 3509 } 3510 } 3511 3512 /** 3513 * Restores the progress indicator from suspended state and shows it if it was visible before. 3514 * @method ProgressIndicator#resume 3515 */ 3516 this.resume = function() { 3517 if(!suspended) { 3518 console.warn('Attempt to resume non suspended ProgressIndicator.'); 3519 return; 3520 } 3521 3522 suspended = false; 3523 if(counter > 0) { 3524 listener.changeIndicatorVisibility(true); 3525 } 3526 3527 } 3528 3529 /** 3530 * Sets a new listener for progress indicator. 3531 * @returns {IndicatorListener} Previous listener. 3532 * @method ProgressIndicator#setListener 3533 */ 3534 this.setListener = function(newListener) { 3535 var oldListener = listener; 3536 listener = newListener; 3537 return oldListener; 3538 } 3539 3540 Object.freeze(this); 3541 } 3542 3543 return { 3544 /** 3545 * Returns ProgressIndicator instance. 3546 * @returns {ProgressIndicator} The single ProgressIndicator instance. 3547 * @memberof ProgressIndicator 3548 */ 3549 getInstance: function() { 3550 if(instance === null) { 3551 instance = new ProgressIndicatorImpl(); 3552 } 3553 3554 return instance; 3555 } 3556 } 3557})(); 3558Object.freeze(ProgressIndicator); 3559 3560//Singleton for displaying short toast message. 3561const NNLToastMessage = new (function() { 3562 // The offset from bottom of screen when virtual keyboard is opened. 3563 let mCurrentOffset = 0;
3564 if(window.visualViewport != null) { 3565 // Initialize the NNLUtils to ensure the DOM readiness. 3566 NNLUtils.initialize() 3567 .then(function () { 3568 // When virtual keyboard is opened or closed, the visualViewport is being resized. 3569 // In that case we need to update the virtual keyboard offset for new toast messages as well as 3570 // update position of existing ones. 3571 window.visualViewport.addEventListener('resize', function() { 3572 // The difference between actual window height and visual viewport height is the height of virtual keyboard. 3573 let newOffset = (window.innerHeight - window.visualViewport.height) * window.visualViewport.scale; 3574 let offsetDiff = newOffset - mCurrentOffset; 3575 // If there is an actual change in offset (keyboard opened or closed on mobile browser) 3576 if(offsetDiff !== 0) { 3577 document.querySelector('#nnl-toast').childNodes.forEach(function(childToast) { 3578 childToast.style.marginBottom = (parseInt(childToast.style.marginBottom) + offsetDiff) + 'px'; 3579 }); 3580 mCurrentOffset = newOffset; 3581 } 3582 }); 3583 }); 3584 } 3585 3586 let mNextToastMargin = 0; 3587 3588 this.show = function(message) { 3589 let toastContainer = document.querySelector('#nnl-toast'); 3590 let toast = document.querySelector('#nnl-toast-template').cloneNode(true); 3591 toast.removeAttribute('id'); 3592 toast.style.display = 'block'; 3593 toast.querySelector('span').innerText = message; 3594 new ViewWrapper(toast).onShow(); 3595 3596 if(toastContainer.childNodes.length === 0) { 3597 toastContainer.style.display = 'block'; 3598 } 3599 3600 toastContainer.prepend(toast); 3601 toast.style.marginBottom = mNextToastMargin + mCurrentOffset + 'px'; 3602 mNextToastMargin += (toast.offsetHeight + 10); 3603 setTimeout(function() { 3604 toast.style.opacity = 1; 3605 }, 10); 3606 3607 toast.addEventListener('transitionend', function(event) { 3608 if(event.propertyName !== 'opacity') { 3609 return; 3610 } 3611 3612 if(toast.style.opacity === '1') { 3613 toast.classList.add('nnl-toast-visible'); 3614 } else { 3615 toast.remove(); 3616 if(!toastContainer.hasChildNodes()) { 3617 toastContainer.style.display = 'none'; 3618 } 3619 } 3620 }); 3621 3622 setTimeout(function() { 3623 let margin = toast.offsetHeight + 10; 3624 mNextToastMargin -= margin; 3625 toastContainer.childNodes.forEach(function(childToast) { 3626 if(childToast !== toast) { 3627 childToast.style.marginBottom = (parseInt(childToast.style.marginBottom) - margin) + 'px'; 3628 } 3629 }); 3630 toast.style.opacity = 0; 3631 }, 3000); 3632 } 3633 3634 Object.freeze(this); 3635})(); 3636 3637/** 3638 * @hideconstructor 3639 * @class 3640 * Static class that provides methods for storing and retrieving data. 3641 * Data is stored in browser's local storage and can be backed up in cookies. 3642 */ 3643const NNLStorage = new (function() { 3644 var mEndpoint = null; 3645 3646 // Contains items loaded from cookies, key is item name, value is SET_SEPARATELY or SET_GLOBALLY. 3647 // The initialize method will add all globally stored items into this. 3648 // The fetchItem method will add each fetched separately stored item into this. 3649 // The setItem method will add/update the stored item according to the setSeparately flag. 3650 var mExistingItems = {}; 3651 3652 const SET_SEPARATELY = 'separately'; 3653 const SET_GLOBALLY = 'globally'; 3654 3655 // Cleanup unneded flags. 3656 Object.keys(localStorage).forEach(function(key) { 3657 if(key.indexOf('nnl.cookieSet.') === 0) { 3658 localStorage.removeItem(key) 3659 } 3660 }); 3661 3662 /** 3663 * Initializes the storage with the REST API endpoint that will allow to store and retrieve data in cookies. 3664 * When called, the method will retrieve existing data from cookies and update the local storage. 3665 * @param {string} endpoint The cookie storage REST API endpoint. 3666 * @returns {Promise} Promise that will be resolved when data is successfully transferred from cookies to local storage. 3667 * @memberof NNLStorage
3668 * @alias NNLStorage.initialize 3669 */ 3670 this.initialize = function(endpoint) { 3671 mEndpoint = endpoint; 3672 mExistingItems = {}; 3673 3674 var message = { 3675 operation: 'GET' 3676 } 3677 3678 return NNLUtils.sendRestMessage(mEndpoint, message) 3679 .then(function(response) { 3680 if(response.outcome === Outcome.SUCCESS) { 3681 for(var name in response.data) { 3682 localStorage.setItem(name, response.data[name]); 3683 // All items set for root endpoint are considered global. 3684 mExistingItems[name] = SET_GLOBALLY; 3685 } 3686 } 3687 }); 3688 }; 3689 3690 /** 3691 * Retrieves the item with the specified name from a cookie and stores it in the local storage. 3692 * This method should be called only for items that were stored with storeSeparately=true. 3693 * @param {string} name The name of the item to retrieve. 3694 * @returns {Promise} A promise that will be resolved with the stored item. 3695 */ 3696 this.fetchItem = function(name) { 3697 // If cookie storage is not configured or the requested item is already fetched, just return the value without making request. 3698 if(mEndpoint == null || mExistingItems[name] != null) { 3699 return Promise.resolve(localStorage.getItem(name)); 3700 } 3701 3702 var message = { 3703 operation: 'GET' 3704 } 3705 3706 // Append the name to the endpoint to send only cookies set separately for that name. 3707 return NNLUtils.sendRestMessage(mEndpoint + '/' + encodeURIComponent(name), message) 3708 .then(function(response) { 3709 if(response.outcome === Outcome.SUCCESS) { 3710 // Store the value 3711 localStorage.setItem(name, response.data[name]); 3712 // Remember that the value is already fetched. 3713 mExistingItems[name] = SET_SEPARATELY; 3714 3715 return response.data[name]; 3716 } 3717 3718 return null; 3719 }); 3720 } 3721 3722 /** 3723 * Returns a stored item. 3724 * This method should be called only for items that were stored with storeSeparately=false. 3725 * @param {string} name Name of the item to get. 3726 * @returns {string} The stored item. 3727 * @memberof NNLStorage 3728 * @alias NNLStorage.getItem 3729 */ 3730 this.getItem = function(name) { 3731 var value = localStorage.getItem(name); 3732 // If the requested item exists in the local storage, but is not stored in a cookie, store it. 3733 if(mEndpoint != null && value != null && mExistingItems[name] == null) { 3734 this.setItem(name, value); 3735 } 3736 3737 return value; 3738 }; 3739 3740 /** 3741 * Stores the specified item. 3742 * @param {string} name Name of the item to store. 3743 * @param {string} value Value to store. 3744 * @param {boolean} [storeSeparately] Specifies whether the item should be stored 3745 * in a separate cookie with its own path or not. 3746 * @memberof NNLStorage 3747 * @alias NNLStorage.setItem 3748 */ 3749 this.setItem = function(name, value, storeSeparately) { 3750 localStorage.setItem(name, value); 3751 if(mEndpoint != null) { 3752 var message = { 3753 operation: 'SET', 3754 name: name, 3755 value: value 3756 } 3757 3758 // If the cookie should be set separately, append its name to the path. 3759 var endpoint = mEndpoint; 3760 if(storeSeparately) { 3761 endpoint += ('/' + encodeURIComponent(name)); 3762 3763 // If there already is a cookie stored with global path, delete it. 3764 if(mExistingItems[name] === SET_GLOBALLY) { 3765 var deleteMessage = { 3766 operation: 'DELETE', 3767 name: name 3768 } 3769 // Delete the cookie with the global path (without appending the name). 3770 NNLUtils.sendRestMessage(mEndpoint, deleteMessage); 3771 } 3772 } 3773 3774 // Remember how the cookie was set. 3775 mExistingItems[name] = storeSeparately ? SET_SEPARATELY : SET_GLOBALLY; 3776 3777 // Store the cookie with the calculated path. 3778 NNLUtils.sendRestMessage(endpoint, message) 3779 .then(function(response) { 3780 if(response.outcome !== Outcome.SUCCESS) { 3781 // Remove the flag if the cookie is not successfully set. 3782 delete mExistingItems[name]; 3783 } 3784 }); 3785 } 3786 }; 3787 3788 /** 3789 * Deletes a stored item. 3790 * @param {string} name Name of the item to delete. 3791 * @memberof NNLStorage 3792 * @alias NNLStorage.removeItem 3793 */ 3794 this.removeItem = function(name) { 3795 localStorage.removeItem(name); 3796 if(mEndpoint != null) { 3797 var message = { 3798 operation: 'DELETE', 3799 name: name 3800 } 3801 3802 var endpoint = mEndpoint; 3803 // It the item being deleted was not stored in a global cookie, then append its name to the path. 3804 if(mExistingItems[name] !== SET_GLOBALLY) { 3805 endpoint += ('/' + encodeURIComponent(name)); 3806 } 3807 3808 // Remove the flag regardless of the operation result. 3809 delete mExistingItems[name]; 3810 3811 // Delete the cookie for the calculated path. 3812 NNLUtils.sendRestMessage(endpoint, message); 3813 } 3814 }; 3815 3816 Object.freeze(this); 3817})(); 3818 3819 3820/** 3821 * @hideconstructor 3822 * @class 3823 * @classdesc A facade singleton, whose APIs are used for detection of browser compatibility with FIDO APIs. 3824 */ 3825const BrowserInfo = new (function () { 3826 const PUB_KEY_GET_FEATURE = 'publickey-credentials-get'; 3827 3828 /** 3829 * The regexp will test whether the browser is Chromium based or not. 3830 * @type {RegExp} 3831 */ 3832 const CHROMIUM_REGEX = /Chrom(e|ium)/; 3833 3834 /** 3835 * Regular expressions for parsing the browser information 3836 * from the User-Agent String. 3837 * @type {RegExp} 3838 */ 3839 const UA_STR_BROWSER_INFO_MATCHER = /(opr|edg|chrome|safari|firefox|msie)\/(\d+[.\d]+)?(\ mobile)?(?:\s(safari)\/[.\d]+)?$/i; 3840 3841 /** 3842 * The regex will match with the general browsers on iOS (excluding 3843 * Safari). 3844 * Chrome, Firefox, Edge. 3845 * <b>Note:</> Currently, for the Brave browser there is no suitable API 3846 * for determining the browser name. 3847 * @type {RegExp} 3848 */ 3849 const UA_STR_IOS_MOBILE_BROWSER_MATCHER = /((?:cr|fx|edg)ios)\/(\d+[.\d]+)/i; 3850 3851 /** 3852 * This regexp is used for determining the version of the iOS, which 3853 * matches with the Safari version. 3854 * First capturing group is the version included in the device info (E.g. iPhone 3855 * OS 16_2). 3856 * The second capturing group is for getting the version from the 3857 * "Version/" prefixed part of the UA (E.g "Version/16.2") . 3858 * @type {RegExp} 3859 */ 3860 const UA_STR_APPLE_VERSION_MATCHER = /(?:(?:ipod|iphone|ipad|mac) os ((?!x)[^\s]+)|(?:version\/([^\s]+)))
3860/i; 3861 3862 /** 3863 * A regex for determining whether the browser is WebKit based or not. 3864 * @type {RegExp} 3865 */ 3866 const UA_WEBKIT_REGEX = /(i(pod|phone|pad)|mac os)/i; 3867 3868 const EXCLUDE_UA_BRAND_REGEXES = [ 3869 // As per specification all user agents should include an arbitrary GREASE-like value in the brands array of client hints. 3870 // The regular expression captures the arbitrary value as defined in the specification and excludes it from the brands list. 3871 // This Client Hint might be revised and removed in the future as discussed in the https://github.com/WICG/ua-client-hints/pull/174 pull request. 3872 /([^a-z0-9])?not([^a-zA-Z0-9])((a([^a-zA-Z0-9])?brand([^a-z0-9])?)|([^a-z0-9]?browser([^a-z0-9])?))/i, 3873 /Chromium/i 3874 ]; 3875 3876 const BROWSER_NAME_MAP = { 3877 CriOS: 'Google Chrome', 3878 Chrome: 'Google Chrome', 3879 Safari: 'Safari', 3880 Firefox: 'Firefox', 3881 FxiOS: 'Firefox', 3882 Edg: 'Microsoft Edge', 3883 EdgiOS: 'Microsoft Edge', 3884 Opera: 'Opera', 3885 MSIE: 'Internet Explorer' 3886 } 3887 3888 let mName, mVersion, mIsChromiumBased, mIsWebKitBased; 3889 if (clientHintsAPIAvailable()) { 3890 getInfoFromCH(); 3891 } else { 3892 getInfoFromUAString(); 3893 } 3894 3895 /** 3896 * Getter for the User-Agent name. 3897 * @returns {string} the User-Agent name. (E.g. Google Chrome, Firefox, Safari, etc.) 3898 */ 3899 this.getName = function () { 3900 return mName; 3901 } 3902 3903 /** 3904 * Getter for the User-Agent version. 3905 * @returns {string} the User-Agent version. 3906 */ 3907 this.getVersion = function () { 3908 return mVersion; 3909 } 3910 3911 /** 3912 * Checks whether the FIDO authentication is possible in cross-origin iframes. 3913 * @returns {boolean} A boolean indicating whether the current user agent supports WebAuthn get in a cross-origin iframe. 3914 */ 3915 this.supportsFIDOInCrossOriginIFrame = function () { 3916 const policy = document.permissionsPolicy || document.featurePolicy; 3917 3918 // If the Permission-Policy (A.K.A Feature-Policy) API is available, 3919 // check if the 'publickey-credentials-get' is included in the list. 3920 if (policy != null && policy.features().indexOf(PUB_KEY_GET_FEATURE) !== -1) { 3921 return true; 3922 } 3923 3924 if (mVersion == null) { 3925 return false; 3926 } 3927 3928 let versionSplit = mVersion.split(/[_.]/); 3929 let major = parseInt(versionSplit[0] || '0'); 3930 let minor = parseInt(versionSplit[1] || '0'); 3931 3932 if (mIsChromiumBased) { 3933 return major > 84; 3934 } 3935 3936 if (mIsWebKitBased) { 3937 // When the user-agent runs on iOS or macOS, the FIDO in cross-origin iframes is supported in Safari 15.5+. 3938 // As per version table of iOS and macOS Safari the WebKit running in the platforms are of the same version. 3939 return (major === 15 && minor >= 5) || major > 15; 3940 } 3941 3942 return false; 3943 } 3944 3945 function clientHintsAPIAvailable() { 3946 // Check the required values of client hints API to be available. 3947 // In some cases the API might still be available, but with empty values. E.g.: In the desktop browser with mobile inspect mode. 3948 return navigator.userAgentData != null && 3949 (navigator.userAgentData.platform != null && navigator.userAgentData.platform !== '') && 3950 (navigator.userAgentData.brands != null && navigator.userAgentData.brands.length > 0); 3951 } 3952 3953 /** 3954 * Parses browser information using the User Agent Client-Hints API. 3955 */ 3956 function getInfoFromCH() { 3957 // Get a copy of brands list 3958 let uaBrands = JSON.parse(JSON.stringify(navigator.userAgentData.brands)); 3959 // Iterate through the brands array and exclude the redundant one. 3960 var brandIndex = 0; 3961 while (brandIndex < uaBrands.length) { 3962 var increment = true; 3963 var brand = uaBrands[brandIndex].brand; 3964 if (CHROMIUM_REGEX.test(brand)) { 3965 mIsChromiumBased = true; 3966 } 3967 3968 for (var excludedBrandKey in EXCLUDE_UA_BRAND_REGEXES) { 3969 // Excluded the redundant UA brands. 3970 if (EXCLUDE_UA_BRAND_REGEXES[excludedBrandKey].test(brand)) { 3971 uaBrands.splice(brandIndex, 1); 3972 increment = false; 3973 break; 3974 } 3975 } 3976 3977 // If a single item left in the brands array - break the iteration. 3978 if (uaBrands.length === 1) { 3979 break; 3980 } 3981 3982 if (increment) { 3983 brandIndex++ 3984 } 3985 } 3986 3987 let platform = navigator.userAgentData.platform.toLowerCase(); 3988 mIsWebKitBased = (platform === 'ios' || platform === 'macos'); 3989 mName = uaBrands[0].brand; 3990 mVersion = uaBrands[0].version; 3991 } 3992 3993 /** 3994 * Parses browser information using the User Agent String. 3995 */ 3996 function getInfoFromUAString() { 3997 let ua = navigator.userAgent; 3998 mIsChromiumBased = CHROMIUM_REGEX.test(ua); 3999 4000 // The matched groups will include the following. 4001 // 1st element - the UA name. E.g. Chrome, Edg, etc. 4002 // 2nd element - the version of UA. E.g. 110. 4003 let generalInfoMatch = ua.match(UA_STR_BROWSER_INFO_MATCHER); 4004 // In the IE the metadata will be null. So return. 4005 if (generalInfoMatch == null) { 4006 return; 4007 } 4008 4009 // This will match with the general browser names on iOS. 4010 let iosBrowserMatch = ua.match(UA_STR_IOS_MOBILE_BROWSER_MATCHER); 4011 // This will match with the Apple device version(iPhone, iPod, iPad, 4012 // macOS). 4013 let versionMatch = ua.match(UA_STR_APPLE_VERSION_MATCHER); 4014 4015 mIsWebKitBased = UA_WEBKIT_REGEX.test(ua); 4016 mVersion = versionMatch != null ? (versionMatch[1] || versionMatch[2]) : generalInfoMatch[2]; 4017 mName = BROWSER_NAME_MAP[iosBrowserMatch != null ? iosBrowserMatch[1] : generalInfoMatch[1]] || "Chromium"; 4018 } 4019 4020 /** 4021 * Determines whether the current browser is a mobile browser. 4022 * 4023 * @returns {boolean} Returns true if the browser is a mobile browser, otherwise returns false. 4024 */ 4025 this.isMobileBrowser = function () { 4026 var userAgent = navigator.userAgent || navigator.vendor || window.opera; 4027 // Windows Phone must come first because its UA also contains "Android" 4028 return ((/windows phone/i.test(userAgent)) || 4029 (/android/i.test(userAgent)) || 4030 // iOS detection from: https://stackoverflow.com/questions/9038625 4031 ((/iPad|iPhone|iPod/.test(userAgent) || (navigator.platform === 'MacIntel' 4032 && navigator.maxTouchPoints > 1)) && !window.MSStream)) 4033 } 4034 4035 /** 4036 * Check if the current device is running Mobile Safari. 4037 * 4038 * @returns {boolean} Whether the device is running Mobile Safari. 4039 */ 4040 this.isMobileSafari = function() { 4041 return (/iPad|iPhone|iPod/.test(navigator.userAgent) && 4042 navigator.userAgent.match('Safari') && 4043 !navigator.userAgent.match('CriOS') && 4044 !navigator.userAgent.match('FxiOS')); 4045 } 4046 4047 Object.freeze(this); 4048})(); 4049 4050/** 4051 * The callback that is called when a FIDO operation is completed and view needs to be refreshed. 4052 * @callback ViewUpdateListenerCallback 4053 * @param {FidoOperationType} operationType The operation type that has completed. 4054 * @param {Error} error The error information. If the operation was successful this will be null. 4055 */ 4056 4057/** 4058 * @class 4059 * This class listens for FIDO operation completion events and calls the specified callback if 4060 * the specified view element is in the document. When the element is no longer in the document, 4061 * it stops listening for events. 4062 * @param {Object} view An HTML element to observe. 4063 * @param {ViewUpdateListenerCallback} callback The callback to call when view refresh is needed. 4064 */ 4065const ViewUpdateListener = function(view, callback) { 4066 const mListener = new (function() { 4067 this.onComplete = function(operationType, operationData, extras) { 4068 refreshIfVisible(operationType, null); 4069 } 4070 4071 this.onFailure = function(operationType, error) { 4072 refreshIfVisible(operationType, error); 4073 } 4074 4075 function refreshIfVisible(operationType, error) { 4076 // If the view is still in the document, call controller to refresh. 4077 if(document.body.contains(view)) { 4078 callback(operationType, error); 4079 } else { 4080 // Otherwise there is no need to listen for operation results. 4081 OperationResultListener.removeInstance(mListener); 4082 } 4083 } 4084 })(); 4085 4086 /** 4087 * Starts observing FIDO operation completions and view presence. 4088 */ 4089 this.start = function() { 4090 OperationResultListener.addInstance(mListener); 4091 } 4092 4093 /** 4094 * Stops observing FIDO operation completions and view presence. 4095 */ 4096 this.stop = function() { 4097 OperationResultListener.removeInstance(mListener); 4098 } 4099 4100 Object.freeze(this); 4101} 4102 4103/** 4104 * This interface defines methods for displaying the Sign Up form and confirmation dialogs when registering 4105 * authentication methods during sign up. 4106 * @interface ISignUpLiveData 4107 */ 4108 4109/** 4110 * This method will be called to retrieve the sign up data from the user. The implementation should present a 4111 * form to the user to collect the data and call {@link SignUpController#onSignUpFormSubmitted|SignUpController.onSignUpFormSubmitted} 4112 * when the user submits that form. 4113 * When this method is called first time, `error` will be `null`. 4114 * Then, if there is an error while processing the provided `signUpData`, this method will be called again with the actual error code.
4115 * @method ISignUpLiveData#getSignUpData 4116 * @param {Object} signUpOpts An object containing the fields that should be included in the sign up form. 4117 * @param {SignUpError} error The error code indicating what error has happened after the previous call to this method. 4118 * @param {String} [message] Additional error message. 4119 */ 4120 4121/** 4122 * This method will be called when it is required to get user's consent to register a particular method. 4123 * The implementation should show a confirmation dialog and ask the user whether they want to register a 4124 * method specified in the `method` parameter. When the user takes an action, the implementation should call 4125 * {@link SignUpController#onConfirmationResponse|SignUpController.onConfirmationResponse} and pass in the same 4126 * `method` parameter as well as the user's choice. 4127 * @method ISignUpLiveData#confirmMethodRegistration 4128 * @param {SignUpMethod} method The type of the method that the user needs to confirm to register. 4129 */ 4130 4131/** 4132 * This interface defines base methods for the view class that implements {@link ISignUpLiveData}. 4133 * @interface BaseSignUpView 4134 */ 4135 4136/** 4137 * This method is called when the view should be displayed in a specified container. 4138 * The implementation should prepare the resources for the view (e.g. load HTML fragment) and call 4139 * {@link SignUpController#onRefresh|SignUpController.onRefresh}. 4140 * @method BaseSignUpView#show 4141 * @param {Object} container An HTML element where the view should be placed. 4142 */ 4143 4144/** 4145 * This method is called when the view should be displayed in a modal dialog. 4146 * The implementation should prepare the resources for the view and modal dialog (e.g. load HTML fragment), 4147 * display the modal dialog and call {@link SignUpController#onRefresh|SignUpController.onRefresh}. 4148 * The implementation must hide the modal dialog before calling 4149 * {@link SignUpController#onSignUpFormSubmitted|SignUpController.onSignUpFormSubmitted}. 4150 * When {@link ISignUpLiveData#getSignUpData|ISignUpLiveData.getSignUpData} is called, 4151 * the implementation must display the modal dialog if it is not already visible and place the view in it. 4152 * If the user closes the modal dialog, the implementation must call 4153 * {@link SignUpController#onCancel|SignUpController.onCancel}. 4154 * @method BaseSignUpView#showModal 4155 */ 4156 4157/** 4158 * This method is called to close a modal dialog displayed by showMoodal. 4159 * The implementation should just close the modal dialog if it is visible. 4160 * @method BaseSignUpView#closeModal 4161 */ 4162 4163/** 4164 * This method implementation should return the {@link SignUpController} instance passed in in the constructor. 4165 * @method BaseSignUpView#getController 4166 * @returns {SignUpController} 4167 */ 4168 4169/** 4170 * Indicates the error when {@link ISignUpLiveData#getSignUpData|ISignUpLiveData.getSignUpData} 4171 * is called after an error happened. 4172 * @enum {string} 4173 */ 4174const SignUpError = { 4175 /** 4176 * The "fullName" field was missing or was empty in the "signUpData" passed to 4177 * {@link SignUpController#onSignUpFormSubmitted|SignUpController.onSignUpFormSubmitted} call. 4178 */ 4179 FULL_NAME_REQUIRED: 'FULL_NAME_REQUIRED', 4180 4181 /** 4182 * The "userName" field was missing or was empty in the "signUpData" passed to 4183 * {@link SignUpController#onSignUpFormSubmitted|SignUpController.onSignUpFormSubmitted} call. 4184 */ 4185 USERNAME_REQUIRED: 'USERNAME_REQUIRED', 4186 4187 /** 4188 * The "emailAddress" field was missing or was empty in the "signUpData" passed to 4189 * {@link SignUpController#onSignUpFormSubmitted|SignUpController.onSignUpFormSubmitted} call. 4190 */ 4191 EMAIL_ADDRESS_REQUIRD: 'EMAIL_ADDRESS_REQUIRD', 4192 4193 /** 4194 * The "phoneNumber" field was missing or was empty in the "signUpData" passed to 4195 * {@link SignUpController#onSignUpFormSubmitted|SignUpController.onSignUpFormSubmitted} call. 4196 */ 4197 PHONE_NUMBER_REQUIRED: 'PHONE_NUMBER_REQUIRED', 4198 4199 /** 4200 * The "userName" field in "signUpData" passed to 4201 * {@link SignUpController#onSignUpFormSubmitted|SignUpController.onSignUpFormSubmitted}
4201 call was invalid. 4202 */ 4203 INVALID_USERNAME: 'INVALID_USERNAME', 4204 4205 /** 4206 * The "emailAddress" field in "signUpData" passed to 4207 * {@link SignUpController#onSignUpFormSubmitted|SignUpController.onSignUpFormSubmitted} call was invalid. 4208 */ 4209 INVALID_EMAIL_ADDRESS: 'INVALID_EMAIL_ADDRESS', 4210 4211 /** 4212 * The "phoneNumber" field in "signUpData" passed to 4213 * {@link SignUpController#onSignUpFormSubmitted|SignUpController.onSignUpFormSubmitted} call was invalid. 4214 */ 4215 INVALID_PHONE_NUMBER: 'INVALID_PHONE_NUMBER', 4216 4217 /** 4218 * An account corresponding to the provided sign up data already exists. 4219 */ 4220 ACCOUNT_ALREADY_EXISTS: 'ACCOUNT_ALREADY_EXISTS', 4221 4222 /** 4223 * A network error has occurred. 4224 */ 4225 CONNECTION_ERROR: 'CONNECTION_ERROR', 4226 4227 /** 4228 * Generic error happened. 4229 */ 4230 OTHER: 'OTHER' 4231} 4232Object.freeze(SignUpError); 4233 4234/** 4235 * Indicates the type of method that the user needs to confirm to register during sign up. 4236 * @enum {string} 4237 */ 4238const SignUpMethod = { 4239 /** 4240 * A passkey (platform authenticator) is being registered. 4241 */ 4242 PASSKEY: 'PASSKEY', 4243 4244 /** 4245 * A security key (cross platform authenticator) is being registered. 4246 */ 4247 SECURITY_KEY: 'SECURITY_KEY', 4248 4249 /** 4250 * An ID document verification method is being registered. 4251 */ 4252 PHOTO_ID: 'PHOTO_ID' 4253} 4254Object.freeze(SignUpMethod); 4255 4256/** 4257 * A singleton factory class to create and provide {@link SignUpUIFactory} implementations. 4258 * @class 4259 * @hideconstructor 4260 */ 4261const SignUpUIFactory = new (function() { 4262 let mInstance = null; 4263 4264 /** 4265 * Sets the SignUpUIFactory instance. 4266 * @param {SignUpUIFactory} instance The new instance. 4267 * @alias SignUpUIFactory.setInstance 4268 */ 4269 this.setInstance = function(instance) { 4270 NNLUtils.validateArgument('instance', instance, 'object', null, false); 4271 mInstance = instance; 4272 } 4273 4274 /** 4275 * Returns {@link SignUpUIFactory} instance. 4276 * If no instance was set using the {@link SignUpUIFactory.setInstance|setInstance} method, 4277 * creates and returns {@link DefaultSignUpUIFactory} instance. 4278 * @returns {SignUpUIFactory} The factory instance to use. 4279 * @alias SignUpUIFactory.getInstance 4280 */ 4281 this.getInstance = function() { 4282 if(mInstance == null) { 4283 mInstance = new DefaultSignUpUIFactory(); 4284 } 4285 4286 return mInstance; 4287 } 4288 4289 /** 4290 * This method implementation should create and return {@link BaseSignUpView} implementation. 4291 * @method SignUpUIFactory#createView 4292 * @param {SignUpController} signUpController The SignUpController instance to call back after user input. 4293 * @returns {BaseSignUpView} The created instance. 4294 * @abstract 4295 */ 4296 4297 Object.freeze(this); 4298})(); 4299 4300/** 4301 * Default implementation of {@link SignUpUIFactory}. 4302 * @class 4303 * @implements {SignUpUIFactory} 4304 */ 4305const DefaultSignUpUIFactory = function() { 4306 /** 4307 * Creates and returns an instance of the default implementation of {@link BaseSignUpView}. 4308 * @param {SignUpController} signUpController The SignUpController instance to call back after user input. 4309 * @returns {BaseSignUpView} A {@link DefaultSignUpView} instance. 4310 */ 4311 this.createView = function(signUpController) { 4312 return new DefaultSignUpView(signUpController); 4313 } 4314 4315 Object.freeze(this); 4316} 4317 4318/** 4319 * @classdesc Default implementation of {@link BaseSignUpView} and {@link ISignUpLiveData}. 4320 * Presents the sign up form and confirmation dialogs to the user. 4321 * @class 4322 * @implements BaseSignUpView 4323 * @implements ISignUpLiveData 4324 * @param {SignUpController} signUpController The SignUpController instance to call back after user input. 4325 */ 4326const DefaultSignUpView = function(signUpController) { 4327 NNLUtils.validateArgument('signUpController', signUpController, 'object', SignUpController, false); 4328 4329 const _this = this; 4330 4331 let mSignUpDialog = null; 4332 let mSignUpFormTemplate; 4333 let mView; 4334 let mConfirmDialog; 4335 4336 // Load the HTML and store references to the UI elements. 4337 const mIsReady = NNLUtils.loadHTML('sign-up-ui.html', 'nnl-sign-up-section') 4338 .then(function() { 4339 mSignUpFormTemplate = document.querySelector('#nnl-sign-up-form-template'); 4340 mConfirmDialog = document.querySelector('#nnl-sign-up-confirmation-dialog'); 4341 }); 4342 4343 /** 4344 * Shows the view with sign up form in the specified container. 4345 * @param {Object} container An HTML element where the view should be placed. 4346 */ 4347 this.show = function(container) { 4348 NNLUtils.validateArgument('container', container, 'object', null, false); 4349 4350 mIsReady.then(function() { 4351 container.innerHTML = ''; 4352 mView = mSignUpFormTemplate.cloneNode(true); 4353 mView.removeAttribute('id');
4354 container.appendChild(mView); 4355 signUpController.onRefresh(_this); 4356 }); 4357 } 4358 4359 /** 4360 * Shows the view with sign up form in a modal dialog 4361 */ 4362 this.showModal = function() { 4363 mIsReady.then(function() { 4364 mSignUpDialog = document.querySelector('#nnl-sign-up-form-dialog'); 4365 mView = mSignUpDialog.querySelector('.nnl-dialog-body'); 4366 mView.innerHTML = ''; 4367 mView.appendChild(mSignUpFormTemplate.querySelector('form').cloneNode(true)); 4368 NNLUtils.showModal(mSignUpDialog, true); 4369 signUpController.onRefresh(_this); 4370 }); 4371 } 4372 4373 /** 4374 * Closes the modal dialog. 4375 */ 4376 this.closeModal = function() { 4377 if(mSignUpDialog != null) { 4378 NNLUtils.showModal(mSignUpDialog, false); 4379 } 4380 } 4381 4382 /** 4383 * Returns the {SignUpController} associated with this view. 4384 * @returns {SignUpController} 4385 */ 4386 this.getController = function() { 4387 return signUpController; 4388 } 4389 4390 /** 4391 * Presents a modal dialog with the sign up form containing fields specified in the `signUpOpts`. 4392 * @param {Object} signUpOpts An object containing the fields that should be included in the sign up form. 4393 * @param {SignUpError} error The error code indicating what error has happened after the previous call to this method. 4394 * @param {String} [message] Additional error message. 4395 */ 4396 this.getSignUpData = function(signUpOpts, error, message) { 4397 NNLUtils.validateArgument('signUpOpts', signUpOpts, 'object', null, false); 4398 NNLUtils.validateArgument('error', error, 'string', null, true); 4399 NNLUtils.validateArgument('message', message, 'string', null, true); 4400 4401 mView.style.pointerEvents = ''; 4402 const sections = {}; 4403 const signUpForm = NNLUtils.removeEventListeners(mView.querySelector('.nnl-sign-up-form')); 4404 const genericErrorSection = signUpForm.querySelector('[data-generic-error]'); 4405 hideErrors(genericErrorSection); 4406 4407 signUpForm.querySelectorAll('.nnl-sign-up-form-section').forEach(function(section) { 4408 // Store reference of the section to get the input value when the form is submitted. 4409 sections[section.dataset.sectionName] = section; 4410 4411 hideErrors(section); 4412 // Show or hide section according the provided option value. 4413 if(signUpOpts[section.dataset.sectionName] != null) { 4414 section.style.display = 'block'; 4415 const hint = section.querySelector('.nnl-sign-up-optional'); 4416 hint.style.display = signUpOpts[section.dataset.sectionName] === 'optional' ? 'inline' : 'none'; 4417 } else { 4418 section.style.display = 'none'; 4419 } 4420 }); 4421 4422 // If there is an error, show corresponding error message. 4423 switch(error) { 4424 case SignUpError.FULL_NAME_REQUIRED: 4425 showError(sections['fullName'], 'required', message); 4426 break; 4427 case SignUpError.USERNAME_REQUIRED: 4428 showError(sections['userName'], 'required', message); 4429 break; 4430 case SignUpError.EMAIL_ADDRESS_REQUIRD: 4431 showError(sections['emailAddress'], 'required', message); 4432 break; 4433 case SignUpError.PHONE_NUMBER_REQUIRED: 4434 showError(sections['phoneNumber'], 'required', message); 4435 break; 4436 4437 case SignUpError.INVALID_USERNAME: 4438 showError(sections['userName'], 'invalid', message); 4439 break; 4440 case SignUpError.INVALID_EMAIL_ADDRESS: 4441 showError(sections['emailAddress'], 'invalid', message); 4442 break; 4443 case SignUpError.INVALID_PHONE_NUMBER: 4444 showError(sections['phoneNumber'], 'invalid', message); 4445 break; 4446 4447 case SignUpError.ACCOUNT_ALREADY_EXISTS: 4448 showError(genericErrorSection, 'account-exists', message); 4449 break; 4450 case SignUpError.CONNECTION_ERROR: 4451 showError(genericErrorSection, 'connection', message); 4452 break; 4453 case SignUpError.OTHER: 4454 showError(genericErrorSection, 'generic', message); 4455 break; 4456 } 4457 4458 signUpForm.addEventListener('submit', function(event) { 4459 event.preventDefault(); 4460 const signUpData = {}; 4461 // When the form is submitted, gather the values only for the fields requested in the signUpOpts. 4462 for(let sectionName in sections) { 4463 if(signUpOpts[sectionName] != null) { 4464 signUpData[sectionName] = sections[sectionName].querySelector('input').value.trim() 4465 } 4466 } 4467 4468 hideUI(); 4469 signUpController.onSignUpFormSubmitted(_this, signUpData); 4470 }); 4471 4472 if(mSignUpDialog != null) { 4473 new ViewWrapper(mSignUpDialog).onShow(); 4474 const cancelButton = NNLUtils.removeEventListeners(mSignUpDialog.querySelector('.nnl-dialog-close')); 4475 cancelButton.addEventListener('click', function() { 4476 NNLUtils.showModal(mSignUpDialog, false); 4477 signUpController.onCancel(); 4478 }); 4479 NNLUtils.showModal(mSignUpDialog, true); 4480 } else { 4481 new ViewWrapper(mView).onShow(); 4482 mView.style.display = 'block'; 4483 } 4484 } 4485 4486 /** 4487 * Displays a modal dialog asking the user's confirmation for registering the specified method. 4488 * @param {SignUpMethod} method The type of the method that the user needs to confirm to register. 4489 */ 4490 this.confirmMethodRegistration = function(method) { 4491 NNLUtils.validateArgument('method', method, 'string', null, false); 4492 4493 mIsReady.then(function() { 4494 mConfirmDialog.classList.remove('nnl-sign-up-method-passkey', 'nnl-sign-up-method-security-key', 'nnl-sign-up-method-photo-id');
4495 switch(method) { 4496 case SignUpMethod.PASSKEY: 4497 mConfirmDialog.classList.add('nnl-sign-up-method-passkey'); 4498 break; 4499 case SignUpMethod.SECURITY_KEY: 4500 mConfirmDialog.classList.add('nnl-sign-up-method-security-key'); 4501 break; 4502 case SignUpMethod.PHOTO_ID: 4503 mConfirmDialog.classList.add('nnl-sign-up-method-photo-id'); 4504 break; 4505 } 4506 4507 const confirmButton = NNLUtils.removeEventListeners(mConfirmDialog.querySelector('#nnl-sign-up-method-confirm')); 4508 confirmButton.addEventListener('click', function() { 4509 NNLUtils.showModal(mConfirmDialog, false); 4510 signUpController.onConfirmationResponse(_this, method, true); 4511 }); 4512 4513 const cancelButton = NNLUtils.removeEventListeners(mConfirmDialog.querySelector('#nnl-sign-up-method-cancel')); 4514 cancelButton.addEventListener('click', function() { 4515 NNLUtils.showModal(mConfirmDialog, false); 4516 signUpController.onConfirmationResponse(_this, method, false); 4517 }); 4518 4519 new ViewWrapper(mConfirmDialog.querySelector('.nnl-dialog')).onShow(); 4520 NNLUtils.showModal(mConfirmDialog, true); 4521 }); 4522 } 4523 4524 function hideUI() { 4525 // If modal dialog is displayed, hide it. 4526 if(mSignUpDialog != null) { 4527 NNLUtils.showModal(mSignUpDialog, false); 4528 } else { 4529 // Otherwise disable the container. 4530 mView.style.pointerEvents = 'none'; 4531 } 4532 } 4533 4534 function hideErrors(section) { 4535 const classes = section.classList.values(); 4536 let it = classes.next(); 4537 while(!it.done) { 4538 if(it.value.indexOf('nnl-sign-up-error-') === 0) { 4539 section.classList.remove(it.value); 4540 } 4541 it = classes.next(); 4542 } 4543 } 4544 4545 function showError(section, errorType, message) { 4546 if(message == null || message === '') { 4547 section.classList.add('nnl-sign-up-error-' + errorType); 4548 } else { 4549 section.classList.add('nnl-sign-up-error-custom'); 4550 section.querySelector('.nnl-sign-up-error-custom').innerText = message; 4551 } 4552 } 4553} 4554 4555/** 4556 * This interface defines a method for creating {@link ISignUpProcessor} instance. 4557 * The RP application must implement this interface in order for the sign up functionality to work. 4558 * @interface ISignUpProcessorFactory 4559 */ 4560 4561/** 4562 * The implementation should create and return {@link ISignUpProcessor} instance. 4563 * @method ISignUpProcessorFactory#createSignUpProcessor 4564 * @param {Extras} [signUpOpts] Extra options. 4565 * @returns {ISignUpProcessor} The {@link ISignUpProcessor} instance. 4566 */ 4567 4568/** 4569 * This interface defines methods for starting and competing the sign up process by the RP application. 4570 * The RP application must implement this interface in order for the sign up functionality to work. 4571 * @interface ISignUpProcessor 4572 */ 4573 4574/** 4575 * This method will be called when the user submits the sign up form. The `signUpData` parameter will 4576 * contain the sign up form fields' values. 4577 * The implementation should make a call to the RP backend to start account creation. 4578 * The backend should create an "unverified" account and a session key for that account that contains 4579 * a custom claim "sign_up" with the value "true" in the JWT payload. The implementation should return a Promise that should 4580 * be resolved with a {@link SignUpSession|SignUpSession} instance containing a userName and sessionData object containing that session key. 4581 * In case of errors the implementation should reject the Promise with one of the {@link SignUpError} values and optional error message. 4582 * @method ISignUpProcessor#startSignUp 4583 * @param {Object} signUpData The data collected from the sign up form. 4584 * @param {string} [signUpData.fullName] The value of the "Full Name" field from the sign up form if provided. 4585 * @param {string} [signUpData.userName] The value of the "Username" field from the sign up form if provided. 4586 * @param {string} [signUpData.emailAddress] The value of the "Email Address" field from the sign up form if provided. 4587 * @param {string} [signUpData.phoneNumber] The value of the "Phone Number" field from the sign up form if provided. 4588 * @returns {Promise} A Promise that should be resolved with a {@link SignUpSession|SignUpSession} instance or rejected 4589 * with one of the {@link SignUpError} values. 4590 */ 4591 4592/** 4593 * This method will be called after the requested methods have been registered. The implementation 4594 * should make a call to the RP backend to complete the account creation. The RP backend should 4595 * verify that all required methods were registered by making `LIST_METHODS` and `LIST_REG` requests to API Server.
4596 * The implementation should return a Promise that should be resolved with a {@link SignUpSession|SignUpSession} 4597 * instance containing the `userName` and a new `sessionData` without the "sign_up" claim (as if the user has passed 4598 * an authentication). The `userName` and `sessionData` will be returned to the RP application to sign the user in. 4599 * In case of an error the implementation should reject the Promise with one of the {@link SignUpError} values. 4600 * @method ISignUpProcessor#finishSignUp 4601 * @param {SignUpSession} session Sign up session containing the `userName` and `sessionData` used during sign up, 4602 * which may be updated during the process. 4603 * @returns {Promise} A Promise that should be resolved with a {@link SignUpSession|SignUpSession} instance or rejected 4604 * with one of the {@link SignUpError} values. 4605 */ 4606 4607/** 4608 * @class 4609 * @classdesc Represents a session associated with a sign-up process. 4610 * @param {string} userName The user's username associated with the session. 4611 * @param {SessionData} sessionData The session data containing the session key. 4612 */ 4613const SignUpSession = function(userName, sessionData) { 4614 this.userName = userName; 4615 this.sessionData = sessionData; 4616} 4617 4618/** 4619 * @class 4620 * @classdesc A singleton factory class to store and return {@link ISignUpProcessorFactory} implementation. 4621 * @hideconstructor 4622 */ 4623const SignUpProcessorFactory = new (function() { 4624 let mInstance = null; 4625 4626 /** 4627 * Stores the specified {@link ISignUpProcessorFactory} instance. 4628 * @alias SignUpProcessorFactory.setInstance 4629 */ 4630 this.setInstance = function(instance) { 4631 NNLUtils.validateArgument('instance', instance, 'object', null, false); 4632 mInstance = instance; 4633 } 4634 4635 /** 4636 * Returns the stored {@link ISignUpProcessorFactory} instance or null if setInstance 4637 * method was not called. The RP application must call the setInstance method to specify 4638 * the implementation, otherwise the sign up option will not be displayed. 4639 * @alias SignUpProcessorFactory.getInstance 4640 */ 4641 this.getInstance = function() { 4642 return mInstance; 4643 } 4644 4645 Object.freeze(this); 4646})(); 4647 4648/** 4649 * @class 4650 * @classdesc Executes the sign up process. 4651 * @param {object} appSdkConfig An object containing endpoints for registration operations. 4652 * @param {string} appSdkConfig.regEndpoint Endpoint for registration operations. 4653 * @param {object} signUpOpts Options for sign up operation. 4654 * @param {boolean} signUpOpts.enabled Flag indicating whether the sign up should be enabled or not. 4655 * @param {string} [signUpOpts.fullName] If specified, accepted values are "required" and "optional". 4656 * The sign up form will display the "Full Name" field and if the value is "required", 4657 * the input will not be allowed to be empty when the form is submitted. 4658 * @param {string} [signUpOpts.userName] If specified, accepted values are "required" and "optional". 4659 * The sign up form will display the "Username" field and if the value is "required", 4660 * the input will not be allowed to be empty when the form is submitted. 4661 * @param {string} [signUpOpts.emailAddress] If specified, accepted values are "required" and "optional". 4662 * The sign up form will display the "Email Address" field and if the value is "required", 4663 * the input will not be allowed to be empty when the form is submitted and the sign up process will fail 4664 * if the registration of the "Email OTP" method is canceled or has failed. 4665 * @param {string} [signUpOpts.phoneNumber] If specified, accepted values are "required" and "optional". 4666 * The sign up form will display the "Phone Number" field and if the value is "required", 4667 * the input will not be allowed to be empty when the form is submitted and the sign up process will fail 4668 * if the registration of the "SMS OTP" method is canceled or has failed. 4669 * @param {boolean} [signUpOpts.fido] When the value is `true`, the sign up process will ask the user to create a 4670 * passkey or register a security key if supported by the platform. 4671 * @param {boolean} [signUpOpts.photoID] When the value is `true`, the sign up process will ask the user to register 4672 * a ID Document for verification. 4673 */ 4674const SignUpController = function(appSdkConfig, signUpOpts) { 4675 NNLUtils.validateArgument('appSdkConfig', appSdkConfig, 'object', null, false); 4676 NNLUtils.validateArgument('signUpOpts', signUpOpts, 'object', null, false); 4677 4678 const mSignUpOpts = NNLUtils.cloneObject(signUpOpts); 4679 4680 const mSignUpProcessor = SignUpProcessorFactory.getInstance().createSignUpProcessor(mSignUpOpts); 4681 4682 // Function that will either resolve or reject the Promise returned by the enable() method. 4683 let returnResult; 4684 4685 let mSignUpData = null; 4686 let mSignUpSession = null; 4687 let mNextMethodIndex = 0; 4688 let mOnConfirmReg; 4689 4690 const mRegistrationOrder = [ 4691 AdaptiveType.FIDO, 4692 AdaptiveType.EMAIL, 4693 AdaptiveType.SMS, 4694 AdaptiveType.PHOTO_ID 4695 ]; 4696 4697 // Create and initialize SDK instances. 4698 const mAdaptiveCore = new AdaptiveCore(); 4699 mAdaptiveCore.regEndpoint = appSdkConfig.regEndpoint; 4700 4701 const mAppSdk = new AppSdk(); 4702 mAppSdk.regEndpoint = appSdkConfig.regEndpoint; 4703 mAppSdk.mode = AppSdk.MODE_NATIVE; 4704 4705 const mIsReady = Promise.all([ 4706 NNLUtils.libphonenumber.load(),
4707 mAdaptiveCore.init(), 4708 mAppSdk.init(AppSdk.PROTOCOl_FIDO2) 4709 ]); 4710 4711 /** 4712 * Returns a Promise that will be resolved or rejected with AdaptiveResult. 4713 * The "adaptiveOp" field of that AdaptiveResult will be set to {@link AdaptiveOperation|AdaptiveOperation.SIGN_UP}. 4714 * When the Promise is resolved, the AdaptiveResult will contain the `sessionData` and `userName`. 4715 * The Promise will be rejected only when the user cancels the sign up form. The AdaptiveResult in this case will 4716 * contain only the `outcome` field equal to {@link Outcome|CANCELED}. 4717 */ 4718 this.enable = function() { 4719 return new Promise(function(resolve, reject) { 4720 returnResult = function(result) { 4721 if(result.outcome == null) { 4722 resolve(result); 4723 } else { 4724 reject(result); 4725 } 4726 } 4727 }); 4728 } 4729 4730 /** 4731 * Starts the sign up process. 4732 * @param {ISignUpLiveData} liveData The ISignUpLiveData instance to notify about UI updates. 4733 */ 4734 this.onRefresh = function(liveData) { 4735 mIsReady.then(function() { 4736 ProgressIndicator.getInstance().suspend(); 4737 liveData.getSignUpData(mSignUpOpts, null, null); 4738 }); 4739 } 4740 4741 /** 4742 * Should be called by the implementation of {@link ISignUpLiveData#getSignUpData|ISignUpLiveData.getSignUpData}. 4743 * Validates the `signUpData` and calls {@link ISignUpProcessor#startSignUp|startSignUp}. 4744 * @param {ISignUpLiveData} liveData The ISignUpLiveData instance to notify about UI updates. 4745 * @param {object} signUpData An object containing values from the sign up form. 4746 * @param {string} [signUpData.fullName] The value of the "Full Name" field. 4747 * @param {string} [signUpData.userName] The value of the "Username" field. 4748 * @param {string} [signUpData.emailAddress] The value of the "Email Address" field. 4749 * @param {string} [signUpData.phoneNumber] The value of the "Phone Number" field. 4750 */ 4751 this.onSignUpFormSubmitted = function(liveData, signUpData) { 4752 NNLUtils.validateArgument('signUpData', signUpData, 'object', null, false); 4753 4754 if(mSignUpOpts.fullName === 'required' && signUpData.fullName === '') { 4755 liveData.getSignUpData(mSignUpOpts, SignUpError.FULL_NAME_REQUIRED, null); 4756 return; 4757 } 4758 4759 if(mSignUpOpts.userName === 'required' && signUpData.userName === '') { 4760 liveData.getSignUpData(mSignUpOpts, SignUpError.USERNAME_REQUIRED, null); 4761 return; 4762 } 4763 4764 if(mSignUpOpts.emailAddress === 'required' && signUpData.emailAddress === '') { 4765 liveData.getSignUpData(mSignUpOpts, SignUpError.EMAIL_ADDRESS_REQUIRD, null); 4766 return; 4767 } 4768 4769 if(mSignUpOpts.phoneNumber === 'required' && signUpData.phoneNumber === '') { 4770 liveData.getSignUpData(mSignUpOpts, SignUpError.PHONE_NUMBER_REQUIRED, null); 4771 return; 4772 } 4773 4774 if(signUpData.emailAddress !== '' && !NNLUtils.isEmail(signUpData.emailAddress)) { 4775 liveData.getSignUpData(mSignUpOpts, SignUpError.INVALID_EMAIL_ADDRESS, null); 4776 return; 4777 } 4778 4779 if(signUpData.phoneNumber !== '') { 4780 const phoneNumber = NNLUtils.libphonenumber.parse(signUpData.phoneNumber, mSignUpOpts); 4781 if(phoneNumber == null) { 4782 liveData.getSignUpData(mSignUpOpts, SignUpError.INVALID_PHONE_NUMBER, null); 4783 return; 4784 } 4785 signUpData.phoneNumber = phoneNumber; 4786 } 4787 4788 ProgressIndicator.getInstance().resume(); 4789 mSignUpData = signUpData; 4790 4791 mSignUpProcessor.startSignUp(mSignUpData) 4792 .then(function(signUpSession) { 4793 mSignUpSession = NNLUtils.cloneObject(signUpSession); 4794 mNextMethodIndex = 0; 4795 return UserDataCache.getInstance().setActiveUser(mSignUpSession.userName); 4796 }) 4797 .then(function() { 4798 registerNextMethod(liveData); 4799 }) 4800 .catch(function(error) { 4801 onError(liveData, error, null); 4802 }); 4803 4804 } 4805 4806 /** 4807 * Should be called by the implementation of 4808 * {@link ISignUpLiveData#confirmMethodRegistration|ISignUpLiveData.confirmMethodRegistration}. 4809 * Starts or skips the registration of the specified `method` based on the value of the `confirmed` flag. 4810 * @param {ISignUpLiveData} liveData The ISignUpLiveData instance to notify about UI updates. 4811 * @param {SignUpMethod} method The method that the user was asked to register. 4812 * @param {boolean} confirmed Flag indicating the user's choice. 4813 */ 4814 this.onConfirmationResponse = function(liveData, method, confirmed) { 4815 NNLUtils.validateArgument('method', method, 'string', null, false); 4816 NNLUtils.validateArgument('confirmed', confirmed, 'boolean', null, false); 4817 4818 mOnConfirmReg(method, confirmed); 4819 } 4820 4821 /** 4822 * Cancels the sign up process. 4823 */ 4824 this.onCancel = function() { 4825 ProgressIndicator.getInstance().resume(); 4826 returnResult({ 4827 outcome: Outcome.CANCELED 4828 }); 4829 } 4830 4831 // Completes the sign up process. 4832 function finishSignUp(liveData) { 4833 mSignUpProcessor.finishSignUp(mSignUpSession) 4834 .then(function(signUpSession) { 4835 const adaptiveResult = { 4836 userName: signUpSession.userName, 4837 sessionData: signUpSession.sessionData, 4838 adaptiveOp: AdaptiveOperation.SIGN_UP 4839 } 4840 4841 returnResult(adaptiveResult); 4842 }) 4843 .catch(function(error) { 4844 onError(liveData, error, null); 4845 }); 4846 } 4847 4848 // Calls liveData to show the error on the sign up form. 4849 function onError(liveData, error, message) { 4850 UserDataCache.getInstance().setActiveUser(null); 4851 ProgressIndicator.getInstance().suspend(); 4852 liveData.getSignUpData(mSignUpOpts, error, message); 4853 } 4854 4855 // Calls ISignUpLiveData.confirmMethodRegistration and returns a Promise that 4856 // will be resolved or rejected based on the user's choice. 4857 function confirmRegistration(liveData, signUpMethod) { 4858 return new Promise(function(resolve, reject) { 4859 mOnConfirmReg = function(method, confirmed) { 4860 ProgressIndicator.getInstance().resume(); 4861 if(confirmed) { 4862 resolve(method); 4863 } else { 4864 reject({outcome: Outcome.CANCELED}); 4865 } 4866 } 4867 4868 ProgressIndicator.getInstance().suspend(); 4869 liveData.confirmMethodRegistration(signUpMethod); 4870 }); 4871 } 4872 4873 // Starts the registration of the next method in the queue. Calls SignUpProcessor.finishSignUp() 4874 // when the end of the queue is reached. 4875 function registerNextMethod(liveData) { 4876 // No methods left to register, complete the process. 4877 if(mNextMethodIndex === mRegistrationOrder.length) { 4878 finishSignUp(liveData); 4879 return; 4880 } 4881 4882 // Get the next method type to register. 4883 const type = mRegistrationOrder[mNextMethodIndex]; 4884 mNextMethodIndex++; 4885 4886 Promise.resolve() 4887 .then(function() { 4888 if(type === AdaptiveType.FIDO && mSignUpOpts.fido) { 4889 return checkAndRegisterMethod(liveData, AdaptiveType.FIDO); 4890 } 4891 4892 if(type === AdaptiveType.EMAIL && mSignUpData.emailAddress != null && mSignUpData.emailAddress !== '') { 4893 return checkAndRegisterMethod(liveData, AdaptiveType.EMAIL, mSignUpData.emailAddress); 4894 } 4895 4896 if(type === AdaptiveType.SMS && mSignUpData.phoneNumber != null && mSignUpData.phoneNumber !== '') { 4897 return checkAndRegisterMethod(liveData, AdaptiveType.SMS, mSignUpData.phoneNumber); 4898 } 4899 4900 if(type === AdaptiveType.PHOTO_ID && mSignUpOpts.photoID) { 4901 return checkAndRegisterMethod(liveData, AdaptiveType.PHOTO_ID); 4902 } 4903 }) 4904 .then(function() { 4905 registerNextMethod(liveData); 4906 }) 4907 .catch(function(result) { 4908 const isRequiredMethod = (type === AdaptiveType.EMAIL && mSignUpOpts.emailAddress === 'required' || 4909 type === AdaptiveType.SMS && mSignUpOpts.phoneNumber === 'required');
4910 const message = Outcome.getSignUpErrorMessage(result.outcome); 4911 4912 // If sign up cannot continue (required method has failed, session has expired or connection is lost) 4913 // show the sign up form again with the corresponding error message. 4914 if(isRequiredMethod || result.outcome === Outcome.INVALID_SESSION || result.outcome === Outcome.CONNECTION_ERROR) { 4915 // If a required method is canceled, show the sign up form without error message. 4916 const error = result.outcome === Outcome.CANCELED ? null : SignUpError.OTHER; 4917 onError(liveData, error, message); 4918 } else { 4919 // Otherwise show an error message and register next method. 4920 if(message != null && message !== '') { 4921 NNLToastMessage.show(message); 4922 } 4923 registerNextMethod(liveData); 4924 } 4925 }); 4926 } 4927 4928 function checkAndRegisterMethod(liveData, type, identifier) { 4929 // If method is not supported, resolve to skip it. 4930 if(!MethodUIFactory.getInstance().isSupported(type)) { 4931 return Promise.resolve(); 4932 } 4933 4934 if(type === AdaptiveType.FIDO) { 4935 // For FIDO method, first do a check policy to find out if passkey or security key are supported. 4936 return getFidoMethodType() 4937 .then(function(signUpMethod) { 4938 return confirmRegistration(liveData, signUpMethod); 4939 }) 4940 .then(registerFidoMethod); 4941 } 4942 4943 // For adaptive methods, first check whether the method is already registered or not. 4944 return isMethodRegistered(type, identifier) 4945 .then(function(isRegistered) { 4946 if(!isRegistered) { 4947 return registerMethod(liveData, type, identifier); 4948 } 4949 }); 4950 } 4951 4952 function registerMethod(liveData, type, identifier) { 4953 const method = { 4954 type: type, 4955 name: AdaptiveType.names[type] 4956 } 4957 4958 const extras = NNLUtils.cloneObject(mSignUpOpts); 4959 4960 return Promise.resolve() 4961 .then(function() { 4962 // If identifier is not provided, then we are registering a Photo ID method, 4963 // which should be confirmed first. 4964 if(identifier == null) { 4965 return confirmRegistration(liveData, SignUpMethod.PHOTO_ID); 4966 } 4967 4968 // For OTP methods put provided identifier into extras. 4969 extras[AppSdk.EXTRA_KEY_OTP_IDENTIFIER] = identifier; 4970 }) 4971 .then(function() { 4972 const job = mAdaptiveCore.startRegister(mSignUpSession.sessionData, extras); 4973 return job.processMethod([method], OperationType.REGISTRATION, extras); 4974 }) 4975 .then(function(result) { 4976 updateSessionData(result.sessionData); 4977 return result; 4978 }); 4979 } 4980 4981 let mRegisteredMethods = null; 4982 function isMethodRegistered(type, identifier) { 4983 return Promise.resolve() 4984 .then(function() { 4985 // Use the cached list if it exists. 4986 if(mRegisteredMethods != null) { 4987 return mRegisteredMethods; 4988 } 4989 4990 // Otherwise get list from the server. 4991 return mAdaptiveCore.getMethods(mSignUpSession.sessionData); 4992 }) 4993 .then(function(methods) { 4994 mRegisteredMethods = methods; 4995 // Check if the specified method is in the list of registered methods. 4996 return mRegisteredMethods.some(function(method) { 4997 // If the identifier is provided, is should match too. Otherwise only type is checked. 4998 return method.type === type && (identifier == null || method.data != null && method.data.identifier === identifier); 4999 }); 5000 }) 5001 .catch(function(result) { 5002 if(result.outcome === Outcome.SERVER_USER_NOT_FOUND) { 5003 // If the user does not have any registration, return false. 5004 mRegisteredMethods = []; 5005 return false; 5006 } 5007 5008 // Report other errors. 5009 return Promise.reject(result); 5010 }); 5011 } 5012 5013 function registerFidoMethod(signUpMethod) { 5014 const extras = NNLUtils.cloneObject(mSignUpOpts); 5015 // Delete the flag (required/optional) from extras to not use it as user display name. 5016 delete extras.userName; 5017 5018 // Create a filter extension based on the method type. 5019 extras.extensions = []; 5020 var extData = { 5021 authenticatorAttachment: signUpMethod === SignUpMethod.PASSKEY ? 'platform' : 'cross-platform' 5022 } 5023 5024 var filterExtension = { 5025 id: 'noknok.webauthnAuthenticatorFilter', 5026 data: JSON.stringify(extData), 5027 fail_if_unknown: false 5028 } 5029 5030 extras.extensions.push(filterExtension); 5031 5032 // Register the FIDO method. 5033 return mAppSdk.register(mSignUpSession.sessionData, extras) 5034 .then(function(result) { 5035 updateSessionData(result.sessionData); 5036 return result; 5037 }); 5038 } 5039 5040 function getFidoMethodType() { 5041 return mAppSdk.checkRegPossible(mSignUpSession.sessionData, mSignUpOpts) 5042 .then(function(result) { 5043 // If FIDO is supported (promise is resolved) but the SDK did not report which methods are possible, try to register a passkey. 5044 if(result.additionalInfo == null || result.additionalInfo.authenticators == null || result.additionalInfo.authenticators.length === 0) { 5045 return SignUpMethod.PASSKEY; 5046 } 5047 5048 let passkeyMethod = null; 5049 // Wrap platform authenticator in FidoMethod to get the "isRegPossible" flag value.
5050 result.additionalInfo.authenticators.forEach(function(availableAuthenticator) { 5051 if(availableAuthenticator.authenticatorAttachment === 'platform') { 5052 passkeyMethod = new FidoMethod(availableAuthenticator); 5053 } 5054 }); 5055 5056 if(passkeyMethod != null) { 5057 if(passkeyMethod.isRegPossible) { 5058 // Passkey is supported and is not registered yet. 5059 return SignUpMethod.PASSKEY; 5060 } 5061 5062 // Passkey is already registered, return CANCELED to skip it. 5063 return Promise.reject({outcome: Outcome.CANCELED}); 5064 } 5065 5066 // Passkey is not supported. 5067 return SignUpMethod.SECURITY_KEY; 5068 }) 5069 .catch(function(result) { 5070 // If FIDO is not supported, return CANCELED to skip the registration. 5071 if(result.outcome === Outcome.NO_MATCH) { 5072 return Promise.reject({outcome: Outcome.CANCELED}); 5073 } 5074 5075 // Report other errors. 5076 return Promise.reject(result); 5077 }); 5078 } 5079 5080 function updateSessionData(newSessionData) { 5081 if(newSessionData != null) { 5082 for(let prop in newSessionData) { 5083 mSignUpSession.sessionData[prop] = newSessionData[prop]; 5084 } 5085 } 5086 } 5087 5088 Object.freeze(this); 5089} 5090 5091/** 5092 * This interface defines a method for providing the current time in milliseconds. 5093 * @interface ITimeProvider 5094 */ 5095 5096/** 5097 * This method implementation should return the current time in milliseconds. 5098 * @method ITimeProvider#getCurrentTime 5099 * @returns {Promise} A Promise that resolves with the value if the current time in milliseconds. 5100 */ 5101 5102/** 5103 * The default implementation of {@link ITimeProvider}. 5104 * @class 5105 * @implements ITimeProvider 5106 */ 5107const DefaultTimeProvider = function() { 5108 /** 5109 * Returns the current time by calling JavaScript `new Date().getTime()` API. 5110 * @returns {Promise} A Promise that resolves with the current time in milliseconds since midnight, January 1, 1970 UTC. 5111 */ 5112 this.getCurrentTime = function() { 5113 return Promise.resolve(new Date().getTime()); 5114 } 5115 5116 Object.freeze(this); 5117} 5118 5119/** 5120 * @class 5121 * @classdesc A singleton class to store and return {@link ITimeProvider} implementation. 5122 * @hideconstructor 5123 */ 5124const TimeProviderManager = new (function() { 5125 let mTimeProvider = null; 5126 5127 /** 5128 * Sets the {@link ITimeProvider} instance. 5129 * @param {ITimeProvider} timeProvider The new ITimeProvider instance. 5130 */ 5131 this.setInstance = function(timeProvider) { 5132 mTimeProvider = timeProvider; 5133 } 5134 5135 /** 5136 * Returns {@link ITimeProvider} instance. 5137 * If no instance was set using the {@link TimeProviderManager.setInstance|setInstance} method, creates and returns {@link DefaultTimeProvider} instance. 5138 * @returns {ITimeProvider} The current ITimeProvider instance. 5139 */ 5140 this.getInstance = function() { 5141 if(mTimeProvider == null) { 5142 mTimeProvider = new DefaultTimeProvider(); 5143 } 5144 5145 return mTimeProvider; 5146 } 5147 5148 Object.freeze(this); 5149})(); 5150
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.