update webauthn flow

This commit is contained in:
2025-02-27 16:34:20 +05:30
parent 0b9f941cb3
commit 63bcc4bafd
5 changed files with 45 additions and 45 deletions

View File

@@ -19,6 +19,7 @@ import { noteSchemas } from "./note/note.schema";
import { webAuthnRoutes } from "./webauthn/webauthn.route"; import { webAuthnRoutes } from "./webauthn/webauthn.route";
import { configSchemas } from "./config/config.schema"; import { configSchemas } from "./config/config.schema";
import { mailSchemas } from "./mailProxy/mailProxy.schema"; import { mailSchemas } from "./mailProxy/mailProxy.schema";
import { viewSchemas } from "./view/view.schema";
const app = fastify({ logger: true, trustProxy: true }); const app = fastify({ logger: true, trustProxy: true });
@@ -51,6 +52,7 @@ for (const schema of [
...noteSchemas, ...noteSchemas,
...configSchemas, ...configSchemas,
...mailSchemas, ...mailSchemas,
...viewSchemas,
]) { ]) {
app.addSchema(schema); app.addSchema(schema);
} }

View File

@@ -36,7 +36,7 @@ const userSchema = new mongoose.Schema({
}, },
{ _id: false } { _id: false }
), ),
otp: new mongoose.Schema( token: new mongoose.Schema(
{ {
value: String, value: String,
expiry: Date, expiry: Date,
@@ -61,7 +61,7 @@ const userCore = {
invalid_type_error: "Email must be a valid string", invalid_type_error: "Email must be a valid string",
}) })
.email(), .email(),
avatar: z.string().url().optional(), avatar: z.string().optional(),
role: z.enum(roles), role: z.enum(roles),
orgId: z.string().optional(), orgId: z.string().optional(),
}; };

View File

@@ -1,16 +1,31 @@
import mongoose from "mongoose"; import mongoose from "mongoose";
import { generateId } from "../utils/id"; import { generateId, generateToken } from "../utils/id";
import { CreateUserInput, UpdateUserInput, userModel } from "./user.schema"; import { CreateUserInput, UpdateUserInput, userModel } from "./user.schema";
import { sendMail } from "../utils/mail";
export async function createUser(input: CreateUserInput, tenantId: string) { export async function createUser(input: CreateUserInput, tenantId: string) {
const token = await generateToken();
const user = await userModel.create({ const user = await userModel.create({
tenantId: tenantId, tenantId: tenantId,
pid: generateId(), pid: generateId(),
name: input.firstName + " " + input.lastName, name: input.firstName + " " + input.lastName,
createdAt: new Date(), createdAt: new Date(),
token: {
value: token,
expiry: new Date(Date.now() + 3600 * 6 * 1000),
},
...input, ...input,
}); });
const sent = await sendMail(
input.email,
"You have been invited to Quicker Permtis.",
`Click <a href="${
process.env.SERVER_DOMAIN + "/auth/webauthn/regiester?token=" + token
}">here</a> to register.`
);
return user; return user;
} }
@@ -24,6 +39,10 @@ export async function getUser(userId: string) {
}); });
} }
export async function getUserByToken(token: string) {
return await userModel.findOne({ "token.value": token });
}
export async function getUserByEmail(email: string) { export async function getUserByEmail(email: string) {
return await userModel.findOne({ email: email }); return await userModel.findOne({ email: email });
} }

View File

