@@ -81,13 +81,16 @@ async function generateRegistrationOptions(
8181): PublicKeyCredentialCreationOptionsJSON {
8282 // A domain name for your site (e.g. "passkeys.dev")
8383 const rpID: string = process .env .RP_ID ;
84- // A human-readable name for your website (e.g. "Passkeys Developer Resources")
84+
85+ // A human-read able name for your website (e.g. "Passkeys Developer Resources")
8586 const rpName: string = process .env .RP_NAME ;
8687
8788 // Generate one-time-use random bytes for the authenticator to sign
8889 const challenge: Uint8Array = await pseudocodeGenerateChallenge (currentUser );
90+
8991 // Generate or retrieve a pseudonymous, WebAuthn-specific user identifier as random bytes
9092 const userID: Uint8Array = await pseudocodeGetWebAuthnUserID (currentUser );
93+
9194 // Get a list of the user's currently registered passkeys to prevent re-registration
9295 const userCurrentPasskeys: PasskeyModel [] = await pseudocodeGetCurrentPasskeys (currentUser );
9396
@@ -157,14 +160,13 @@ in the shape of [`RegistrationResponseJSON`](https://w3c.github.io/webauthn/#dic
157160 * Check that the WebAuthn registration data represents a well-formed passkey
158161 */
159162async function verifyRegistrationResponse(
160- currentUser : UserModel ,
161163 registrationOptions : PublicKeyCredentialCreationOptionsJSON ,
162164 registrationResponse : RegistrationResponseJSON ,
163165): VerifiedRegistration {
164166 try {
165167 // TODO: Write basic attestation-less response verification here?
166- } catch (error ) {
167- throw new Error (` Couldn't verify registration response ` , { cause: error });
168+ } catch (err ) {
169+ throw new Error (` Couldn't verify registration response ` , { cause: err });
168170 }
169171
170172 return {
@@ -195,16 +197,16 @@ type VerifiedRegistration = {
195197// The ID of the user's auth session that was created after the user logged in
196198const sessionID = request .session .id ;
197199
198- // User data associated with the current auth session
199- const currentUser: UserModel = await getUserData (sessionID );
200-
201200// Retrieve registration options for the current attempt to check for expected values
202201const regOptions: PublicKeyCredentialCreationOptionsJSON =
203202 await pseudocodeRetrieveAndDeleteRegistrationOptions (sessionID );
204203
204+ // Assume `RegistrationResponseJSON` was sent back as JSON via a POST command
205+ const regResponse = request .body ;
206+
205207let passkey;
206208try {
207- const verification = await verifyRegistrationResponse (currentUser , regOptions , regResponse );
209+ const verification = await verifyRegistrationResponse (regOptions , regResponse );
208210 passkey = verification .passkey ;
209211 // User successfully registered a passkey, continue
210212} catch (err ) {
@@ -217,6 +219,9 @@ Assuming successful creation, information about the newly-created passkey
217219should then get stored as a ` PasskeyModel ` record in the database:
218220
219221``` ts
222+ // User data associated with the current auth session
223+ const currentUser: UserModel = await getUserData (sessionID );
224+
220225/**
221226 * - Use `currentUser` for the foreign key needed for `PasskeyModel.user`
222227 * - Use `regOptions.user.id` for `PasskeyModel.webauthnUserID`
@@ -225,10 +230,177 @@ should then get stored as a `PasskeyModel` record in the database:
225230await pseudocodeSaveNewPasskey (currentUser , regOptions , passkey );
226231```
227232
233+ The user is now ready to use their passkey for subsequent authentication.
234+
228235## 3. Generate authentication options
229236
237+ For similar reasons, authentication options generation should aim to return a value
238+ shaped like [ ` PublicKeyCredentialRequestOptionsJSON ` ] ( https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson ) .
239+ This will simplify the work of sending these options to the front end as JSON:
240+
241+ ``` ts
242+ /**
243+ * Create authentication options for a user to sign in with a passkey
244+ */
245+ async function generateAuthenticationOptions(
246+ currentUser ? : UserModel ,
247+ ): PublicKeyCredentialRequestOptionsJSON {
248+ // This must be the same value specified during registration (e.g. "passkeys.dev")
249+ const rpID: string = process .env .RP_ID ;
250+
251+ // Generate one-time-use random bytes for the authenticator to sign
252+ const challenge: Uint8Array = await pseudocodeGenerateChallenge (currentUser );
253+
254+ // If you know the user that's trying to log in then get the list of passkeys they can use
255+ // Hint: You won't know the user if you're using conditional UI...
256+ let userCurrentPasskeys: PasskeyModel [] = [];
257+ if (currentUser ) {
258+ userCurrentPasskeys = await pseudocodeGetCurrentPasskeys (currentUser );
259+ }
260+
261+ return {
262+ rpId: rpID ,
263+ challenge: pseudocodeBytesToBase64URLString (challenge ),
264+ allowCredentials: userCurrentPasskeys .map ((passkey ) => ({
265+ id: passkey .id ,
266+ transports: passkey .transports ,
267+ type: ' public-key' ,
268+ })),
269+ userVerification: ' preferred' ,
270+ }
271+ }
272+ ```
273+
274+ Generate options, then store them for * someone* to use to log in with a registered passkey:
275+
276+ ``` ts
277+ // The ID of an UNAUTHENTICATED user session, established when a user hits the login page
278+ const unknownUserSessionID = request .session .id ;
279+
280+ // User data associated with the provided account identifier (email, username, etc...)
281+ let currentUser: UserModel | undefined = undefined ;
282+ if (enteredAccountID ) {
283+ currentUser = await getUserData (enteredAccountID );
284+ }
285+
286+ const authOptions = await generateAuthenticationOptions (currentUser );
287+
288+ // Persist the options so we can reference values in them during verification
289+ await pseudocodeSaveAuthenticationOptions (unknownUserSessionID , authOptions );
290+ ```
291+
292+ Send these options to the {{< link "./frontend.md" >}}frontend{{< /link >}} to have the user attempt to log in.
293+
294+ {{< alert type="info" >}}
295+ WebAuthn is capable of handling both "passwordless" and "usernameless" authentication flows:
296+
297+ - ** Passwordless** authentication often starts with the user typing in an account identifier.
298+ In this flow the Relying Party should populate ` allowCredentials ` with that user's
299+ registered passkeys to lean on the browser to help the user understand when they have a passkey
300+ for the site.
301+
302+ - ** Usernameless** authentication involves initiating a WebAuthn authentication attempt
303+ without any upfront knowledge about the user's identity.
304+ Instead, the user first chooses to use a registered passkey in response to a WebAuthn call
305+ with an empty ` allowCredentials. `
306+ Next the website checks that it recognizes the passkey ID,
307+ verifies the signature in the response with the public key for that passkey,
308+ then logs the user in as whatever user is assigned to ` PasskeyModel.user `
309+ when the passkey was created.
310+ {{< /alert >}}
311+
230312## 4. Verify authentication responses
231313
314+ Assuming successful use of WebAuthn to generate an authentication response on the frontend,
315+ the Relying Party should now verify the instance of
316+ [ ` AuthenticationResponseJSON ` ] ( https://w3c.github.io/webauthn/#dictdef-authenticationresponsejson ) :
317+
318+ ``` ts
319+ /**
320+ * Make sure the authentication response is valid for the specified, registered passkey
321+ */
322+ async function verifyAuthenticationResponse(
323+ authOptions : PublicKeyCredentialRequestOptionsJSON ,
324+ authResponse : AuthenticationResponseJSON ,
325+ registeredPasskey : PasskeyModel ,
326+ ): VerifiedAuthentication {
327+ try {
328+ // TODO: Write basic authentication verification here?
329+ } catch (err ) {
330+ throw new Error (` Couldn't verify authentication response ` , { cause: err });
331+ }
332+
333+ return {
334+ passkey: {
335+ id: registeredPasskey .id ,
336+ newCounter: 0 ,
337+ backupEligible: true ,
338+ backupStatus: true ,
339+ },
340+ }
341+ }
342+
343+ type VerifiedAuthentication = {
344+ passkey: {
345+ id: Base64URLString ;
346+ newCounter: number ;
347+ backupEligible: boolean ;
348+ backupStatus: boolean ;
349+ }
350+ };
351+ ```
352+
353+ Call the method, then log the user in upon successful verification:
354+
355+ ``` ts
356+ // The ID of an UNAUTHENTICATED user session, established when a user hits the login page
357+ const unknownUserSessionID = request .session .id ;
358+
359+ // Retrieve registration options for the current attempt to check for expected values
360+ const authOptions: PublicKeyCredentialRequestOptionsJSON =
361+ await pseudocodeRetrieveAndDeleteAuthenticationOptions (unknownUserSessionID );
362+
363+ // Assume `AuthenticationResponseJSON` was sent back as JSON via a POST command
364+ const authResponse = request .body ;
365+
366+ // Make sure the credential ID specified in the response is one the site recognizes
367+ let registeredPasskey: PasskeyModel ;
368+ try {
369+ registeredPasskey = await pseudocodeGetRegisteredPasskey (authResponse .id );
370+ } catch (err ) {
371+ console .error (err );
372+ // Something went wrong, notify the user accordingly
373+ throw new Error (' Unrecognized passkey ID' );
374+ }
375+
376+ // Now try to verify the response
377+ let passkey;
378+ try {
379+ const verification = await verifyAuthenticationResponse (
380+ authOptions ,
381+ authResponse ,
382+ registeredPasskey ,
383+ );
384+ passkey = verification .passkey ;
385+ // User successfully registered a passkey, continue
386+ } catch (err ) {
387+ console .error (err );
388+ // Something went wrong, notify the user accordingly
389+ }
390+ ```
391+
392+ Assuming successful verification, log the user in and update information about the passkey:
393+
394+ ``` ts
395+ // "Log the user in", whatever that looks like for your site
396+ request .session .user = await pseudocodeGetUserForPasskeyID (passkey .id );
397+
398+ // Update `PasskeyModel.counter` and `PasskeyModel.backupStatus` in the database
399+ await pseudocodeUpdatePasskeyRecord (passkey );
400+ ```
401+
402+ The user has now successfully used a passkey to log in.
403+
232404## Third-Party Libraries
233405
234406Many third-party libraries exist to simplify the job of generating options and verifying responses.
0 commit comments