Skip to content

Commit

Permalink
Update to simplewebauthn 11
Browse files Browse the repository at this point in the history
  • Loading branch information
DigitallyRefined committed Oct 23, 2024
1 parent 2a4f148 commit 7df324a
Show file tree
Hide file tree
Showing 16 changed files with 159 additions and 155 deletions.
18 changes: 9 additions & 9 deletions api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
38 changes: 19 additions & 19 deletions api/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) => {
Expand Down Expand Up @@ -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) };
};
Expand Down
26 changes: 13 additions & 13 deletions api/src/db/users.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
/**
Expand All @@ -24,14 +24,14 @@ export interface User {
validUntil: number;
data: string;
};
devices: AuthenticatorDeviceDetails[];
credentials: WebAuthnCredentialDetails[];
challenge: {
validUntil: number;
data: string;
};
}

export type JwtData = Pick<User, 'id' | 'email' | 'devices'>;
export type JwtData = Pick<User, 'id' | 'email' | 'credentials'>;

const users = database.collection<User>('users');

Expand Down Expand Up @@ -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),
})),
};
};
Expand Down Expand Up @@ -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;
Expand Down
30 changes: 15 additions & 15 deletions api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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']))
);
})
);
Expand Down Expand Up @@ -126,38 +126,38 @@ app.post(
);

app.post(
'/account/add-device/generate-options',
'/account/add-credential/generate-options',
asyncHandler(async (req: RequestJwt<any>, 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<any>, 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) });
})
);

app.post(
'/account/device/:id',
'/account/credential/:id',
asyncHandler(async (req: RequestJwt<any>, 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<any>, 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);
})
Expand Down
34 changes: 17 additions & 17 deletions api/src/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@ 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,
});

/**
* 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();
Expand Down Expand Up @@ -85,40 +85,40 @@ 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({
response: authenticationBody,
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,
};
};
Loading

0 comments on commit 7df324a

Please sign in to comment.