diff --git a/api/package-lock.json b/api/package-lock.json index 4a0d21d..82e3c5f 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.1", "license": "ISC", "dependencies": { - "@simplewebauthn/server": "^10.0.1", + "@simplewebauthn/server": "^11.0.0", "base64url": "^3.0.1", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -22,7 +22,7 @@ "uuid": "^10.0.0" }, "devDependencies": { - "@simplewebauthn/types": "^10.0.0", + "@simplewebauthn/types": "^11.0.0", "@types/cookie-parser": "^1.4.7", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", @@ -161,9 +161,9 @@ } }, "node_modules/@simplewebauthn/server": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-10.0.1.tgz", - "integrity": "sha512-djNWcRn+H+6zvihBFJSpG3fzb0NQS9c/Mw5dYOtZ9H+oDw8qn9Htqxt4cpqRvSOAfwqP7rOvE9rwqVaoGGc3hg==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-11.0.0.tgz", + "integrity": "sha512-zu8dxKcPiRUNSN2kmrnNOzNbRI8VaR/rL4ENCHUfC6PEE7SAAdIql9g5GBOd/wOVZolIsaZz3ccFxuGoVP0iaw==", "license": "MIT", "dependencies": { "@hexagon/base64": "^1.1.27", @@ -173,7 +173,7 @@ "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8", - "@simplewebauthn/types": "^10.0.0", + "@simplewebauthn/types": "^11.0.0", "cross-fetch": "^4.0.0" }, "engines": { @@ -181,9 +181,9 @@ } }, "node_modules/@simplewebauthn/types": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-10.0.0.tgz", - "integrity": "sha512-SFXke7xkgPRowY2E+8djKbdEznTVnD5R6GO7GPTthpHrokLvNKw8C3lFZypTxLI7KkCfGPfhtqB3d7OVGGa9jQ==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-11.0.0.tgz", + "integrity": "sha512-b2o0wC5u2rWts31dTgBkAtSNKGX0cvL6h8QedNsKmj8O4QoLFQFR3DBVBUlpyVEhYKA+mXGUaXbcOc4JdQ3HzA==", "license": "MIT" }, "node_modules/@tsconfig/node10": { diff --git a/api/package.json b/api/package.json index 60cd8a5..15c1bc7 100644 --- a/api/package.json +++ b/api/package.json @@ -7,7 +7,7 @@ "start": "nodemon src/index.ts" }, "dependencies": { - "@simplewebauthn/server": "^10.0.1", + "@simplewebauthn/server": "^11.0.0", "base64url": "^3.0.1", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -20,7 +20,7 @@ "uuid": "^10.0.0" }, "devDependencies": { - "@simplewebauthn/types": "^10.0.0", + "@simplewebauthn/types": "^11.0.0", "@types/cookie-parser": "^1.4.7", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", diff --git a/api/src/account.ts b/api/src/account.ts index 1c5fe2e..615c2f0 100644 --- a/api/src/account.ts +++ b/api/src/account.ts @@ -15,10 +15,10 @@ export const getJwtToken = (user: User | null) => { id: user.id, email: user.email, - devices: user.devices.map((device) => ({ - credentialID: device.credentialID, - name: device.name, - lastUsed: device.lastUsed, + credentials: user.credentials.map((credential) => ({ + id: credential.id, + name: credential.name, + lastUsed: credential.lastUsed, })), } as users.JwtData, JWT_SECRET, @@ -28,7 +28,7 @@ export const getJwtToken = (user: User | null) => export const getAccount = (user: users.JwtData) => ({ email: user.email, - devices: user.devices, + credentials: user.credentials, }); export const sendValidationEmail = async ({ id, email }: User) => { @@ -60,41 +60,41 @@ export const updateAccount = async (user: User, { newEmail }: { newEmail: string const userToUpdate = await users.get(user); userToUpdate.email = newEmail; - userToUpdate.devices = []; + userToUpdate.credentials = []; await users.replace(user, userToUpdate); return { jwtToken: getJwtToken(userToUpdate) }; }; -export const addDeviceGenerateOptions = async (user: User) => +export const addCredentialGenerateOptions = async (user: User) => registrationGenerateOptions(user, await users.get(user)); -export const addDeviceVerify = async ( +export const addCredentialVerify = async ( user: User, registrationBody: RegistrationResponseJSON, - deviceName: string + credentialName: string ) => { - await registrationVerify({ registrationBody, email: user.email }, deviceName, true); + await registrationVerify({ registrationBody, email: user.email }, credentialName, true); return { jwtToken: getJwtToken(await users.get(user)) }; }; -export const renameDevice = async ( +export const renameCredential = async ( user: User, - deviceIndex: string, - { newName }: { credentialID: string; newName: string } + credentialIndex: string, + { newName }: { id: string; newName: string } ) => { const userToUpdate = await users.get(user); - const deviceToUpdate = userToUpdate.devices[Number(deviceIndex)]; - if (!deviceToUpdate) throw new Error('Device not found'); - deviceToUpdate.name = newName; + const credentialToUpdate = userToUpdate.credentials[Number(credentialIndex)]; + if (!credentialToUpdate) throw new Error('Credential not found'); + credentialToUpdate.name = newName; - await users.updateDevice(userToUpdate, deviceToUpdate); + await users.updateCredential(userToUpdate, credentialToUpdate); return { jwtToken: getJwtToken(userToUpdate) }; }; -export const deleteDevice = async (user: User, deviceIndex: string) => { - const updatedUser = await users.removeDevice(user, Number(deviceIndex)); +export const deleteCredential = async (user: User, credentialIndex: string) => { + const updatedUser = await users.removeCredential(user, Number(credentialIndex)); return { jwtToken: getJwtToken(updatedUser) }; }; diff --git a/api/src/db/users.ts b/api/src/db/users.ts index 7dff7c7..4d1f486 100644 --- a/api/src/db/users.ts +++ b/api/src/db/users.ts @@ -1,7 +1,7 @@ -import { AuthenticatorDevice } from '@simplewebauthn/types'; +import { WebAuthnCredential } from '@simplewebauthn/types'; import { convertMongoDbBinaryToBuffer, database } from './index'; -export interface AuthenticatorDeviceDetails extends AuthenticatorDevice { +export interface WebAuthnCredentialDetails extends WebAuthnCredential { name?: string; lastUsed?: number; clientExtensionResults?: AuthenticationExtensionsClientOutputs; @@ -12,7 +12,7 @@ export interface User { * 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. + * addresses, as it may be stored within the passkey. */ id: string; /** @@ -24,14 +24,14 @@ export interface User { validUntil: number; data: string; }; - devices: AuthenticatorDeviceDetails[]; + credentials: WebAuthnCredentialDetails[]; challenge: { validUntil: number; data: string; }; } -export type JwtData = Pick; +export type JwtData = Pick; const users = database.collection('users'); @@ -64,9 +64,9 @@ const convertUser = (user: User | null): User => { return { ...user, - devices: user.devices.map((device) => ({ - ...device, - credentialPublicKey: convertMongoDbBinaryToBuffer(device.credentialPublicKey), + credentials: user.credentials.map((credential) => ({ + ...credential, + publicKey: convertMongoDbBinaryToBuffer(credential.publicKey), })), }; }; @@ -113,19 +113,19 @@ export const getForChallenge = async (user: EmailOrId, requireEmailValidated = t export const replace = async (user: EmailOrId, update: User) => users.findOneAndReplace(byIdOrEmail(user), update); -export const updateDevice = async (user: EmailOrId, device: AuthenticatorDevice) => +export const updateCredential = async (user: EmailOrId, credential: WebAuthnCredential) => users.findOneAndUpdate( - { ...byIdOrEmail(user), 'devices.credentialID': device.credentialID }, + { ...byIdOrEmail(user), 'credentials.id': credential.id }, { $set: { - 'devices.$': device, + 'credentials.$': credential, }, } ); -export const removeDevice = async (user: EmailOrId, deviceIndex: number) => { +export const removeCredential = async (user: EmailOrId, credentialIndex: number) => { const userToUpdate = await get(user); - userToUpdate.devices.splice(deviceIndex, 1); + userToUpdate.credentials.splice(credentialIndex, 1); await replace(user, userToUpdate); return userToUpdate; diff --git a/api/src/index.ts b/api/src/index.ts index a444b4a..c36899a 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -12,17 +12,17 @@ import { registrationGenerateOptions, registrationVerify } from './register'; import { authenticationGenerateOptions, authenticationVerify } from './login'; import { config } from './config'; import { - addDeviceGenerateOptions, - addDeviceVerify, + addCredentialGenerateOptions, + addCredentialVerify, deleteAccount, - deleteDevice, + deleteCredential, emailVerify, getAccount, - renameDevice, + renameCredential, sendValidationEmail, updateAccount, } from './account'; -import { getDeviceNameFromPlatform } from './utils'; +import { getCredentialNameFromPlatform } from './utils'; export const JWT_SECRET = process.env.JWT_SECRET || uuidv4(); @@ -51,7 +51,7 @@ app.post( '/registration/verify', asyncHandler(async (req, res) => { res.send( - await registrationVerify(req.body, getDeviceNameFromPlatform(req.headers['user-agent'])) + await registrationVerify(req.body, getCredentialNameFromPlatform(req.headers['user-agent'])) ); }) ); @@ -126,19 +126,19 @@ app.post( ); app.post( - '/account/add-device/generate-options', + '/account/add-credential/generate-options', asyncHandler(async (req: RequestJwt, res) => { - res.send(await addDeviceGenerateOptions(req.auth)); + res.send(await addCredentialGenerateOptions(req.auth)); }) ); app.post( - '/account/add-device/verify', + '/account/add-credential/verify', asyncHandler(async (req: RequestJwt, res) => { - const updatedUser = await addDeviceVerify( + const updatedUser = await addCredentialVerify( req.auth, req.body.registrationBody, - req.body.deviceName || getDeviceNameFromPlatform(req.headers['user-agent']) + req.body.credentialName || getCredentialNameFromPlatform(req.headers['user-agent']) ); setLoginJwtCookie(res, updatedUser.jwtToken); res.send({ verified: Boolean(updatedUser.jwtToken) }); @@ -146,18 +146,18 @@ app.post( ); app.post( - '/account/device/:id', + '/account/credential/:id', asyncHandler(async (req: RequestJwt, res) => { - const updatedUser = await renameDevice(req.auth, req.params.id, req.body); + const updatedUser = await renameCredential(req.auth, req.params.id, req.body); setLoginJwtCookie(res, updatedUser.jwtToken); res.send(updatedUser); }) ); app.delete( - '/account/device/:id', + '/account/credential/:id', asyncHandler(async (req: RequestJwt, res) => { - const updatedUser = await deleteDevice(req.auth, req.params.id); + const updatedUser = await deleteCredential(req.auth, req.params.id); setLoginJwtCookie(res, updatedUser.jwtToken); res.send(updatedUser); }) diff --git a/api/src/login.ts b/api/src/login.ts index 5a8e0a1..ec48d5a 100644 --- a/api/src/login.ts +++ b/api/src/login.ts @@ -31,9 +31,9 @@ export const authenticationGenerateOptions = async ({ email }: users.User) => { const options = await generateAuthenticationOptions({ timeout, allowCredentials: - user?.devices.map((device) => ({ - id: device.credentialID, - transports: device.transports || [], + user?.credentials.map((credential) => ({ + id: credential.id, + transports: credential.transports || [], })) || [], userVerification, rpID, @@ -41,7 +41,7 @@ export const authenticationGenerateOptions = async ({ email }: users.User) => { /** * The server needs to temporarily remember this value for verification, so don't lose it until - * after you verify an authenticator response. + * after you verify a passkey response. */ if (user) { user.challenge.validUntil = getWebAuthnValidUntil(); @@ -85,17 +85,17 @@ export const authenticationVerify = async ({ throw new Error('Unable to verify login'); } - let dbAuthenticator: users.AuthenticatorDeviceDetails | undefined; - // "Query the DB" here for an authenticator matching `credentialID` - for (const device of user.devices) { - if (device.credentialID === authenticationBody.rawId) { - dbAuthenticator = device; + let dbCredential: users.WebAuthnCredentialDetails | undefined; + // "Query the DB" here for a credential matching `credential.id` + for (const credential of user.credentials) { + if (credential.id === authenticationBody.rawId) { + dbCredential = credential; break; } } - if (!dbAuthenticator) { - throw new Error('Authenticator not found'); + if (!dbCredential) { + throw new Error('Credential not found'); } const verification = await verifyAuthenticationResponse({ @@ -103,22 +103,22 @@ export const authenticationVerify = async ({ expectedChallenge: `${expectedChallenge}`, expectedOrigin: webUrl, expectedRPID: rpID, - authenticator: dbAuthenticator, + credential: dbCredential, requireUserVerification: userVerification === 'required', }); 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; - dbAuthenticator.lastUsed = Date.now(); - await users.updateDevice({ email: user.email }, dbAuthenticator); + // Update the credential's counter in the DB to the newest count in the authentication + dbCredential.counter = authenticationInfo.newCounter; + dbCredential.lastUsed = Date.now(); + await users.updateCredential({ email: user.email }, dbCredential); } return { verified, - clientExtensionResults: dbAuthenticator.clientExtensionResults, + clientExtensionResults: dbCredential.clientExtensionResults, jwtToken: verified ? getJwtToken(user) : null, }; }; diff --git a/api/src/register.ts b/api/src/register.ts index 8def4cd..7927230 100644 --- a/api/src/register.ts +++ b/api/src/register.ts @@ -49,9 +49,9 @@ export const registrationGenerateOptions = async ({ email }: User, existingUser? timeout, attestationType, excludeCredentials: existingUser - ? existingUser.devices.map((device) => ({ - id: device.credentialID, - transports: device.transports || [], + ? existingUser.credentials.map((credential) => ({ + id: credential.id, + transports: credential.transports || [], })) : [], /** @@ -75,7 +75,7 @@ export const registrationGenerateOptions = async ({ email }: User, existingUser? * defaults to 150000ms) * * The server needs to temporarily remember this value for verification, so don't lose it until - * after you verify an authenticator response. + * after you verify a passkey response. */ const challenge = { validUntil: getWebAuthnValidUntil(), @@ -95,7 +95,7 @@ export const registrationGenerateOptions = async ({ email }: User, existingUser? validUntil: tenMinutesFromNow(), data: verificationCode, }, - devices: [], + credentials: [], challenge, }); sendVerificationEmail(email, verificationCode); @@ -112,7 +112,7 @@ export const registrationVerify = async ( email: string; registrationBody: RegistrationResponseJSON; }, - deviceName: string, + credentialName: string, requireEmailValidated = false ) => { const user = await users.getForChallenge({ email }, requireEmailValidated); @@ -130,24 +130,26 @@ export const registrationVerify = async ( const { verified, registrationInfo } = verification; if (verified && registrationInfo) { - const { credentialPublicKey, credentialID, counter } = registrationInfo; + const { + credential: { publicKey, id, counter }, + } = registrationInfo; - const existingDevice = user.devices.find((device) => device.credentialID === credentialID); + const existingCredential = user.credentials.find((credential) => credential.id === id); - if (!existingDevice) { + if (!existingCredential) { /** - * Add the returned device to the user's list of devices + * Add the returned credential to the user's list of credentials */ - const newDevice: users.AuthenticatorDeviceDetails = { - credentialPublicKey, - credentialID, + const newCredential: users.WebAuthnCredentialDetails = { + publicKey, + id, counter, transports: registrationBody.response.transports || [], clientExtensionResults: registrationBody.clientExtensionResults, - name: deviceName, + name: credentialName, lastUsed: Date.now(), }; - user.devices.push(newDevice); + user.credentials.push(newCredential); await users.replace({ email: user.email }, user); } } diff --git a/api/src/utils/index.ts b/api/src/utils/index.ts index 93edb3a..bb78daf 100644 --- a/api/src/utils/index.ts +++ b/api/src/utils/index.ts @@ -7,17 +7,17 @@ export const getWebAuthnValidUntil = () => Date.now() + config.webAuthnOptions.t export const getTenMinutesFromNow = () => Date.now() + 600000; -export const getDeviceNameFromPlatform = (userAgent?: string) => { +export const getCredentialNameFromPlatform = (userAgent?: string) => { const { name, product, os } = parseUserAgent(userAgent); return [name, product, os?.family].filter(Boolean).join(' '); }; -export const sendVerificationEmail = async (email: string, code: string, addDevice = false) => { +export const sendVerificationEmail = async (email: string, code: string, addCredential = false) => { const verificationUrl = `${config.webUrl}/validateEmail.html?code=${code}${ - addDevice ? '®isterDevice' : '' + addCredential ? '®isterCredential' : '' }`; - const message = `To ${addDevice ? 'login to' : 'complete your registration on'} ${ + const message = `To ${addCredential ? 'login to' : 'complete your registration on'} ${ config.webAuthnOptions.rpName } please click the following link @@ -49,7 +49,7 @@ DO NOT share this link with anyone else, if you do they can take over your accou await transporter.sendMail({ from: `${config.webAuthnOptions.rpName} <${process.env.SMTP_FROM}>`, to: email, - subject: `${config.webAuthnOptions.rpName} ${addDevice ? 'login' : 'registration'}`, + subject: `${config.webAuthnOptions.rpName} ${addCredential ? 'login' : 'registration'}`, text: message, }); } catch (err) { diff --git a/web/package-lock.json b/web/package-lock.json index ee02af1..061bda9 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,26 +9,26 @@ "version": "0.0.1", "license": "ISC", "dependencies": { - "@simplewebauthn/browser": "^10.0.0" + "@simplewebauthn/browser": "^11.0.0" }, "devDependencies": { - "@simplewebauthn/types": "^10.0.0", + "@simplewebauthn/types": "^11.0.0", "typescript": "^5.6.3" } }, "node_modules/@simplewebauthn/browser": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-10.0.0.tgz", - "integrity": "sha512-hG0JMZD+LiLUbpQcAjS4d+t4gbprE/dLYop/CkE01ugU/9sKXflxV5s0DRjdz3uNMFecatRfb4ZLG3XvF8m5zg==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-11.0.0.tgz", + "integrity": "sha512-KEGCStrl08QC2I561BzxqGiwoknblP6O1YW7jApdXLPtIqZ+vgJYAv8ssLCdm1wD8HGAHd49CJLkUF8X70x/pg==", "license": "MIT", "dependencies": { - "@simplewebauthn/types": "^10.0.0" + "@simplewebauthn/types": "^11.0.0" } }, "node_modules/@simplewebauthn/types": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-10.0.0.tgz", - "integrity": "sha512-SFXke7xkgPRowY2E+8djKbdEznTVnD5R6GO7GPTthpHrokLvNKw8C3lFZypTxLI7KkCfGPfhtqB3d7OVGGa9jQ==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-11.0.0.tgz", + "integrity": "sha512-b2o0wC5u2rWts31dTgBkAtSNKGX0cvL6h8QedNsKmj8O4QoLFQFR3DBVBUlpyVEhYKA+mXGUaXbcOc4JdQ3HzA==", "license": "MIT" }, "node_modules/typescript": { diff --git a/web/package.json b/web/package.json index b8876c5..2d6804f 100644 --- a/web/package.json +++ b/web/package.json @@ -7,10 +7,10 @@ "dev": "tsc --watch" }, "dependencies": { - "@simplewebauthn/browser": "^10.0.0" + "@simplewebauthn/browser": "^11.0.0" }, "devDependencies": { - "@simplewebauthn/types": "^10.0.0", + "@simplewebauthn/types": "^11.0.0", "typescript": "^5.6.3" }, "repository": { diff --git a/web/src/profile.html b/web/src/profile.html index b8ad94f..51935bd 100644 --- a/web/src/profile.html +++ b/web/src/profile.html @@ -29,11 +29,11 @@

