summaryrefslogtreecommitdiffhomepage
path: root/example/index.js
diff options
context:
space:
mode:
Diffstat (limited to 'example/index.js')
-rw-r--r--example/index.js166
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({