MasterKale / SimpleWebAuthn

WebAuthn, Simplified. A collection of TypeScript-first libraries for simpler WebAuthn integration. Supports modern browsers, Node, Deno, and more.
https://simplewebauthn.dev
MIT License
1.62k stars 137 forks source link

Local dev working, production build on deno 2 / Deno Fresh 1.7.X deploy does not #635

Closed robvanvolt closed 1 week ago

robvanvolt commented 1 week ago

I get the following error on production only:

❌ WebAuthn API error details: 
Object { name: "TypeError", message: "t is undefined", code: undefined, options: {…}, errorRaw: TypeError }
const registrationResponse = await startRegistration(options)
        .catch((error) => {
          console.error("❌ WebAuthn API error details:", {
            name: error.name,
            message: error.message,
            code: error.code,
            options: {
              challenge: !!options.challenge,
              rpId: options.rp?.id,
              userId: !!options.user?.id,
            },
            errorRaw: error,
          });
          throw error;
        });

I have already tried to pinpoint the undefined variable.

🔐 Starting WebAuthn registration with validated options
challenge: string
rp: object
rp.name: string
rp.id: string
user: object
user.id: string
user.name: string
user.displayName: string
pubKeyCredParams: object
pubKeyCredParams.0: object
pubKeyCredParams.0.alg: number
pubKeyCredParams.0.type: string
pubKeyCredParams.1: object
pubKeyCredParams.1.alg: number
pubKeyCredParams.1.type: string
timeout: number
attestation: string
excludeCredentials: object
authenticatorSelection: object
authenticatorSelection.residentKey: string
authenticatorSelection.userVerification: string
authenticatorSelection.authenticatorAttachment: string
authenticatorSelection.requireResidentKey: boolean
extensions: object
extensions.credProps: boolean

Does anyone see an issue here?

MasterKale commented 1 week ago

Try console.log(error) in your .catch() to see what the full stack trace is. That'll help identify what might be causing the error as name and message aren't enough for this one.