@@ -45,7 +45,7 @@ export async function sendMail(
toAddress: to, toAddress: to,
subject, subject,
content: body, content: body,
mailFormat: "plaintext", mailFormat: "html",
}, },
}); });
} catch (err) { } catch (err) {

View File

@@ -8,10 +8,8 @@ import {
verifyRegistrationResponse, verifyRegistrationResponse,
} from "@simplewebauthn/server"; } from "@simplewebauthn/server";
import { isoUint8Array } from "@simplewebauthn/server/helpers"; import { isoUint8Array } from "@simplewebauthn/server/helpers";
import { getUserByEmail } from "../user/user.service"; import { getUserByEmail, getUserByToken } from "../user/user.service";
import { createSession } from "../auth/auth.service"; import { createSession } from "../auth/auth.service";
import { generateOTP } from "../utils/id";
import { sendMail } from "../utils/mail";
const rpID = process.env.SERVER_DOMAIN; const rpID = process.env.SERVER_DOMAIN;
const rpName = "Quicker Permits"; const rpName = "Quicker Permits";
@@ -19,36 +17,36 @@ const origin = `https://${rpID}`;
export async function webAuthnRoutes(fastify: FastifyInstance) { export async function webAuthnRoutes(fastify: FastifyInstance) {
// Registration request // Registration request
fastify.post<{ Body: { email: string } }>( fastify.post<{ Body: { token: string } }>(
"/register/request", "/register/request",
{ {
schema: { schema: {
body: { body: {
type: "object", type: "object",
properties: { properties: {
email: { type: "string" }, token: { type: "string" },
}, },
}, },
}, },
}, },
async (req, res: FastifyReply) => { async (req, res: FastifyReply) => {
const { email } = req.body; const { token } = req.body;
if (!email) { if (!token) {
return res.code(400).send({ error: "Email is required" }); return res.code(400).send({ error: "bad request" });
} }
const userInDB = await getUserByEmail(email); const userInDB = await getUserByToken(token);
if (!userInDB) return res.code(400).send({ error: "not allowed" }); if (!userInDB) return res.code(400).send({ error: "bad request" });
if (new Date() > userInDB.token.expiry)
const otp = generateOTP(); return res.code(400).send({ error: "link expired" });
const userId = userInDB.id; const userId = userInDB.id;
const options = await generateRegistrationOptions({ const options = await generateRegistrationOptions({
rpName, rpName,
rpID, rpID,
userID: isoUint8Array.fromUTF8String(userId), userID: isoUint8Array.fromUTF8String(userId),
userName: email, userName: userInDB.email,
attestationType: "none", attestationType: "none",
excludeCredentials: userInDB.passKeys.map((cred) => ({ excludeCredentials: userInDB.passKeys.map((cred) => ({
// @ts-ignore // @ts-ignore
@@ -61,23 +59,11 @@ export async function webAuthnRoutes(fastify: FastifyInstance) {
userInDB.challenge = { userInDB.challenge = {
value: options.challenge, value: options.challenge,
expiry: new Date(Date.now() + 60 * 5 * 1000), expiry: new Date(Date.now() + 60 * 10 * 1000),
};
userInDB.otp = {
value: otp,
expiry: new Date(Date.now() + 60 * 5 * 1000),
}; };
await userInDB.save(); await userInDB.save();
const sent = await sendMail(
email,
"Code for Quicker Permits Authentication",
`Your code is ${otp}`
);
if (!sent) return res.code(500).send({ error: "server error" });
return res.send(options); return res.send(options);
} }
); );
@@ -85,8 +71,7 @@ export async function webAuthnRoutes(fastify: FastifyInstance) {
// Registration verification // Registration verification
fastify.post<{ fastify.post<{
Body: { Body: {
email: string; token: string;
code: string;
attestationResponse: RegistrationResponseJSON; attestationResponse: RegistrationResponseJSON;
}; };
}>( }>(
@@ -96,31 +81,25 @@ export async function webAuthnRoutes(fastify: FastifyInstance) {
body: { body: {
type: "object", type: "object",
properties: { properties: {
email: { type: "string" }, token: { type: "string" },
code: { type: "string" },
attestationResponse: { type: "object", additionalProperties: true }, attestationResponse: { type: "object", additionalProperties: true },
}, },
}, },
}, },
}, },
async (req, res: FastifyReply) => { async (req, res: FastifyReply) => {
const { email, code, attestationResponse } = req.body; const { token, attestationResponse } = req.body;
const userInDB = await getUserByEmail(email); const userInDB = await getUserByToken(token);
if (!userInDB) return res.code(400).send({ error: "not allowed" }); if (!userInDB) return res.code(400).send({ error: "bad request" });
const challenge = userInDB.challenge; const challenge = userInDB.challenge;
const requiredOTP = userInDB.otp; const tokenInDb = userInDB.token;
if (!challenge) return res.code(400).send({ error: "not allowed" });
if (!challenge || !requiredOTP) if (new Date() > challenge.expiry || new Date() > tokenInDb.expiry)
return res.code(400).send({ error: "not allowed" });
if (new Date() > challenge.expiry || new Date() > requiredOTP.expiry)
return res.code(400).send({ error: "challenge expired" }); return res.code(400).send({ error: "challenge expired" });
if (code != requiredOTP.value)
return res.code(400).send({ error: "invalid code" });
try { try {
const verification = await verifyRegistrationResponse({ const verification = await verifyRegistrationResponse({
response: attestationResponse, response: attestationResponse,