Usernameless flow: how to store the challenge created in generateAuthenticationOptions
?
#321
Replies: 2 comments 3 replies
-
This is a great question that's come up a few times in the discussions here...I really need to take this as a TODO to add something to the docs! The technique I've been advocating for to handle this challenge of going usernameless (implementing Conditional UI, for example) is to start a "session" for any user who views the login page, and assign a session ID via HTTP-only cookie on page load. If the user then attempts to auth, you run server's When the response comes in, look up the expected challenge by the session cookie and attempt verification (also make sure to delete the challenge so it can't be reused, even if verification fails, to prevent replay attacks.) If the authentication response succeeds then you log the user in officially and from there on use their user ID as you've stored in the DB for the given credential ID. Does that all make sense? |
Beta Was this translation helpful? Give feedback.
-
So this is kind of a yucky-way to handle the problem; but I think it works - and doesn't require a database. Basically, the browser creates a new ECDSA Keypair, signs and dates the public-key, and sends it to the server. // These change with every page refresh - and cannot be extracted
const browserKeys = await ezcrypto.EcMakeSigKeys(false);
// Prove that BROWSER owns the key-pair by signing our public key with a current-timestamp
let browserData = btoa(
JSON.stringify({
timestamp: new Date().getTime().toString(),
publicKey: browserKeys.publicKey,
})
);
let browserSignature = await ezcrypto.EcSignData(
browserKeys.privateKey,
browserData
);
// Send this data to the server as a POST or part of a querystring.
const POST_REQUEST_ONE = btoa(JSON.stringify({ data: browserData, signature: browserSignature })); The server then signs this signed object and replaces it as the challenge in the auth-options, and returns it to the browser. The browser gets a response from the authenticator, which it (wait for it) signs and returns to the server. The server then gets the user's public key out of the So the server verifies the browser's signature of the authenticator-response, which contains a clientDataJSON object, whose challenge is the server's signature of the browser's signature of the browser's public key and a timestamp - which can't be more than say 60s old. Not sure if this is better or worse than adding a data-layer to the solution but I feel like this would work and doesn't have any glaring security issues... Thanks again for putting together this super awesome library! p.s. I'm working on a WebCrypto abstraction library ( ez-web-crypto ) and couldn't help but come up with an over-engineered solution to this problem, lol |
Beta Was this translation helpful? Give feedback.
-
(I've read through https://simplewebauthn.dev/docs/advanced/passkeys and #103 and posted a comment there but I think it may have been lost as that discussion is quite old. Please let me know if I should delete this discussion though if it's more appropriate to stay in the previous one!)
I'm struggling with one part of the usernameless flow, namely how to store the challenge created from
generateAuthenticationOptions
(server GET) against the user (who we do not know at this point), so that it can be verified via theexpectedChallenge
inverifyAuthenticationResponse
(server POST)?I think I understand that in
verifyAuthenticationResponse
(POST), we would use theuserHandle
to find the Challenge to pass asexpectedChallenge
, but I think I may be missing something obvious for the step before, to actually save the authentication Challenge against the (unknown) user in the first place!I think my question is similar to #103 (comment) but unfortunately I still couldn't quite figure out what to do here. I'd be really grateful for any advice!
Beta Was this translation helpful? Give feedback.
All reactions