Passkey - Account

- +

-
    +
      -

      🔐 Add new device

      +

      🔐 Add new Passkey

      ⚠️ Delete account

      @@ -41,4 +41,4 @@

      Passkey - Account

      - \ No newline at end of file + diff --git a/web/src/profile.mts b/web/src/profile.mts index 7535b1b..d4832a8 100644 --- a/web/src/profile.mts +++ b/web/src/profile.mts @@ -36,17 +36,17 @@ const getRelativeTimeSince = (pastDate: number) => { return formatter.format(Math.floor(seconds), 'second'); }; -const deviceList = document.querySelector('#deviceList'); -accountDetails.devices?.map( +const credentialList = document.querySelector('#credentialList'); +accountDetails.credentials?.map( ({ name, lastUsed }: { name: string; lastUsed: number }, idx: string) => { - const deviceLi = document.createElement('li'); - deviceLi.id = idx; - deviceLi.innerText = `${name} `; - deviceLi.innerHTML += `Last used ${getRelativeTimeSince(lastUsed)} - ✏️ Rename - ❌ Remove`; - - deviceList?.appendChild(deviceLi); + const credentialLi = document.createElement('li'); + credentialLi.id = idx; + credentialLi.innerText = `${name} `; + credentialLi.innerHTML += `Last used ${getRelativeTimeSince(lastUsed)} + ✏️ Rename + ❌ Remove`; + + credentialList?.appendChild(credentialLi); } ); @@ -67,15 +67,15 @@ const handleRes = (isOk: boolean, action: string) => { } }; -const addDevice = async ({ askForDeviceName = false } = {}) => { +const addCredential = async ({ askForCredentialName = false } = {}) => { try { - const registrationResult = await register({ isExistingUser: true, askForDeviceName }); + const registrationResult = await register({ isExistingUser: true, askForCredentialName }); if (!(registrationResult === 'registered' || registrationResult === 'ok')) { throw new Error(registrationResult); } window.location.reload(); } catch (err) { - alert(`Error adding device ${(err as Error)?.message}`); + alert(`Error adding Passkey ${(err as Error)?.message}`); } }; @@ -112,16 +112,16 @@ form?.addEventListener('submit', async (e) => { return; } - await addDevice(); + await addCredential(); await logout(); }); -document.querySelectorAll('.deviceRename').forEach((el) => +document.querySelectorAll('.credentialRename').forEach((el) => el?.addEventListener('click', async (e) => { const { id } = (e.target as HTMLAnchorElement).parentNode as HTMLLIElement; - const newName = prompt('Rename device to', accountDetails.devices[id].name); + const newName = prompt('Rename Passkey to', accountDetails.credentials[id].name); if (newName) { - const req = await fetch(`${config.apiUrl}/account/device/${id}`, { + const req = await fetch(`${config.apiUrl}/account/credential/${id}`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -135,11 +135,11 @@ document.querySelectorAll('.deviceRename').forEach((el) => }) ); -document.querySelectorAll('.deviceRemove').forEach((el) => +document.querySelectorAll('.credentialRemove').forEach((el) => el?.addEventListener('click', async (e) => { - if (confirm('Are you sure you want to remove this device?') === true) { + if (confirm('Are you sure you want to remove this Passkey?') === true) { const req = await fetch( - `${config.apiUrl}/account/device/${ + `${config.apiUrl}/account/credential/${ ((e.target as HTMLAnchorElement).parentNode as HTMLLIElement)?.id }`, { @@ -153,8 +153,8 @@ document.querySelectorAll('.deviceRemove').forEach((el) => }) ); -document.querySelector('#deviceAdd')?.addEventListener('click', async (e) => { - await addDevice({ askForDeviceName: true }); +document.querySelector('#credentialAdd')?.addEventListener('click', async (e) => { + await addCredential({ askForCredentialName: true }); }); document.querySelector('#deleteAccount')?.addEventListener('click', async (e) => { diff --git a/web/src/register.mts b/web/src/register.mts index c6c1d75..b817164 100644 --- a/web/src/register.mts +++ b/web/src/register.mts @@ -23,7 +23,7 @@ regForm?.addEventListener('submit', async (e) => { if (registrationResult === 'registered' || registrationResult === 'ok') { regForm.style.display = 'none'; feedbackTxt.innerText = `Check your email/console, we've sent you a login link. ${ - registrationResult === 'registered' ? ' Authenticator registered.' : '' + registrationResult === 'registered' ? ' Passkey registered.' : '' }`; } else { feedbackTxt.innerText = registrationResult; diff --git a/web/src/validateEmail.mts b/web/src/validateEmail.mts index 135edea..6a6b7bb 100644 --- a/web/src/validateEmail.mts +++ b/web/src/validateEmail.mts @@ -6,7 +6,7 @@ const feedbackTxt = document.querySelector('#feedbackTxt') as HTMLParagraphEleme const params = new URLSearchParams(window.location.search); -const registerDevice = async () => { +const registerCredential = async () => { (document.querySelector('.alert') as HTMLElement).style.display = 'flex'; try { await register({ isExistingUser: true }); @@ -32,8 +32,8 @@ const validateEmail = async () => { throw new Error(`Failed: ${req.statusText} ${JSON.stringify(res, null, 2)}`); } - if (params.has('registerDevice')) { - await registerDevice(); + if (params.has('registerCredential')) { + await registerCredential(); } (window as Window).location = 'account.html'; } catch (err) { diff --git a/web/src/webauthn/authenticate.mts b/web/src/webauthn/authenticate.mts index 377fc23..f8806d6 100644 --- a/web/src/webauthn/authenticate.mts +++ b/web/src/webauthn/authenticate.mts @@ -40,14 +40,16 @@ export const authenticate = async ({ ...(email && { body: JSON.stringify({ email }) }), }); - const opts = await res.json(); - console.log('Authentication Options', JSON.stringify(opts, null, 2)); + const options = await res.json(); + console.log('Authentication Options', JSON.stringify(options, null, 2)); if (!res.ok) { - throw new AuthenticationError(`Failed: ${res.statusText} ${JSON.stringify(opts, null, 2)}`); + throw new AuthenticationError( + `Failed: ${res.statusText} ${JSON.stringify(options, null, 2)}` + ); } - const asseRes = await startAuthentication(opts, useBrowserAutofill); + const asseRes = await startAuthentication({ optionsJSON: options, useBrowserAutofill }); console.log('Authentication Response', JSON.stringify(asseRes, null, 2)); const verificationRes = await fetch(`${config.apiUrl}/authentication/verify`, { diff --git a/web/src/webauthn/register.mts b/web/src/webauthn/register.mts index 0e5067c..800c657 100644 --- a/web/src/webauthn/register.mts +++ b/web/src/webauthn/register.mts @@ -7,17 +7,17 @@ const { authenticatorAttachment, residentKey } = config.webAuthnOptions; export const register = async ({ email, isExistingUser = false, - askForDeviceName = false, + askForCredentialName = false, }: { email?: string; isExistingUser?: boolean; - askForDeviceName?: boolean; + askForCredentialName?: boolean; } = {}) => { const credentials = isExistingUser ? 'include' : 'same-origin'; try { const generateOptionsUrl = isExistingUser - ? `${config.apiUrl}/account/add-device/generate-options` + ? `${config.apiUrl}/account/add-credential/generate-options` : `${config.apiUrl}/registration/generate-options`; const res = await fetch(generateOptionsUrl, { method: 'POST', @@ -28,21 +28,21 @@ export const register = async ({ credentials, }); - const opts = await res.json(); + const options = await res.json(); if (!res.ok) { - throw new Error(`Failed: ${res.statusText} ${JSON.stringify(opts, null, 2)}`); + throw new Error(`Failed: ${res.statusText} ${JSON.stringify(options, null, 2)}`); } - opts.authenticatorSelection.authenticatorSelection = authenticatorAttachment; - opts.authenticatorSelection.residentKey = residentKey; - opts.authenticatorSelection.requireResidentKey = residentKey === 'required'; - opts.extensions = { + options.authenticatorSelection.authenticatorSelection = authenticatorAttachment; + options.authenticatorSelection.residentKey = residentKey; + options.authenticatorSelection.requireResidentKey = residentKey === 'required'; + options.extensions = { credProps: Boolean(residentKey === 'preferred' || residentKey === 'required'), }; - console.log('Registration Options', JSON.stringify(opts, null, 2)); + console.log('Registration Options', JSON.stringify(options, null, 2)); - const attRes = await startRegistration(opts); + const attRes = await startRegistration({ optionsJSON: options }); console.log('Registration Response', JSON.stringify(attRes, null, 2)); if ( attRes?.authenticatorAttachment === 'platform' || @@ -53,17 +53,17 @@ export const register = async ({ localStorage.removeItem('canLoginWithResidentKey'); } - const deviceName = askForDeviceName ? prompt('Device name') : undefined; + const credentialName = askForCredentialName ? prompt('Passkey name') : undefined; const verificationUrl = isExistingUser - ? `${config.apiUrl}/account/add-device/verify` + ? `${config.apiUrl}/account/add-credential/verify` : `${config.apiUrl}/registration/verify`; const verificationRes = await fetch(verificationUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ email, registrationBody: attRes, deviceName }), + body: JSON.stringify({ email, registrationBody: attRes, credentialName }), credentials, }); @@ -91,7 +91,7 @@ export const register = async ({ ? 'This email address is already registered.' : 'There was an error while trying to create your account.'; } - if (askForDeviceName) { + if (askForCredentialName) { throw err; } else { console.error(err);