diff options
Diffstat (limited to 'example/index.js')
-rw-r--r-- | example/index.js | 166 |
1 files changed, 119 insertions, 47 deletions
diff --git a/example/index.js b/example/index.js index 25f343f..1f1c0b8 100644 --- a/example/index.js +++ b/example/index.js @@ -24,47 +24,100 @@ const port = 443; app.use(express.static('./public/')); app.use(express.json()); -// Domain where the WebAuthn interactions are expected to occur -const origin = 'dev.dontneeda.pw'; -// GENERATE A NEW VALUE FOR THIS EVERY TIME! The server needs to temporarily remember this value, -// so don't lose it until after you verify -const randomChallenge = 'totallyUniqueValueEveryTime'; -// Your internal, _unique_ ID for the user (uuid, etc...). Avoid using identifying information here, -// like an email address -const userId = 'webauthntineInternalUserId'; -// A username for the user -const username = 'user@webauthntine.foo'; +/** + * RP ID represents the "scope" of websites on which a authenticator should be usable. The Origin + * represents the expected URL from which an attestation or assertion occurs. + */ +const rpID = 'dev.yourdomain.com'; +const origin = `https://${rpID}`; +/** + * WebAuthn expects you to be able to uniquely identify the user that performs an attestation or + * assertion. The user ID you specify here should be your internal, _unique_ ID for that user + * (uuid, etc...). Avoid using identifying information here, like email addresses, as it may be + * stored within the authenticator. + * + * Here, the example server assumes the following user has completed login: + */ +const loggedInUserId = 'webauthntineInternalUserId'; +/** + * You'll need a database to store a few things: + * + * 1. Users + * + * You'll need to be able to associate attestation and assertions challenges, and authenticators to + * a specific user + * + * 2. Challenges + * + * The totally-random-unique-every-time values you pass into every execution of + * `generateAttestationOptions()` or `generateAssertionOptions()` MUST be stored until + * `verifyAttestationResponse()` or `verifyAssertionResponse()` (respectively) is called to verify + * that the response contains the signed challenge. + * + * These values only need to be persisted for `timeout` number of milliseconds (see the `generate` + * methods and their optional `timeout` parameter) + * + * 3. Authenticator Devices + * + * After an attestation, you'll need to store three things about the authenticator: + * + * - Base64-encoded "Credential ID" (varchar) + * - Base64-encoded "Public Key" (varchar) + * - Counter (int) + * + * Each authenticator must also be associated to a user so that you can generate a list of + * authenticator credential IDs to pass into `generateAssertionOptions()`, from which one is + * expected to generate an assertion response. + */ const inMemoryUserDeviceDB = { - [userId]: [ + [loggedInUserId]: { + id: loggedInUserId, + username: 'user@yourdomain.com', + devices: [ + /** + * { + * base64CredentialID: string, + * base64PublicKey: string, + * counter: number, + * } + */ + ], /** - * After an attestation, the following authenticator info returned by - * verifyAttestationResponse() should be persisted somewhere that'll tie it back to the user - * specified during attestation: - * - * { - * base64PublicKey: string, - * base64CredentialID: string, - * counter: number, - * } - * - * After an assertion, the `counter` value above should be updated to the value returned by - * verifyAssertionResponse(). This method will also return a credential ID of the device that - * needs to have its `counter` value updated. - * + * A simple way of storing a user's current challenge being signed by attestation or assertion. + * It should be expired after `timeout` milliseconds (optional argument for `generate` methods, + * defaults to 60000ms) */ - ], + currentChallenge: undefined, + }, }; /** * Registration (a.k.a. "Attestation") */ app.get('/generate-attestation-options', (req, res) => { + const user = inMemoryUserDeviceDB[loggedInUserId]; + + const { + /** + * The username can be a human-readable name, email, etc... as it is intended only for display. + */ + username, + } = user; + + /** + * A new, random value needs to be generated every time an attestation is performed! + * The server needs to temporarily remember this value for verification, so don't lose it until + * after you verify an authenticator response. + */ + const challenge = 'totallyUniqueValueEveryAttestation'; + inMemoryUserDeviceDB[loggedInUserId].currentChallenge = challenge; + res.send(generateAttestationOptions( 'WebAuthntine Example', - origin, - randomChallenge, - userId, + rpID, + challenge, + loggedInUserId, username, )); }); @@ -72,12 +125,16 @@ app.get('/generate-attestation-options', (req, res) => { app.post('/verify-attestation', (req, res) => { const { body } = req; + const user = inMemoryUserDeviceDB[loggedInUserId]; + + const expectedChallenge = user.currentChallenge; + let verification; try { verification = verifyAttestationResponse( body, - randomChallenge, - `https://${origin}`, + expectedChallenge, + origin, ); } catch (error) { console.error(error); @@ -88,11 +145,16 @@ app.post('/verify-attestation', (req, res) => { if (verified) { const { base64PublicKey, base64CredentialID, counter } = authenticatorInfo; - const user = inMemoryUserDeviceDB[userId]; - const existingDevice = user.find((device) => device.base64CredentialID === base64CredentialID); + + const existingDevice = user.devices.find( + (device) => device.base64CredentialID === base64CredentialID, + ); if (!existingDevice) { - inMemoryUserDeviceDB[userId].push({ + /** + * Add the returned device to the user's list of devices + */ + user.devices.push({ base64PublicKey, base64CredentialID, counter, @@ -108,34 +170,44 @@ app.post('/verify-attestation', (req, res) => { */ app.get('/generate-assertion-options', (req, res) => { // You need to know the user by this point - const user = inMemoryUserDeviceDB[userId]; + const user = inMemoryUserDeviceDB[loggedInUserId]; + + /** + * A new, random value needs to be generated every time an assertion is performed! + * The server needs to temporarily remember this value for verification, so don't lose it until + * after you verify an authenticator response. + */ + const challenge = 'totallyUniqueValueEveryAssertion'; + inMemoryUserDeviceDB[loggedInUserId].currentChallenge = challenge; res.send(generateAssertionOptions( - randomChallenge, - user.map(data => data.base64CredentialID), + challenge, + user.devices.map(data => data.base64CredentialID), )); }); app.post('/verify-assertion', (req, res) => { const { body } = req; + const user = inMemoryUserDeviceDB[loggedInUserId]; + + const expectedChallenge = user.currentChallenge; + let dbAuthenticator; // "Query the DB" here for an authenticator matching `base64CredentialID` - Object.values(inMemoryUserDeviceDB).forEach((userDevs) => { - for(let dev of userDevs) { - if (dev.base64CredentialID === body.base64CredentialID) { - dbAuthenticator = dev; - return; - } + for(let dev of user.devices) { + if (dev.base64CredentialID === body.base64CredentialID) { + dbAuthenticator = dev; + break; } - }); + } let verification; try { verification = verifyAssertionResponse( body, - randomChallenge, - `https://${origin}`, + expectedChallenge, + origin, dbAuthenticator, ); } catch (error) { @@ -150,7 +222,7 @@ app.post('/verify-assertion', (req, res) => { dbAuthenticator.counter = authenticatorInfo.counter; } - res.send({ verified }) + res.send({ verified }); }); https.createServer({ |