index.js
5 days ago
user-login-ajax.js
5 days ago
user-login.js
5 days ago
user-profile-ajax.js
5 days ago
user-profile.js
5 days ago
index.js
364 lines
| 1 | /* [@simplewebauthn/browser@7.4.0] */ |
| 2 | function utf8StringToBuffer(value) { |
| 3 | return new TextEncoder().encode(value); |
| 4 | } |
| 5 | |
| 6 | function bufferToBase64URLString(buffer) { |
| 7 | const bytes = new Uint8Array(buffer); |
| 8 | let str = ''; |
| 9 | for (const charCode of bytes) { |
| 10 | str += String.fromCharCode(charCode); |
| 11 | } |
| 12 | const base64String = btoa(str); |
| 13 | return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); |
| 14 | } |
| 15 | |
| 16 | function base64URLStringToBuffer(base64URLString) { |
| 17 | const base64 = base64URLString.replace(/-/g, '+').replace(/_/g, '/'); |
| 18 | const padLength = (4 - (base64.length % 4)) % 4; |
| 19 | const padded = base64.padEnd(base64.length + padLength, '='); |
| 20 | const binary = atob(padded); |
| 21 | const buffer = new ArrayBuffer(binary.length); |
| 22 | const bytes = new Uint8Array(buffer); |
| 23 | for (let i = 0; i < binary.length; i++) { |
| 24 | bytes[i] = binary.charCodeAt(i); |
| 25 | } |
| 26 | return buffer; |
| 27 | } |
| 28 | |
| 29 | function browserSupportsWebAuthn() { |
| 30 | return (window?.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === 'function'); |
| 31 | } |
| 32 | |
| 33 | function toPublicKeyCredentialDescriptor(descriptor) { |
| 34 | const { id } = descriptor; |
| 35 | return { |
| 36 | ...descriptor, |
| 37 | id: base64URLStringToBuffer(id), |
| 38 | transports: descriptor.transports, |
| 39 | }; |
| 40 | } |
| 41 | |
| 42 | function isValidDomain(hostname) { |
| 43 | return (hostname === 'localhost' || /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i.test(hostname)); |
| 44 | } |
| 45 | |
| 46 | class WebAuthnError extends Error { |
| 47 | code; |
| 48 | constructor({ message, code, cause, name, }) { |
| 49 | super(message, { cause }); |
| 50 | this.name = name ?? cause.name; |
| 51 | this.code = code; |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | function identifyRegistrationError({ error, options, }) { |
| 56 | const { publicKey } = options; |
| 57 | if (!publicKey) { |
| 58 | throw Error('options was missing required publicKey property'); |
| 59 | } |
| 60 | if (error.name === 'AbortError') { |
| 61 | if (options.signal instanceof AbortSignal) { |
| 62 | return new WebAuthnError({ |
| 63 | message: 'Registration ceremony was sent an abort signal', |
| 64 | code: 'ERROR_CEREMONY_ABORTED', |
| 65 | cause: error, |
| 66 | }); |
| 67 | } |
| 68 | } |
| 69 | else if (error.name === 'ConstraintError') { |
| 70 | if (publicKey.authenticatorSelection?.requireResidentKey === true) { |
| 71 | return new WebAuthnError({ |
| 72 | message: 'Discoverable credentials were required but no available authenticator supported it', |
| 73 | code: 'ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT', |
| 74 | cause: error, |
| 75 | }); |
| 76 | } |
| 77 | else if (publicKey.authenticatorSelection?.userVerification === 'required') { |
| 78 | return new WebAuthnError({ |
| 79 | message: 'User verification was required but no available authenticator supported it', |
| 80 | code: 'ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT', |
| 81 | cause: error, |
| 82 | }); |
| 83 | } |
| 84 | } |
| 85 | else if (error.name === 'InvalidStateError') { |
| 86 | return new WebAuthnError({ |
| 87 | message: 'The authenticator was previously registered', |
| 88 | code: 'ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED', |
| 89 | cause: error |
| 90 | }); |
| 91 | } |
| 92 | else if (error.name === 'NotAllowedError') { |
| 93 | return new WebAuthnError({ |
| 94 | message: error.message, |
| 95 | code: 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY', |
| 96 | cause: error, |
| 97 | }); |
| 98 | } |
| 99 | else if (error.name === 'NotSupportedError') { |
| 100 | const validPubKeyCredParams = publicKey.pubKeyCredParams.filter(param => param.type === 'public-key'); |
| 101 | if (validPubKeyCredParams.length === 0) { |
| 102 | return new WebAuthnError({ |
| 103 | message: 'No entry in pubKeyCredParams was of type "public-key"', |
| 104 | code: 'ERROR_MALFORMED_PUBKEYCREDPARAMS', |
| 105 | cause: error, |
| 106 | }); |
| 107 | } |
| 108 | return new WebAuthnError({ |
| 109 | message: 'No available authenticator supported any of the specified pubKeyCredParams algorithms', |
| 110 | code: 'ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG', |
| 111 | cause: error, |
| 112 | }); |
| 113 | } |
| 114 | else if (error.name === 'SecurityError') { |
| 115 | const effectiveDomain = window.location.hostname; |
| 116 | if (!isValidDomain(effectiveDomain)) { |
| 117 | return new WebAuthnError({ |
| 118 | message: `${window.location.hostname} is an invalid domain`, |
| 119 | code: 'ERROR_INVALID_DOMAIN', |
| 120 | cause: error |
| 121 | }); |
| 122 | } |
| 123 | else if (publicKey.rp.id !== effectiveDomain) { |
| 124 | return new WebAuthnError({ |
| 125 | message: `The RP ID "${publicKey.rp.id}" is invalid for this domain`, |
| 126 | code: 'ERROR_INVALID_RP_ID', |
| 127 | cause: error, |
| 128 | }); |
| 129 | } |
| 130 | } |
| 131 | else if (error.name === 'TypeError') { |
| 132 | if (publicKey.user.id.byteLength < 1 || publicKey.user.id.byteLength > 64) { |
| 133 | return new WebAuthnError({ |
| 134 | message: 'User ID was not between 1 and 64 characters', |
| 135 | code: 'ERROR_INVALID_USER_ID_LENGTH', |
| 136 | cause: error, |
| 137 | }); |
| 138 | } |
| 139 | } |
| 140 | else if (error.name === 'UnknownError') { |
| 141 | return new WebAuthnError({ |
| 142 | message: 'The authenticator was unable to process the specified options, or could not create a new credential', |
| 143 | code: 'ERROR_AUTHENTICATOR_GENERAL_ERROR', |
| 144 | cause: error, |
| 145 | }); |
| 146 | } |
| 147 | return error; |
| 148 | } |
| 149 | |
| 150 | class WebAuthnAbortService { |
| 151 | controller; |
| 152 | createNewAbortSignal() { |
| 153 | if (this.controller) { |
| 154 | const abortError = new Error('Cancelling existing WebAuthn API call for new one'); |
| 155 | abortError.name = 'AbortError'; |
| 156 | this.controller.abort(abortError); |
| 157 | } |
| 158 | const newController = new AbortController(); |
| 159 | this.controller = newController; |
| 160 | return newController.signal; |
| 161 | } |
| 162 | } |
| 163 | const webauthnAbortService = new WebAuthnAbortService(); |
| 164 | |
| 165 | const attachments = ['cross-platform', 'platform']; |
| 166 | function toAuthenticatorAttachment(attachment) { |
| 167 | if (!attachment) { |
| 168 | return; |
| 169 | } |
| 170 | if (attachments.indexOf(attachment) < 0) { |
| 171 | return; |
| 172 | } |
| 173 | return attachment; |
| 174 | } |
| 175 | |
| 176 | async function startRegistration(creationOptionsJSON) { |
| 177 | if (!browserSupportsWebAuthn()) { |
| 178 | throw new Error('WebAuthn is not supported in this browser'); |
| 179 | } |
| 180 | const publicKey = { |
| 181 | ...creationOptionsJSON, |
| 182 | challenge: base64URLStringToBuffer(creationOptionsJSON.challenge), |
| 183 | user: { |
| 184 | ...creationOptionsJSON.user, |
| 185 | id: utf8StringToBuffer(creationOptionsJSON.user.id), |
| 186 | }, |
| 187 | excludeCredentials: creationOptionsJSON.excludeCredentials?.map(toPublicKeyCredentialDescriptor), |
| 188 | }; |
| 189 | const options = { publicKey }; |
| 190 | |
| 191 | options.signal = webauthnAbortService.createNewAbortSignal(); |
| 192 | let credential; |
| 193 | try { |
| 194 | credential = (await navigator.credentials.create(options)); |
| 195 | } |
| 196 | catch (err) { |
| 197 | throw identifyRegistrationError({ error: err, options }); |
| 198 | } |
| 199 | if (!credential) { |
| 200 | throw new Error('Registration was not completed'); |
| 201 | } |
| 202 | const { id, rawId, response, type } = credential; |
| 203 | let transports = undefined; |
| 204 | if (typeof response.getTransports === 'function') { |
| 205 | transports = response.getTransports(); |
| 206 | } |
| 207 | let responsePublicKeyAlgorithm = undefined; |
| 208 | if (typeof response.getPublicKeyAlgorithm === 'function') { |
| 209 | responsePublicKeyAlgorithm = response.getPublicKeyAlgorithm(); |
| 210 | } |
| 211 | let responsePublicKey = undefined; |
| 212 | if (typeof response.getPublicKey === 'function') { |
| 213 | const _publicKey = response.getPublicKey(); |
| 214 | if (_publicKey !== null) { |
| 215 | responsePublicKey = bufferToBase64URLString(_publicKey); |
| 216 | } |
| 217 | } |
| 218 | let responseAuthenticatorData; |
| 219 | if (typeof response.getAuthenticatorData === 'function') { |
| 220 | responseAuthenticatorData = bufferToBase64URLString(response.getAuthenticatorData()); |
| 221 | } |
| 222 | return { |
| 223 | id, |
| 224 | rawId: bufferToBase64URLString(rawId), |
| 225 | response: { |
| 226 | attestationObject: bufferToBase64URLString(response.attestationObject), |
| 227 | clientDataJSON: bufferToBase64URLString(response.clientDataJSON), |
| 228 | transports, |
| 229 | publicKeyAlgorithm: responsePublicKeyAlgorithm, |
| 230 | publicKey: responsePublicKey, |
| 231 | authenticatorData: responseAuthenticatorData, |
| 232 | }, |
| 233 | type, |
| 234 | clientExtensionResults: credential.getClientExtensionResults(), |
| 235 | authenticatorAttachment: toAuthenticatorAttachment(credential.authenticatorAttachment), |
| 236 | }; |
| 237 | } |
| 238 | |
| 239 | function bufferToUTF8String(value) { |
| 240 | return new TextDecoder('utf-8').decode(value); |
| 241 | } |
| 242 | |
| 243 | async function browserSupportsWebAuthnAutofill() { |
| 244 | const globalPublicKeyCredential = window.PublicKeyCredential; |
| 245 | return (globalPublicKeyCredential.isConditionalMediationAvailable !== undefined && |
| 246 | globalPublicKeyCredential.isConditionalMediationAvailable()); |
| 247 | } |
| 248 | |
| 249 | function identifyAuthenticationError({ error, options, }) { |
| 250 | const { publicKey } = options; |
| 251 | if (!publicKey) { |
| 252 | throw Error('options was missing required publicKey property'); |
| 253 | } |
| 254 | if (error.name === 'AbortError') { |
| 255 | if (options.signal instanceof AbortSignal) { |
| 256 | return new WebAuthnError({ |
| 257 | message: 'Authentication ceremony was sent an abort signal', |
| 258 | code: 'ERROR_CEREMONY_ABORTED', |
| 259 | cause: error, |
| 260 | }); |
| 261 | } |
| 262 | } |
| 263 | else if (error.name === 'NotAllowedError') { |
| 264 | return new WebAuthnError({ |
| 265 | message: error.message, |
| 266 | code: 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY', |
| 267 | cause: error, |
| 268 | }); |
| 269 | } |
| 270 | else if (error.name === 'SecurityError') { |
| 271 | const effectiveDomain = window.location.hostname; |
| 272 | if (!isValidDomain(effectiveDomain)) { |
| 273 | return new WebAuthnError({ |
| 274 | message: `${window.location.hostname} is an invalid domain`, |
| 275 | code: 'ERROR_INVALID_DOMAIN', |
| 276 | cause: error, |
| 277 | }); |
| 278 | } |
| 279 | else if (publicKey.rpId !== effectiveDomain) { |
| 280 | return new WebAuthnError({ |
| 281 | message: `The RP ID "${publicKey.rpId}" is invalid for this domain`, |
| 282 | code: 'ERROR_INVALID_RP_ID', |
| 283 | cause: error, |
| 284 | }); |
| 285 | } |
| 286 | } |
| 287 | else if (error.name === 'UnknownError') { |
| 288 | return new WebAuthnError({ |
| 289 | message: 'The authenticator was unable to process the specified options, or could not create a new assertion signature', |
| 290 | code: 'ERROR_AUTHENTICATOR_GENERAL_ERROR', |
| 291 | cause: error, |
| 292 | }); |
| 293 | } |
| 294 | return error; |
| 295 | } |
| 296 | |
| 297 | async function startAuthentication(requestOptionsJSON, useBrowserAutofill = false) { |
| 298 | if (!browserSupportsWebAuthn()) { |
| 299 | throw new Error('WebAuthn is not supported in this browser'); |
| 300 | } |
| 301 | let allowCredentials; |
| 302 | if (requestOptionsJSON.allowCredentials?.length !== 0) { |
| 303 | allowCredentials = requestOptionsJSON.allowCredentials?.map(toPublicKeyCredentialDescriptor); |
| 304 | } |
| 305 | |
| 306 | const publicKey = { |
| 307 | ...requestOptionsJSON, |
| 308 | challenge: base64URLStringToBuffer(requestOptionsJSON.challenge), |
| 309 | allowCredentials, |
| 310 | }; |
| 311 | const options = {}; |
| 312 | if (useBrowserAutofill) { |
| 313 | if (!(await browserSupportsWebAuthnAutofill())) { |
| 314 | throw Error('Browser does not support WebAuthn autofill'); |
| 315 | } |
| 316 | const eligibleInputs = document.querySelectorAll("input[autocomplete*='webauthn']"); |
| 317 | if (eligibleInputs.length < 1) { |
| 318 | throw Error('No <input> with `"webauthn"` in its `autocomplete` attribute was detected'); |
| 319 | } |
| 320 | options.mediation = 'conditional'; |
| 321 | publicKey.allowCredentials = []; |
| 322 | } |
| 323 | options.publicKey = publicKey; |
| 324 | options.signal = webauthnAbortService.createNewAbortSignal(); |
| 325 | let credential; |
| 326 | try { |
| 327 | credential = (await navigator.credentials.get(options)); |
| 328 | } |
| 329 | catch (err) { |
| 330 | throw identifyAuthenticationError({ error: err, options }); |
| 331 | } |
| 332 | if (!credential) { |
| 333 | throw new Error('Authentication was not completed'); |
| 334 | } |
| 335 | const { id, rawId, response, type } = credential; |
| 336 | |
| 337 | let userHandle = undefined; |
| 338 | if (response.userHandle) { |
| 339 | userHandle = bufferToUTF8String(response.userHandle); |
| 340 | } |
| 341 | return { |
| 342 | id, |
| 343 | rawId: bufferToBase64URLString(rawId), |
| 344 | response: { |
| 345 | authenticatorData: bufferToBase64URLString(response.authenticatorData), |
| 346 | clientDataJSON: bufferToBase64URLString(response.clientDataJSON), |
| 347 | signature: bufferToBase64URLString(response.signature), |
| 348 | userHandle, |
| 349 | }, |
| 350 | type, |
| 351 | clientExtensionResults: credential.getClientExtensionResults(), |
| 352 | authenticatorAttachment: toAuthenticatorAttachment(credential.authenticatorAttachment), |
| 353 | }; |
| 354 | } |
| 355 | |
| 356 | async function platformAuthenticatorIsAvailable() { |
| 357 | if (!browserSupportsWebAuthn()) { |
| 358 | return false; |
| 359 | } |
| 360 | return PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); |
| 361 | } |
| 362 | |
| 363 | export { browserSupportsWebAuthn, browserSupportsWebAuthnAutofill, platformAuthenticatorIsAvailable, startAuthentication, startRegistration }; |
| 364 |