Some important concepts:
You normally do one of the three:
There are some security risks to be aware of:
A good website to use for "have I been pwned".
From most-used to least:
It also mentions passkeys coming into the fray and it being powerful for passwordless.
It's also mentioned that the difference between 3 and 4 will be covered later, but with (3) the passkeys are stored on device and not multi-device, whereas (4) will be stored on the cloud with the larger companies (i.e. iCloud for Apple, etc).
I skipped most of this section since I believe it would just cover basic login process with username and password.
This API let's up save and retrieve data in the browser's password manager. It can store:
It let's us implement auto login safely.
For credentials, it's Chromium-only. This is very important for feature detection.
Some super useful links related to this section that I've found:
For post login, you can save the credentials:
// Credential Management API if (window.PasswordCredential && user.password) { const credential = new PasswordCredential({ name: user.name, id: user.email, password: user.password }); navigator.credentials.store(credential); }
When logging out, we should also add this:
if (window.PasswordCredential) { navigator.credentials.preventSilentAccess() }
This means it will prevent an auto-login next visit. Safari may be missing this at the time of this course (2022 maybe).
This enables auto-logins:
if (window.PasswordCredential) { const credentials = await navigator.credentials.get({ password: true }); try { document.getElementById("login_email").value = credentials.id; document.getElementById("login_password").value = credentials.password; Auth.login(); } catch (e) {} }
In the above example, you still need to submit the login.
I've also skipped over this part as I am familiar with enough with it.
This party covers the relationship between the website, user and authenticator.
You have three options, but you can use IDaaS.
For libraries, the recommendation is https://simplewebauthn.dev/
There is a also a website to demo webauthn https://webauthn.io/
With the demo, it showed how different browsers respond to the request the register.
Find more here https://firtman.github.io/authentication/lessons/using-webauthn/setting-up-endpoints
Adding in the 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 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 } }); });
addWebAuthn: async () => { const options = await API.webAuthn.registrationOptions(); options.authenticatorSelection.residentKey = 'required'; options.authenticatorSelection.requireResidentKey = true; options.extensions = { credProps: true, }; const authRes = await SimpleWebAuthnBrowser.startRegistration(options); const verificationRes = await API.webAuthn.registrationVerification(authRes); if (verificationRes.ok) { alert("You can now login using the registered method!"); } else { alert(verificationRes.message) } },
webAuthnLogin: async (optional) => { const email = document.getElementById("login_email").value; const options = await API.webAuthn.loginOptions(email); const loginRes = await SimpleWebAuthnBrowser.startAuthentication(options); const verificationRes = await API.webAuthn.loginVerification(email, loginRes); if (verificationRes) { Auth.postLogin(verificationRes, verificationRes.user); } else { alert(verificationRes.message) } }
Passwordless options:
"It's the new DNA of WebAuthn". The idea is to use WebAuthn as first factor auth
Authenticators are saving the keys in the cloud, so you can use them on different devices.
https://passkeys.io
This is some recommendations if you wanted to continue on with the project from the the course https://firtman.github.io/authentication/lessons/bonus/next-steps