robvanvolt commented 1 week ago
function u(e){let t=new Uint8Array(e),n="";for(let r of t)n+=String.fromCharCode(r);return btoa(n).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}function p(e){let t=e.replace(/-/g,"+").replace(/_/g,"/"),n=(4-t.length%4)%4,a=t.padEnd(t.length+n,"="),r=atob(a),c=new ArrayBuffer(r.length),d=new Uint8Array(c);for(let o=0;o<r.length;o++)d[o]=r.charCodeAt(o);return c}function A(){return window?.PublicKeyCredential!==void 0&&typeof window.PublicKeyCredential=="function"}function m(e){let{id:t}=e;return{...e,id:p(t),transports:e.transports}}function y(e){return e==="localhost"||/^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i.test(e)}var i=class extends Error{constructor({message:t,code:n,cause:a,name:r}){super(t,{cause:a}),this.name=r??a.name,this.code=n}};function I({error:e,options:t}){let{publicKey:n}=t;if(!n)throw Error("options was missing required publicKey property");if(e.name==="AbortError"){if(t.signal instanceof AbortSignal)return new i({message:"Registration ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:e})}else if(e.name==="ConstraintError"){if(n.authenticatorSelection?.requireResidentKey===!0)return new i({message:"Discoverable credentials were required but no available authenticator supported it",code:"ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT",cause:e});if(t.mediation==="conditional"&&n.authenticatorSelection?.userVerification==="required")return new i({message:"User verification was required during automatic registration but it could not be performed",code:"ERROR_AUTO_REGISTER_USER_VERIFICATION_FAILURE",cause:e});if(n.authenticatorSelection?.userVerification==="required")return new i({message:"User verification was required but no available authenticator supported it",code:"ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT",cause:e})}else{if(e.name==="InvalidStateError")return new i({message:"The authenticator was previously registered",code:"ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED",cause:e});if(e.name==="NotAllowedError")return new i({message:e.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:e});if(e.name==="NotSupportedError")return n.pubKeyCredParams.filter(r=>r.type==="public-key").length===0?new i({message:'No entry in pubKeyCredParams was of type "public-key"',code:"ERROR_MALFORMED_PUBKEYCREDPARAMS",cause:e}):new i({message:"No available authenticator supported any of the specified pubKeyCredParams algorithms",code:"ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG",cause:e});if(e.name==="SecurityError"){let a=window.location.hostname;if(y(a)){if(n.rp.id!==a)return new i({message:`The RP ID "${n.rp.id}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:e})}else return new i({message:`${window.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:e})}else if(e.name==="TypeError"){if(n.user.id.byteLength<1||n.user.id.byteLength>64)return new i({message:"User ID was not between 1 and 64 characters",code:"ERROR_INVALID_USER_ID_LENGTH",cause:e})}else if(e.name==="UnknownError")return new i({message:"The authenticator was unable to process the specified options, or could not create a new credential",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:e})}return e}var b=class{createNewAbortSignal(){if(this.controller){let n=new Error("Cancelling existing WebAuthn API call for new one");n.name="AbortError",this.controller.abort(n)}let t=new AbortController;return this.controller=t,t.signal}cancelCeremony(){if(this.controller){let t=new Error("Manually cancelling existing WebAuthn API call");t.name="AbortError",this.controller.abort(t),this.controller=void 0}}},_=new b,S=["cross-platform","platform"];function O(e){if(e&&!(S.indexOf(e)<0))return e}async function T(e){let{optionsJSON:t,useAutoRegister:n=!1}=e;if(!A())throw new Error("WebAuthn is not supported in this browser");let a={...t,challenge:p(t.challenge),user:{...t.user,id:p(t.user.id)},excludeCredentials:t.excludeCredentials?.map(m)},r={};n&&(r.mediation="conditional"),r.publicKey=a,r.signal=_.createNewAbortSignal();let c;try{c=await navigator.credentials.create(r)}catch(l){throw I({error:l,options:r})}if(!c)throw new Error("Registration was not completed");let{id:d,rawId:o,response:s,type:g}=c,f;typeof s.getTransports=="function"&&(f=s.getTransports());let w;if(typeof s.getPublicKeyAlgorithm=="function")try{w=s.getPublicKeyAlgorithm()}catch(l){E("getPublicKeyAlgorithm()",l)}let R;if(typeof s.getPublicKey=="function")try{let l=s.getPublicKey();l!==null&&(R=u(l))}catch(l){E("getPublicKey()",l)}let h;if(typeof s.getAuthenticatorData=="function")try{h=u(s.getAuthenticatorData())}catch(l){E("getAuthenticatorData()",l)}return{id:d,rawId:u(o),response:{attestationObject:u(s.attestationObject),clientDataJSON:u(s.clientDataJSON),transports:f,publicKeyAlgorithm:w,publicKey:R,authenticatorData:h},type:g,clientExtensionResults:c.getClientExtensionResults(),authenticatorAttachment:O(c.authenticatorAttachment)}}function E(e,t){console.warn(`The browser extension that intercepted this WebAuthn API call incorrectly implemented ${e}. You should report this error to them.
`,t)}function P(){if(!A())return new Promise(t=>t(!1));let e=window.PublicKeyCredential;return e.isConditionalMediationAvailable===void 0?new Promise(t=>t(!1)):e.isConditionalMediationAvailable()}function C({error:e,options:t}){let{publicKey:n}=t;if(!n)throw Error("options was missing required publicKey property");if(e.name==="AbortError"){if(t.signal instanceof AbortSignal)return new i({message:"Authentication ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:e})}else{if(e.name==="NotAllowedError")return new i({message:e.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:e});if(e.name==="SecurityError"){let a=window.location.hostname;if(y(a)){if(n.rpId!==a)return new i({message:`The RP ID "${n.rpId}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:e})}else return new i({message:`${window.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:e})}else if(e.name==="UnknownError")return new i({message:"The authenticator was unable to process the specified options, or could not create a new assertion signature",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:e})}return e}async function N(e){let{optionsJSON:t,useBrowserAutofill:n=!1,verifyBrowserAutofillInput:a=!0}=e;if(!A())throw new Error("WebAuthn is not supported in this browser");let r;t.allowCredentials?.length!==0&&(r=t.allowCredentials?.map(m));let c={...t,challenge:p(t.challenge),allowCredentials:r},d={};if(n){if(!await P())throw Error("Browser does not support WebAuthn autofill");if(document.querySelectorAll("input[autocomplete$='webauthn']").length<1&&a)throw Error('No <input> with "webauthn" as the only or last value in its `autocomplete` attribute was detected');d.mediation="conditional",c.allowCredentials=[]}d.publicKey=c,d.signal=_.createNewAbortSignal();let o;try{o=await navigator.credentials.get(d)}catch(h){throw C({error:h,options:d})}if(!o)throw new Error("Authentication was not completed");let{id:s,rawId:g,response:f,type:w}=o,R;return f.userHandle&&(R=u(f.userHandle)),{id:s,rawId:u(g),response:{authenticatorData:u(f.authenticatorData),clientDataJSON:u(f.clientDataJSON),signature:u(f.signature),userHandle:R},type:w,clientExtensionResults:o.getClientExtensionResults(),authenticatorAttachment:O(o.authenticatorAttachment)}}export{A as a,T as b,N as c};

It's this file

The raw error is:

Object { name: "TypeError", message: "t is undefined", code: undefined, options: {…}, errorRaw: TypeError }
​
code: undefined
​
errorRaw: TypeError: t is undefined
​​
columnNumber: 3867
​​
fileName: "https://mydomain.deno.dev/_frsh/js/ff7b3f1d7363578066fdbc5cdd468ec93df5f266/chunk-6IR4Q7Z4.js"
​​
lineNumber: 1
​​
message: "t is undefined"
​​
stack: "T@https://mydomain.deno.dev/_frsh/js/ff7b3f1d7363578066fdbc5cdd468ec93df5f266/chunk-6IR4Q7Z4.js:1:3867\nonClick@https://mydomain.deno.dev/_frsh/js/ff7b3f1d7363578066fdbc5cdd468ec93df5f266/island-registerisland.js:1:1314\nasync*K/<@https://mydomain.deno.dev/_frsh/js/ff7b3f1d7363578066fdbc5cdd468ec93df5f266/chunk-RS4YOACS.js:1:4680\nEventListener.handleEvent*M@https://mydomain.deno.dev/_frsh/js/ff7b3f1d7363578066fdbc5cdd468ec93df5f266/chunk-RS4YOACS.js:1:4141\npn@https://mydomain.deno.dev/_frsh/js/ff7b3f1d7363578066fdbc5cdd468ec93df5f266/chunk-RS4YOACS.js:1:8099\nV@https://mydomain.deno.dev/_frsh/js/ff7b3f1d7363578066fdbc5cdd468ec93df5f266/chunk-RS4YOACS.js:1:6886\ntn@https://mydomain.deno.dev/_frsh/js/ff7b3f1d7363578066fdbc5cdd468ec93df5f266/chunk-RS4YOACS.js:1:1828\npn@https://mydomain.deno.dev/_frsh/js/ff7b3f1d7363578066fdbc5cdd468ec93df5f266/chunk-RS4YOACS.js:1:8233\nV@https://mydomain.deno.dev/_frsh/js/ff7b3f1d7363578066fdbc5cdd468ec93df5f266/chunk-RS4YOACS.js:1:6886\ntn@https://mydomain.deno.dev/_frsh/js/ff7b3f1d7363578066fdbc5cdd468ec93df5f266/chunk-RS4YOACS.js:1:1828\nV@https://mydomain.deno.dev/_frsh/js/ff7b3f1d7363578066fdbc5cdd468ec93df5f266/chunk-RS4YOACS.js:1:6552\ntn@https://mydomain.deno.dev/_frsh/js/ff7b3f1d7363578066fdbc5cdd468ec93df5f266/chunk-RS4YOACS.js:1:1828\nV@https://mydomain.deno.dev/_frsh/js/ff7b3f1d7363578066fdbc5cdd468ec93df5f266/chunk-RS4YOACS.js:1:6552\nan@https://mydomain.deno.dev/_frsh/js/ff7b3f1d7363578066fdbc5cdd468ec93df5f266/chunk-RS4YOACS.js:1:9130\ns@https://mydomain.deno.dev/_frsh/js/ff7b3f1d7363578066fdbc5cdd468ec93df5f266/main.js:1:2378\nsetTimeout handler*Me@https://mydomain.deno.dev/_frsh/js/ff7b3f1d7363578066fdbc5cdd468ec93df5f266/main.js:1:2428\n@https://mydomain.deno.dev/:1:3218\n"
​​
<prototype>: TypeError.prototype { stack: "", … }
​
message: "t is undefined"
​
name: "TypeError"
​
options: Object { challenge: true, rpId: "mydomain.deno.dev", userId: true }
​​
challenge: true
​​
rpId: "mydomain.deno.dev"

I think it might be the following line:

p(e){let t=e.replace(/-/g,"+").replace(/_/g,"/")

That leads to t being undefined --> e is not correctly a string even though it should be, and it is clearly a string as shown in the options I have posted earlier:

🔐 Starting WebAuthn registration with validated options
...
challenge: string
...
rp.id: string
...

Edit: I was running the code on firefox.

For Safari, I get this error: Cannot read properties of undefined (reading 'challenge')

And in Chrome, i get: Cannot read properties of undefined (reading 'challenge')

So it is weird that options.challenge is clearly there (as after the generate-registration-options), but gets lost in the startregistration(options) process...

MasterKale commented 1 week ago

Hmm, can you share an example of your options argument?

And is startRegistration() out of @simplewebauthn/browser, or your own that's wrapping it?

robvanvolt commented 1 week ago

Thank you very much! I have solved it by a weird hack:

  options.optionsJSON = options; // without this line, it does not work
  const registrationResponse = await startRegistration(options)

I thought one could use the options directly without having to nest it again within itself... Have I overread something in the documentation where it is stated or is it a bug / unintentional? I am using 11.0.0.

import {
  browserSupportsWebAuthn,
  startRegistration,
} from "npm:@simplewebauthn/browser";

Sample options (without nesting that does not work):

🔑 Received options: 
Object { challenge: "3sI-mx3heP...", rp: {…}, user: {…}, pubKeyCredParams: (2) […], timeout: 60000, attestation: "none", excludeCredentials: (1) […], authenticatorSelection: {…}, extensions: {…} }
attestation: "none"
authenticatorSelection: Object { residentKey: "discouraged", userVerification: "preferred", authenticatorAttachment: "platform", … }
challenge: "3sI-mx3heP..."
excludeCredentials: Array [ {…} ]
extensions: Object { credProps: true }
credProps: true
<prototype>: Object { … }
pubKeyCredParams: Array [ {…}, {…} ]
rp: Object { name: "App Example", id: "localhost" }
timeout: 60000
user: Object { id: "aW50ZXJuYW...", name: "user@localhost", displayName: "" }
MasterKale commented 1 week ago

Ah, maybe that's the confusion, Breaking Changes for v11 in the CHANGELOG detail the API change to startRegistration() and startAuthentication() that requires refactoring calls to pass in a single object with properties, instead of using positional arguments:

https://github.com/MasterKale/SimpleWebAuthn/blob/master/CHANGELOG.md#browser-positional-arguments-in-startregistration-and-startauthentication-have-been-replaced-by-a-single-object

Sorry for the confusion. I'm seeing an opportunity here to make this experience a little nicer (e.g. checking for the presence of optionsJSON and nesting automatically, or perhaps erroring more descriptively when optionsJSON is missing.)

If there are any docs I failed to update to account for the v11 changes can you please let me know? It'll give me a chance to update those to save other developers from your pain 😂 😭

robvanvolt commented 1 week ago

No worries, my bad, I have reread the documentation and now it makes sense - i think i have overread the naming convention (that it needs to explicitly be called {optionsJSON} instead of just {options} and the error confused me at the beginning!:)

The following part was not too clear for me:

   // GET registration options from the endpoint that calls
    // @simplewebauthn/server -> generateRegistrationOptions()
    const resp = await fetch('/generate-registration-options');
    const optionsJSON = await resp.json();

    let attResp;
    try {
      // Pass the options to the authenticator and wait for a response
      attResp = await startRegistration({ optionsJSON });
    } catch (error) {
      // Some basic error handling
      if (error.name === 'InvalidStateError') {
        elemError.innerText = 'Error: Authenticator was probably already registered by user';
      } else {
        elemError.innerText = error;
      }

      throw error;
    }

https://simplewebauthn.dev/docs/packages/browser

I have just named my resp.json() options instead of optionsJSON.

Who would have thought that the naming convention plays a role a few lines further down, and I was not seeing that a few the name of the variable is not arbitrarilly chosen but needs to be exactl optionsJSON, because it is not just deconstructing { ...optionsJSON } but { optionsJSON } ---> { optionsJSON: optionsJSON }

Best,

RVV

Edit:

The example is better to understand:

            const opts = await resp.json();

            printDebug(elemDebug, 'Registration Options', JSON.stringify(opts, null, 2));

            hideAuthForm();

            attResp = await startRegistration({ optionsJSON: opts });

https://github.com/MasterKale/SimpleWebAuthn/blob/master/example/public/index.html