Skip to content

Commit f014028

Browse files
committed
Add auth opts generation and resp verification
1 parent cf30a19 commit f014028

File tree

1 file changed

+180
-8
lines changed

1 file changed

+180
-8
lines changed

content/en/docs/implementation/backend.md

Lines changed: 180 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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
*/
159162
async 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
196198
const 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
202201
const 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+
205207
let passkey;
206208
try {
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
217219
should 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:
225230
await 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

234406
Many third-party libraries exist to simplify the job of generating options and verifying responses.

0 commit comments

Comments
 (0)