Setting Up endpoints for WebAuthn
Now, the largest part: implementing WebAuthn in our server
Registration endpoints
app.post("/auth/webauth-registration-options", (req, res) =>{
const user = findUser(req.body.email);
const options = {
rpName: 'Coffee Masters',
rpID,
userID: user.email,
userName: user.name,
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: user.devices ? user.devices.map(dev => ({
id: dev.credentialID,
type: 'public-key',
transports: dev.transports,
})) : [],
authenticatorSelection: {
userVerification: 'required',
residentKey: 'required',
},
/**
* The two most common algorithms: ES256, and RS256
*/
supportedAlgorithmIDs: [-7, -257],
};
/**
* The server needs to temporarily remember this value for verification, so don't lose it until
* after you verify an authenticator response.
*/
const regOptions = SimpleWebAuthnServer.generateRegistrationOptions(options)
user.currentChallenge = regOptions.challenge;
db.write();
res.send(regOptions);
});
app.post("/auth/webauth-registration-verification", async (req, res) => {
const user = findUser(req.body.user.email);
const data = req.body.data;
const expectedChallenge = user.currentChallenge;
let verification;
try {
const options = {
credential: data,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin,
expectedRPID: rpID,
requireUserVerification: true,
};
verification = await SimpleWebAuthnServer.verifyRegistrationResponse(options);
} catch (error) {
console.log(error);
return res.status(400).send({ error: error.toString() });
}
const { verified, registrationInfo } = verification;
if (verified && registrationInfo) {
const { credentialPublicKey, credentialID, counter } = registrationInfo;
const existingDevice = user.devices ? user.devices.find(
device => new Buffer(device.credentialID.data).equals(credentialID)
) : false;
if (!existingDevice) {
const newDevice = {
credentialPublicKey,
credentialID,
counter,
transports: data.response.transports,
};
if (user.devices==undefined) {
user.devices = [];
}
user.webauthn = true;
user.devices.push(newDevice);
db.write();
}
}
res.send({ ok: true });
});
Login/Authentication Endpoints
app.post("/auth/webauth-login-options", (req, res) =>{
const user = findUser(req.body.email);
// if (user==null) {
// res.sendStatus(404);
// return;
// }
const options = {
timeout: 60000,
allowCredentials: [],
devices: user && user.devices ? user.devices.map(dev => ({
id: dev.credentialID,
type: 'public-key',
transports: dev.transports,
})) : [],
userVerification: 'required',
rpID,
};
const loginOpts = SimpleWebAuthnServer.generateAuthenticationOptions(options);
if (user) user.currentChallenge = loginOpts.challenge;
res.send(loginOpts);
});
app.post("/auth/webauth-login-verification", async (req, res) => {
const data = req.body.data;
const user = findUser(req.body.email);
if (user==null) {
res.sendStatus(400).send({ok: false});
return;
}
const expectedChallenge = user.currentChallenge;
let dbAuthenticator;
const bodyCredIDBuffer = base64url.toBuffer(data.rawId);
for (const dev of user.devices) {
const currentCredential = Buffer(dev.credentialID.data);
if (bodyCredIDBuffer.equals(currentCredential)) {
dbAuthenticator = dev;
break;
}
}
if (!dbAuthenticator) {
return res.status(400).send({ ok: false, message: 'Authenticator is not registered with this site' });
}
let verification;
try {
const options = {
credential: data,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin,
expectedRPID: rpID,
authenticator: {
...dbAuthenticator,
credentialPublicKey: new Buffer(dbAuthenticator.credentialPublicKey.data) // Re-convert to Buffer from JSON
},
requireUserVerification: true,
};
verification = await SimpleWebAuthnServer.verifyAuthenticationResponse(options);
} catch (error) {
return res.status(400).send({ ok: false, message: error.toString() });
}
const { verified, authenticationInfo } = verification;
if (verified) {
dbAuthenticator.counter = authenticationInfo.newCounter;
}
res.send({
ok: true,
user: {
name: user.name,
email: user.email
}
});
});
Finally, we add these endpoints to API.js
webAuthn: {
loginOptions: async (email) => {
return await API.makePostRequest(API.endpoint + "webauth-login-options", { email });
},
loginVerification: async (email, data) => {
return await API.makePostRequest(API.endpoint + "webauth-login-verification", {
email,
data
});
},
registrationOptions: async () => {
return await API.makePostRequest(API.endpoint + "webauth-registration-options", Auth.account);
},
registrationVerification: async (data) => {
return await API.makePostRequest(API.endpoint + "webauth-registration-verification", {
user: Auth.account,
data
});
}
},