/* 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 http from 'http'; import fs from 'fs'; import express from 'express'; import session from 'express-session'; import memoryStore from 'memorystore'; import dotenv from 'dotenv'; import base64url from 'base64url'; dotenv.config(); import { // Registration generateRegistrationOptions, verifyRegistrationResponse, // Authentication generateAuthenticationOptions, verifyAuthenticationResponse, } from '@simplewebauthn/server'; import { isoUint8Array } from '@simplewebauthn/server/helpers'; import type { GenerateRegistrationOptionsOpts, GenerateAuthenticationOptionsOpts, VerifyRegistrationResponseOpts, VerifyAuthenticationResponseOpts, VerifiedRegistrationResponse, VerifiedAuthenticationResponse, } from '@simplewebauthn/server'; import type { RegistrationResponseJSON, AuthenticationResponseJSON, AuthenticatorDevice, } from '@simplewebauthn/typescript-types'; import { LoggedInUser } from './example-server'; const app = express(); const MemoryStore = memoryStore(session); const { ENABLE_CONFORMANCE, ENABLE_HTTPS, RP_ID = 'localhost', } = process.env; app.use(express.static('./public/')); app.use(express.json()); app.use( session({ secret: 'secret123', saveUninitialized: true, resave: false, cookie: { maxAge: 86400000, httpOnly: true, // Ensure to not expose session cookies to clientside scripts }, store: new MemoryStore({ checkPeriod: 86_400_000, // prune expired entries every 24h }), }), ); /** * 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 registration or authentication occurs. */ export const rpID = RP_ID; // This value is set at the bottom of page as part of server initialization (the empty string is // to appease TypeScript until we determine the expected origin based on whether or not HTTPS // support is enabled) export let expectedOrigin = ''; /** * 2FA and Passwordless WebAuthn flows expect you to be able to uniquely identify the user that * performs registration or authentication. 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: [], }, }; /** * Registration (a.k.a. "Registration") */ app.get('/generate-registration-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 opts: GenerateRegistrationOptionsOpts = { rpName: 'SimpleWebAuthn Example', rpID, userID: loggedInUserId, userName: username, timeout: 60000, attestationType: 'none', /** * 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 registration when one of these ID's already resides * on it. */ excludeCredentials: devices.map(dev => ({ id: dev.credentialID, type: 'public-key', transports: dev.transports, })), authenticatorSelection: { residentKey: 'discouraged', }, /** * Support the two most common algorithms: ES256, and RS256 */ supportedAlgorithmIDs: [-7, -257], }; const options = generateRegistrationOptions(opts); /** * The server needs to temporarily remember this value for verification, so don't lose it until * after you verify an authenticator response. */ req.session.currentChallenge = options.challenge; res.send(options); }); app.post('/verify-registration', async (req, res) => { const body: RegistrationResponseJSON = req.body; const user = inMemoryUserDeviceDB[loggedInUserId]; const expectedChallenge = req.session.currentChallenge; let verification: VerifiedRegistrationResponse; try { const opts: VerifyRegistrationResponseOpts = { response: body, expectedChallenge: `${expectedChallenge}`, expectedOrigin, expectedRPID: rpID, requireUserVerification: true, }; verification = await verifyRegistrationResponse(opts); } catch (error) { const _error = error as Error; console.error(_error); return res.status(400).send({ error: _error.message }); } const { verified, registrationInfo } = verification; if (verified && registrationInfo) { const { credentialPublicKey, credentialID, counter } = registrationInfo; const existingDevice = user.devices.find(device => isoUint8Array.areEqual(device.credentialID, credentialID)); if (!existingDevice) { /** * Add the returned device to the user's list of devices */ const newDevice: AuthenticatorDevice = { credentialPublicKey, credentialID, counter, transports: body.response.transports, }; user.devices.push(newDevice); } } req.session.currentChallenge = undefined; res.send({ verified }); }); /** * Login (a.k.a. "Authentication") */ app.get('/generate-authentication-options', (req, res) => { // You need to know the user by this point const user = inMemoryUserDeviceDB[loggedInUserId]; const opts: GenerateAuthenticationOptionsOpts = { timeout: 60000, allowCredentials: user.devices.map(dev => ({ id: dev.credentialID, type: 'public-key', transports: dev.transports, })), userVerification: 'required', rpID, }; const options = generateAuthenticationOptions(opts); /** * The server needs to temporarily remember this value for verification, so don't lose it until * after you verify an authenticator response. */ req.session.currentChallenge = options.challenge; res.send(options); }); app.post('/verify-authentication', async (req, res) => { const body: AuthenticationResponseJSON = req.body; const user = inMemoryUserDeviceDB[loggedInUserId]; const expectedChallenge = req.session.currentChallenge; let dbAuthenticator; const bodyCredIDBuffer = base64url.toBuffer(body.rawId); // "Query the DB" here for an authenticator matching `credentialID` for (const dev of user.devices) { if (isoUint8Array.areEqual(dev.credentialID, bodyCredIDBuffer)) { dbAuthenticator = dev; break; } } if (!dbAuthenticator) { return res.status(400).send({ error: 'Authenticator is not registered with this site' }); } let verification: VerifiedAuthenticationResponse; try { const opts: VerifyAuthenticationResponseOpts = { response: body, expectedChallenge: `${expectedChallenge}`, expectedOrigin, expectedRPID: rpID, authenticator: dbAuthenticator, requireUserVerification: true, }; verification = await verifyAuthenticationResponse(opts); } catch (error) { const _error = error as Error; console.error(_error); return res.status(400).send({ error: _error.message }); } const { verified, authenticationInfo } = verification; if (verified) { // Update the authenticator's counter in the DB to the newest count in the authentication dbAuthenticator.counter = authenticationInfo.newCounter; } req.session.currentChallenge = undefined; res.send({ verified }); }); if (ENABLE_HTTPS) { const host = '0.0.0.0'; const port = 443; expectedOrigin = `https://${rpID}`; https .createServer( { /** * 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 ${expectedOrigin} (${host}:${port})`); }); } else { const host = '127.0.0.1'; const port = 8000; expectedOrigin = `http://localhost:${port}`; http.createServer(app).listen(port, host, () => { console.log(`🚀 Server ready at ${expectedOrigin} (${host}:${port})`); }); }