1function fidoWrapper() { 2 3 const ENROLL_INIT_URL = 'FidoEnroll/init'; 4 const ENROLL_FINALIZE_URL = 'FidoEnroll/finalize'; 5 6 const AUTH_INIT_URL = 'FidoAuth/init'; 7 const AUTH_FINALIZE_URL = 'FidoAuth/finalize' 8 9 let abortController = new AbortController(); 10 let timeoutEvent; 11 12 const TIMEOUT_OFFSET = 5000; 13 14 const ERR_GENERAL = -1 15 const ERR_CANCELLED = -10; 16 const ERR_ABORT = -11; 17 const ERR_INVALID_STATE = -12; 18 const ERR_NOT_SUPPORTED = -13; 19 const ERR_AUTOFILL_NOT_AVAILABLE = -14; 20 21 return { 22 ERR_CANCELLED: ERR_CANCELLED, 23 // The same error is used for both timeouts and regular aborts (initiating a new flow while the old one is in progress) 24 ERR_TIMEOUT: ERR_ABORT, 25 ERR_ABORT: ERR_ABORT, 26 ERR_INVALID_STATE: ERR_INVALID_STATE, 27 ERR_NOT_SUPPORTED: ERR_NOT_SUPPORTED, 28 ERR_AUTOFILL_NOT_AVAILABLE: ERR_AUTOFILL_NOT_AVAILABLE, 29 30 /** 31 * Enroll an authenticator (passkey) 32 * 33 * @param callback called with {errorCode:iapiERR_OK} when enrollment was successful, or {errorCode: <error>} when something went wrong. 34 * if errorCode < 0 then the error originated from the frontend. 35 * @param authenticatorName name for the authenticator 36 * @param username if null then will attempt to fall back to using sessionStorage or cookies. 37 * @param token sessionToken. if null then will attempt to fall back to using sessionStorage or cookies. 38 * @returns {Promise<Object>} resolved with the same data as callback, except promise is rejected if there was an error. 39 */ 40 enroll: async function (callback, authenticatorName, username, token) { 41 try { 42 ensureConf(); 43 44 abortController.abort({ 45 errorCode: ERR_ABORT, 46 message: "New enroll initiated" 47 }) 48 abortController = new AbortController(); 49 50 const enrollCredentials = await initEnroll(username, token); 51 const credentialCreateResult = await webAuthn.create(enrollCredentials); 52 53 const enrollResponse = await finalizeEnroll(credentialCreateResult, authenticatorName, username, token); 54 55 call(callback, enrollResponse); 56 return enrollResponse; 57 } catch (e) { 58 const pasError = mapError(e); 59 call(callback, pasError); 60 throw pasError; 61 } finally { 62 clearTimeout(timeoutEvent) 63 timeoutEvent = null; 64 } 65 }, 66 67 /** 68 * Authentication flow 69 * 70 * Use when you want to enable autofill UI 71 * Only call this on a login screen, when you have a "username" text input field on screen with autocomplete="username webauthn" params 72 * Availability can be checked first with isAutofillSupported() 73 * 74 * @param callback called with {errorCode:iapiERR_OK} when authentication was successful, or {errorCode: <error>} when something went wrong. 75 * if errorCode < 0 then the error originated from the frontend. 76 * @returns {Promise<Object>} resolved with the same data as callback, except promise is rejected if there was an error. 77 */ 78 authenticateAutofillUI: async function (callback) { 79 try { 80 ensureConf(); 81 await ensureConditionalMediationAvailable(); 82 } catch (e) { 83 const pasError = mapError(e) 84 call(callback, pasError) 85 throw pasError; 86 } 87 88 return await continueAuth(callback, true); 89 }, 90 91 /** 92 * Authentication flow 93 * 94 * for use on a "Login with Passkey" button. 95 * 96 * @param callback called with {errorCode:iapiERR_OK} when authentication was successful, or {errorCode: <error>} when something went wrong. 97 * if errorCode < 0 then the error originated from the frontend. 98 * @returns {Promise<Object>} resolved with the same data as callback, except promise is rejected if there was an error. 99 */ 100 authenticate: async function (callback) { 101 try { 102 ensureConf(); 103 } catch (e) { 104 const pasError = mapError(e) 105 call(callback, pasError) 106 throw pasError; 107 } 108 109 return await continueAuth(callback, false); 110 }, 111 112 /** 113 * Check if browser supports FIDO2/Passkey authentication and the feature has been enabled in the casino conf. 114 * 115 * @returns {boolean} 116 */ 117 isSupported: function () { 118 try { 119 ensureConf(); 120 return true; 121 } catch (e) { 122 return false;
123 } 124 }, 125 126 /** 127 * Check if the FIDO2/Passkey login feature has been enabled in the casino conf. 128 * 129 * @returns {boolean} 130 */ 131 isConfigured: function () { 132 try { 133 ensureEnabled(); 134 return true; 135 } catch (e) { 136 return false; 137 } 138 }, 139 140 /** 141 * Check if autofill auth flow is supported by the browser, and it is possible to show to the user a passkey login prompt. 142 * @returns {Promise<boolean>} 143 */ 144 isAutofillSupported: async function () { 145 try { 146 ensureConf(); 147 await ensureConditionalMediationAvailable(); 148 return true; 149 } catch (e) { 150 return false; 151 } 152 }, 153 154 /** 155 * Abort the currently pending user interaction. 156 */ 157 abortCurrent() { 158 abortController.abort({ 159 errorCode: ERR_ABORT, 160 message: "Request aborted." 161 }) 162 } 163 } 164 165 async function continueAuth(callback, conditionalMediation) { 166 abortController.abort({ 167 errorCode: ERR_ABORT, 168 message: conditionalMediation ? "New autofill auth initiated" : "New auth initiated" 169 }) 170 171 abortController = new AbortController(); 172 try { 173 const authCredentials = await initAuthFetch(conditionalMediation); 174 const credentialGetResult = await webAuthn.get(authCredentials); 175 const authResponse = await finalizeAuth(credentialGetResult); 176 177 call(callback, authResponse); 178 return authResponse; 179 } catch (e) { 180 const pasError = mapError(e); 181 call(callback, pasError) 182 throw pasError; 183 } finally { 184 clearTimeout(timeoutEvent) 185 timeoutEvent = null; 186 } 187 } 188 189 async function initAuthFetch(conditionalMediation) { 190 191 const postParams = {casinoname: iapiConf['casinoname']} 192 193 const initAuthResponse = await fetchRequest(AUTH_INIT_URL, postParams) 194 195 if (initAuthResponse.errorCode !== iapiERR_OK || !initAuthResponse.credentialRequestOptions) { 196 throw initAuthResponse; 197 } 198 199 const requestOptions = JSON.parse(initAuthResponse.credentialRequestOptions); 200 201 setTimeoutEvent(requestOptions.timeout) 202 203 let requestJSON = { 204 publicKey: requestOptions, 205 signal: abortController.signal 206 }; 207 208 if (conditionalMediation) { 209 requestJSON.mediation = 'conditional' 210 } 211 212 return webAuthn.getRequestFromJSON(requestJSON); 213 } 214 215 216 async function finalizeAuth(result) { 217 let credentialGetResultJSON = webAuthn.getResponseToJSON(result); 218 219 if (!credentialGetResultJSON.response.userHandle) { 220 throw { 221 errorCode: ERR_NOT_SUPPORTED, 222 errorText: "Operation not supported by this browser." 223 } 224 } 225 credentialGetResultJSON.userAgent = navigator.userAgent; 226 227 228 const postParams = { 229 "casinoname": iapiConf['casinoname'], 230 "credentials": JSON.stringify(credentialGetResultJSON) 231 } 232 233 const finalizeResponse = await fetchRequest(AUTH_FINALIZE_URL, postParams); 234 235 if (finalizeResponse.errorCode !== iapiERR_OK) { 236 throw finalizeResponse; 237 } 238 return finalizeResponse 239 } 240 241 242// 243 244 async function initEnroll(username, token) { 245 let params = {casinoname: iapiConf["casinoname"]} 246 247 if (username && token) { 248 params["username"] = username 249 params["token"] = token 250 } else { 251 const username = getFromStorage("username"); 252 const token = getFromStorage("token"); 253 254 if (username) { 255 params["username"] = username; 256 } 257 if (token) { 258 params["token"] = token; 259 } 260 } 261 262 const initEnrollResponse = await fetchRequest(ENROLL_INIT_URL, params) 263 if (initEnrollResponse.errorCode !== iapiERR_OK || !initEnrollResponse.credentialCreateOptions) { 264 throw initEnrollResponse; 265 } 266 267 const creationOptions = JSON.parse(initEnrollResponse.credentialCreateOptions); 268 269 setTimeoutEvent(creationOptions.timeout) 270 271 return webAuthn.createRequestFromJSON({ 272 publicKey: creationOptions, 273 signal: abortController.signal 274 }); 275 } 276 277 async function finalizeEnroll(credentialCreateResult, authenticatorName, username, token) { 278 let credentialCreateResultJSON = webAuthn.createResponseToJSON(credentialCreateResult); 279 credentialCreateResultJSON.userAgent = navigator.userAgent; 280 credentialCreateResultJSON.userFriendlyName = authenticatorName; 281 282 let params = { 283 "casinoname": iapiConf['casinoname'], 284 "credentials": JSON.stringify(credentialCreateResultJSON) 285 } 286 287 if (username && token) { 288 params["username"] = username 289 params["token"] = token 290 } else { 291 const username = getFromStorage("username"); 292 const token = getFromStorage("token"); 293 294 if (username) { 295 params["username"] = username; 296 } 297 if (token) { 298 params["token"] = token; 299 } 300 } 301 302 const finalizeResponse = await fetchRequest(ENROLL_FINALIZE_URL, params); 303 304 if (finalizeResponse.errorCode !== iapiERR_OK) { 305 throw finalizeResponse; 306 } 307 return finalizeResponse; 308 } 309 310 311 async function fetchRequest(credentialsUrl, params) { 312 const availableServers = iapiConf['loginServer'].split('|'); 313 const loginServer = availableServers[Math.floor(Math.random() * availableServers.length)]; 314
315 let url = getHttpProtocol() + '://' + loginServer + "/" + iapiAppendContextParameters(credentialsUrl) 316 317 if (iapiConf['errorLevel'] === '1' || iapiConf['errorLevel'] === 1) { 318 url += '&errorLevel=1'; 319 } 320 321 let bodyMap = new Map(); 322 for (let property in params) { 323 const encodedKey = encodeURIComponent(property); 324 const encodedValue = encodeURIComponent(params[property]); 325 bodyMap.set(encodedKey, encodedValue); 326 } 327 bodyMap.set("responseType", "json"); 328 bodyMap.set("sessionRef", getFromStorage("sessionRef")); 329 330 let body = "" 331 bodyMap.forEach((value, key) => { 332 body += key + "=" + value + "&"; 333 }); 334 if (body.endsWith('&')) { 335 body = body.substring(0, body.length - 1) 336 } 337 338 const response = await fetch(url, { 339 method: "POST", 340 headers: { 341 "Content-Type": "application/x-www-form-urlencoded" 342 }, 343 body: body 344 }) 345 346 return await response.json(); 347 } 348 349// 350 351 function getFromStorage(key) { 352 return sessionStorage.getItem(getSessionKey(iapiConf["casinoname"], key)) 353 } 354 355 function getSessionKey(casino, key) { 356 return "pas[" + casino + "][real][" + key + "]"; 357 } 358 359 function call(callback, param) { 360 if (callback && typeof callback === "function") { 361 callback(param) 362 } 363 } 364 365 // maps errors coming from frontend to be in the same format as PAS API errors. 366 function mapError(e) { 367 if (typeof iapiConf === 'object' && iapiConf["fidoDebug"] === '1') { 368 console.error(e) 369 } 370 371 if (e.errorCode >= 0) { 372 // 373 return e; 374 } 375 376 // map WebAuthn api errors to have an errorCode. 377 const errorMap = { 378 "NotAllowedError": ERR_CANCELLED, 379 "InvalidStateError": ERR_INVALID_STATE, 380 "AbortError": ERR_ABORT, 381 "NotSupportedError": ERR_NOT_SUPPORTED 382 }; 383 384 const errorCode = e.errorCode ?? (errorMap[e.name] || ERR_GENERAL); 385 const errorText = e.errorText ?? e.message ?? e; 386 let error = {errorCode: errorCode, errorText: errorText}; 387 if (e.name) error['name'] = e.name 388 389 return error 390 391 } 392 393 function setTimeoutEvent(timeoutMs) { 394 if (!timeoutMs || timeoutMs <= 0) { 395 return // 396 } 397 // 398 timeoutMs = timeoutMs <= TIMEOUT_OFFSET ? timeoutMs : timeoutMs - TIMEOUT_OFFSET 399 400 if (timeoutEvent) { 401 clearTimeout(timeoutEvent) 402 timeoutEvent = null; 403 } 404 405 timeoutEvent = setTimeout(() => { 406 abortController.abort({ 407 errorCode: ERR_ABORT, 408 message: "User did not complete the interaction in the allocated time." 409 }) 410 }, timeoutMs) 411 } 412 413 function ensureConf() { 414 ensureEnabled(); 415 ensureWebAuthnSupported(); 416 } 417 418 function ensureEnabled() { 419 if (typeof iapiConf === 'undefined' || !iapiConf) { 420 throw { 421 errorCode: ERR_GENERAL, 422 errorText: 'Not configured!. Please, do add integration.js' 423 }; 424 } 425 426 if (iapiConf['fidoAuth'] !== "1") { 427 throw { 428 errorCode: ERR_GENERAL, 429 errorText: 'FIDO2 authentication not enabled in casino' 430 }; 431 } 432 } 433 434 function ensureWebAuthnSupported() { 435 if (!navigator.credentials || !navigator.credentials.get || !navigator.credentials.get) { 436 throw { 437 errorCode: ERR_NOT_SUPPORTED, 438 errorText: 'No navigator.credentials. Is this a secure context?' 439 }; 440 } 441 442 if (!window.PublicKeyCredential) { 443 throw { 444 errorCode: ERR_NOT_SUPPORTED, 445 errorText: 'Browser does not support webAuthn' 446 }; 447 } 448 } 449 450 async function ensureConditionalMediationAvailable() { 451 if (window.PublicKeyCredential && typeof PublicKeyCredential.isConditionalMediationAvailable === "function") { 452 if (await PublicKeyCredential.isConditionalMediationAvailable()) { 453 return 454 } else { 455 throw { 456 errorCode: ERR_AUTOFILL_NOT_AVAILABLE, 457 errorText: "Browser is not reporting a discoverable resident key. Cannot initiate autofill flow." 458 }; 459 } 460 } 461 462 throw { 463 errorCode: ERR_AUTOFILL_NOT_AVAILABLE, 464 errorText: "Browser does not support autofill." 465 }; 466 } 467} 468 469self.fidoAuth = fidoWrapper(); 470 471 472// https://github.com/github/webauthn-json 473function webAuthnWrapper() { 474 function base64urlToBuffer(baseurl64String) { 475 const padding = "==".slice(0, (4 - baseurl64String.length % 4) % 4); 476 const base64String = baseurl64String.replace(/-/g, "+").replace(/_/g, "/") + padding; 477 const str = atob(base64String); 478 const buffer = new ArrayBuffer(str.length); 479 const byteView = new Uint8Array(buffer); 480 for (let i = 0; i < str.length; i++) { 481 byteView[i] = str.charCodeAt(i); 482 } 483 return buffer; 484 } 485 486 function bufferToBase64url(buffer) { 487 const byteView = new Uint8Array(buffer); 488 let str = "";
489 for (const charCode of byteView) { 490 str += String.fromCharCode(charCode); 491 } 492 const base64String = btoa(str); 493 return base64String.replace(/\+/g, "-").replace( 494 /\//g, 495 "_" 496 ).replace(/=/g, ""); 497 } 498 499 const copyValue = "copy"; 500 const convertValue = "convert"; 501 502 function convert(conversionFn, schema, input) { 503 if (schema === copyValue) { 504 return input; 505 } 506 if (schema === convertValue) { 507 return conversionFn(input); 508 } 509 if (schema instanceof Array) { 510 return input.map((v) => convert(conversionFn, schema[0], v)); 511 } 512 if (schema instanceof Object) { 513 const output = {}; 514 for (const [key, schemaField] of Object.entries(schema)) { 515 if (schemaField.derive) { 516 const v = schemaField.derive(input); 517 if (v !== void 0) { 518 input[key] = v; 519 } 520 } 521 if (!(key in input)) { 522 if (schemaField.required) { 523 throw new Error("Missing key: " + key); 524 } 525 continue; 526 } 527 if (input[key] == null) { 528 output[key] = null; 529 continue; 530 } 531 output[key] = convert( 532 conversionFn, 533 schemaField.schema, 534 input[key] 535 ); 536 } 537 return output; 538 } 539 } 540 541 function derived(schema, derive) { 542 return { 543 required: true, 544 schema, 545 derive 546 }; 547 } 548 549 function required(schema) { 550 return { 551 required: true, 552 schema 553 }; 554 } 555 556 function optional(schema) { 557 return { 558 required: false, 559 schema 560 }; 561 } 562 563 const publicKeyCredentialDescriptorSchema = { 564 type: required(copyValue), 565 id: required(convertValue), 566 transports: optional(copyValue) 567 }; 568 const simplifiedExtensionsSchema = { 569 appid: optional(copyValue), 570 appidExclude: optional(copyValue), 571 credProps: optional(copyValue) 572 }; 573 const simplifiedClientExtensionResultsSchema = { 574 appid: optional(copyValue), 575 appidExclude: optional(copyValue), 576 credProps: optional(copyValue) 577 }; 578 const credentialCreationOptions = { 579 publicKey: required({ 580 rp: required(copyValue), 581 user: required({ 582 id: required(convertValue), 583 name: required(copyValue), 584 displayName: required(copyValue) 585 }), 586 challenge: required(convertValue), 587 pubKeyCredParams: required(copyValue), 588 timeout: optional(copyValue), 589 excludeCredentials: optional([publicKeyCredentialDescriptorSchema]), 590 authenticatorSelection: optional(copyValue), 591 attestation: optional(copyValue), 592 extensions: optional(simplifiedExtensionsSchema) 593 }), 594 signal: optional(copyValue) 595 }; 596 const publicKeyCredentialWithAttestation = { 597 type: required(copyValue), 598 id: required(copyValue), 599 rawId: required(convertValue), 600 authenticatorAttachment: optional(copyValue), 601 response: required({ 602 clientDataJSON: required(convertValue), 603 attestationObject: required(convertValue), 604 transports: derived( 605 copyValue, 606 (response) => { 607 let _a; 608 return ((_a = response.getTransports) == null ? void 0 : _a.call(response)) || []; 609 } 610 ) 611 }), 612 clientExtensionResults: derived( 613 simplifiedClientExtensionResultsSchema, 614 (pkc) => pkc.getClientExtensionResults() 615 ) 616 }; 617 const credentialRequestOptions = { 618 mediation: optional(copyValue), 619 publicKey: required({ 620 challenge: required(convertValue), 621 timeout: optional(copyValue), 622 rpId: optional(copyValue), 623 allowCredentials: optional([publicKeyCredentialDescriptorSchema]), 624 userVerification: optional(copyValue), 625 extensions: optional(simplifiedExtensionsSchema) 626 }), 627 signal: optional(copyValue) 628 }; 629 const publicKeyCredentialWithAssertion = { 630 type: required(copyValue), 631 id: required(copyValue), 632 rawId: required(convertValue), 633 authenticatorAttachment: optional(copyValue), 634 response: required({ 635 clientDataJSON: required(convertValue), 636 authenticatorData: required(convertValue), 637 signature: required(convertValue), 638 userHandle: required(convertValue) 639 }), 640 clientExtensionResults: derived( 641 simplifiedClientExtensionResultsSchema, 642 (pkc) => pkc.getClientExtensionResults() 643 ) 644 }; 645 646 return { 647 createRequestFromJSON: function createRequestFromJSON(requestJSON) { 648 return convert(base64urlToBuffer, credentialCreationOptions, requestJSON); 649 }, 650 createResponseToJSON: function createResponseToJSON(credential) { 651 return convert( 652 bufferToBase64url, 653 publicKeyCredentialWithAttestation, 654 credential 655 ); 656 }, 657 getRequestFromJSON: function getRequestFromJSON(requestJSON) { 658 return convert(base64urlToBuffer, credentialRequestOptions, requestJSON); 659 }, 660 getResponseToJSON: function getResponseToJSON(credential) { 661 return convert( 662 bufferToBase64url, 663 publicKeyCredentialWithAssertion, 664 credential 665 ); 666 }, 667 668 create: async function create(options) { 669 const response = await navigator.credentials.create( 670 options 671 ); 672 response.toJSON = () => this.createResponseToJSON(response); 673 return response; 674 }, 675 get: async function get(options) { 676 return navigator.credentials.get( 677 options 678 ); 679 } 680 } 681} 682 683self.webAuthn = webAuthnWrapper();
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.