summaryrefslogtreecommitdiffhomepage
path: root/example/index.ts
diff options
context:
space:
mode:
authorMatthew Miller <matthew@millerti.me>2020-11-16 21:21:28 -0800
committerGitHub <noreply@github.com>2020-11-16 21:21:28 -0800
commit0f0a2d9b85ca549f3ebcb8cc4678085779dbf92c (patch)
tree8d03baba70c4969118647c361882c76443b114f9 /example/index.ts
parent79b8188f5a7dab3dc70234e20216cd3b24267c9a (diff)
parent5a05e786ced984f9b41a791cfa7d734d7a5b21e5 (diff)
Merge pull request #71 from MasterKale/feat/port-example-to-ts
feat/port-example-to-ts
Diffstat (limited to 'example/index.ts')
-rw-r--r--example/index.ts267
1 files changed, 267 insertions, 0 deletions
diff --git a/example/index.ts b/example/index.ts
new file mode 100644
index 0000000..b3ecd4d
--- /dev/null
+++ b/example/index.ts
@@ -0,0 +1,267 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+/**
+ * An example Express server showing off a simple integration of @simplewebauthn/server.
+ *
+ * The webpages served from ./public use @simplewebauthn/browser.
+ */
+
+import https from 'https';
+import fs from 'fs';
+
+import express from 'express';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+import {
+ // Registration ("Attestation")
+ generateAttestationOptions,
+ verifyAttestationResponse,
+ // Login ("Assertion")
+ generateAssertionOptions,
+ verifyAssertionResponse,
+} from '@simplewebauthn/server';
+
+import { LoggedInUser } from './example-server';
+
+const app = express();
+const host = '0.0.0.0';
+const port = 443;
+
+const { ENABLE_CONFORMANCE } = process.env;
+
+app.use(express.static('./public/'));
+app.use(express.json());
+
+/**
+ * If the words "metadata statements" mean anything to you, you'll want to enable this route. It
+ * contains an example of a more complex deployment of SimpleWebAuthn with support enabled for the
+ * FIDO Metadata Service. This enables greater control over the types of authenticators that can
+ * interact with the Rely Party (a.k.a. "RP", a.k.a. "this server").
+ */
+if (ENABLE_CONFORMANCE === 'true') {
+ import('./fido-conformance').then(({ fidoRouteSuffix, fidoConformanceRouter }) => {
+ app.use(fidoRouteSuffix, fidoConformanceRouter);
+ });
+}
+
+/**
+ * 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 = 'localhost';
+const expectedOrigin = `https://${rpID}`;
+/**
+ * 2FA and Passwordless WebAuthn flows expect 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 = 'internalUserId';
+
+const inMemoryUserDeviceDB: { [loggedInUserId: string]: LoggedInUser } = {
+ [loggedInUserId]: {
+ id: loggedInUserId,
+ username: `user@${rpID}`,
+ devices: [
+ /**
+ * {
+ * credentialID: string,
+ * publicKey: string,
+ * counter: number,
+ * }
+ */
+ ],
+ /**
+ * 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,
+ devices,
+ } = user;
+
+ const options = generateAttestationOptions({
+ rpName: 'SimpleWebAuthn Example',
+ rpID,
+ userID: loggedInUserId,
+ userName: username,
+ timeout: 60000,
+ attestationType: 'indirect',
+ /**
+ * Passing in a user's list of already-registered authenticator IDs here prevents users from
+ * registering the same device multiple times. The authenticator will simply throw an error in
+ * the browser if it's asked to perform an attestation when one of these ID's already resides
+ * on it.
+ */
+ excludeCredentials: devices.map(dev => ({
+ id: dev.credentialID,
+ type: 'public-key',
+ transports: ['usb', 'ble', 'nfc', 'internal'],
+ })),
+ /**
+ * The optional authenticatorSelection property allows for specifying more constraints around
+ * the types of authenticators that users to can use for attestation
+ */
+ authenticatorSelection: {
+ userVerification: 'preferred',
+ requireResidentKey: false,
+ },
+ });
+
+ /**
+ * The server needs to temporarily remember this value for verification, so don't lose it until
+ * after you verify an authenticator response.
+ */
+ inMemoryUserDeviceDB[loggedInUserId].currentChallenge = options.challenge;
+
+ res.send(options);
+});
+
+app.post('/verify-attestation', async (req, res) => {
+ const { body } = req;
+
+ const user = inMemoryUserDeviceDB[loggedInUserId];
+
+ const expectedChallenge = user.currentChallenge;
+
+ let verification;
+ try {
+ verification = await verifyAttestationResponse({
+ credential: body,
+ expectedChallenge: `${expectedChallenge}`,
+ expectedOrigin,
+ expectedRPID: rpID,
+ });
+ } catch (error) {
+ console.error(error);
+ return res.status(400).send({ error: error.message });
+ }
+
+ const { verified, authenticatorInfo } = verification;
+
+ if (verified && authenticatorInfo) {
+ const { base64PublicKey, base64CredentialID, counter } = authenticatorInfo;
+
+ const existingDevice = user.devices.find(device => device.credentialID === base64CredentialID);
+
+ if (!existingDevice) {
+ /**
+ * Add the returned device to the user's list of devices
+ */
+ user.devices.push({
+ publicKey: base64PublicKey,
+ credentialID: base64CredentialID,
+ counter,
+ });
+ }
+ }
+
+ res.send({ verified });
+});
+
+/**
+ * Login (a.k.a. "Assertion")
+ */
+app.get('/generate-assertion-options', (req, res) => {
+ // You need to know the user by this point
+ const user = inMemoryUserDeviceDB[loggedInUserId];
+
+ const options = generateAssertionOptions({
+ timeout: 60000,
+ allowCredentials: user.devices.map(dev => ({
+ id: dev.credentialID,
+ type: 'public-key',
+ transports: ['usb', 'ble', 'nfc', 'internal'],
+ })),
+ /**
+ * This optional value controls whether or not the authenticator needs be able to uniquely
+ * identify the user interacting with it (via built-in PIN pad, fingerprint scanner, etc...)
+ */
+ userVerification: 'preferred',
+ rpID,
+ });
+
+ /**
+ * The server needs to temporarily remember this value for verification, so don't lose it until
+ * after you verify an authenticator response.
+ */
+ inMemoryUserDeviceDB[loggedInUserId].currentChallenge = options.challenge;
+
+ res.send(options);
+});
+
+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 `credentialID`
+ for (const dev of user.devices) {
+ if (dev.credentialID === body.id) {
+ dbAuthenticator = dev;
+ break;
+ }
+ }
+
+ if (!dbAuthenticator) {
+ throw new Error(`could not find authenticator matching ${body.id}`);
+ }
+
+ let verification;
+ try {
+ verification = verifyAssertionResponse({
+ credential: body,
+ expectedChallenge: `${expectedChallenge}`,
+ expectedOrigin,
+ expectedRPID: rpID,
+ authenticator: dbAuthenticator,
+ });
+ } catch (error) {
+ console.error(error);
+ return res.status(400).send({ error: error.message });
+ }
+
+ const { verified, authenticatorInfo } = verification;
+
+ if (verified) {
+ // Update the authenticator's counter in the DB to the newest count in the assertion
+ dbAuthenticator.counter = authenticatorInfo.counter;
+ }
+
+ res.send({ verified });
+});
+
+https
+ .createServer(
+ {
+ /**
+ * WebAuthn can only be run from https:// URLs. See the README on how to generate this SSL cert and key pair using mkcert
+ */
+ key: fs.readFileSync(`./${rpID}.key`),
+ cert: fs.readFileSync(`./${rpID}.crt`),
+ },
+ app,
+ )
+ .listen(port, host, () => {
+ console.log(`🚀 Server ready at https://${rpID} (${host}:${port})`);
+ });