UNPKG

passport-magic-code

Version:

A passwordless passport strategy to send a magic code (One time password) to let the user authenticate themselves.

180 lines (174 loc) 5.24 kB
// src/strategy.ts import { randomInt } from "crypto"; import PassportStrategy from "passport-strategy"; import { z as z3 } from "zod"; // src/lib/lookup.ts var lookup = (objects, key) => { for (let i = 0; i < objects.length; i++) { if (objects[i] && key in objects[i]) { const obj = objects[i]; return obj[key]; } } }; // src/lib/memoryStorage.ts var memoryStorage = { codes: {}, get: async (key) => { return await memoryStorage.codes[key]; }, set: async (key, value) => { return await void (memoryStorage.codes[key] = value); }, delete: async (key) => { return await void delete memoryStorage.codes[key]; } }; // src/lib/schemas.ts import z from "zod"; var TokenSchema = z.object({ expiresIn: z.number(), user: z.any() }); var MemoryStorageSchema = z.object({ set: z.custom(), get: z.custom(), delete: z.custom(), codes: z.record(z.string(), TokenSchema).default({}) }); var ArgsSchema = z.object({ secret: z.string().min(16, { message: "Secret must be at least 16 characters long" }), codeLength: z.number().gte(4).default(4), storage: MemoryStorageSchema.default( memoryStorage ), expiresIn: z.number().default(30), codeField: z.string().default("code"), userPrimaryKey: z.string().default("email") }); var OptionsSchema = z.object({ action: z.enum(["callback", "login", "register"]) }); var SendCodeSchema = z.custom(); var CallbackSchema = z.custom(); var StrategySchema = z.tuple([ ArgsSchema, SendCodeSchema, // SendCodeSchema CallbackSchema // CallbackSchema ]); // src/lib/testUtils.ts import { z as z2 } from "zod"; var TestMemoryStorageSchema = z2.object({ set: z2.any(), get: z2.any(), delete: z2.any(), codes: z2.record(z2.string(), z2.any()).default({}) }).default(memoryStorage); var TestArgsSchema = ArgsSchema.extend({ storage: TestMemoryStorageSchema }); var TestStrategySchema = z2.tuple([TestArgsSchema, z2.any(), z2.any()]); // src/types.ts var _ArgsSchema = ArgsSchema.required({ secret: true }); // src/strategy.ts var MagicCodeStrategy = class extends PassportStrategy.Strategy { name; args; sendCode; callback; constructor(args, sendCode, callback) { const isTest = process.env.NODE_ENV === "test"; const parsedArguments = (isTest ? TestStrategySchema : StrategySchema).safeParse([args, sendCode, callback]); if (!parsedArguments.success) { throw new Error(parsedArguments.error.message); } super(); this.name = "magic-code"; this.args = parsedArguments.data[0]; this.sendCode = parsedArguments.data[1]; this.callback = parsedArguments.data[2]; } /* passport-strategy does not expect new express Request type from ^5.0.0 */ /* @ts-ignore */ async authenticate(req, options) { const parsedOptions = OptionsSchema.safeParse(options); if (!parsedOptions.success) { throw new Error(parsedOptions.error.message); } options = parsedOptions.data; if (options.action === "callback") { return this.acceptCode(req, options); } if (options.action === "register" || options.action === "login") { return this.requestCode(req, options); } return this.error(new Error("Unknown action")); } async requestCode(req, options) { const parsedBody = z3.object({ [this.args.userPrimaryKey]: z3.string() }).safeParse(req.body); if (!parsedBody.success) this.fail(parsedBody.error, 400); let user = req.body; const code = randomInt( 10 ** (this.args.codeLength - 1), 10 ** this.args.codeLength - 1 ); const error = await this.sendCode(user, code, options); if (error) { throw error; } await this.args.storage.set(code.toString(), { expiresIn: (Date.now() + this.args.expiresIn * 60 * 1e3) / 1, user: options.action === "register" ? user : { [this.args.userPrimaryKey]: user[this.args.userPrimaryKey] } }); this.pass(); return user; } async acceptCode(req, options) { const code = lookup( [req.body, req.query, req.params], this.args.codeField )?.toString(); const userUID = lookup( [req.body, req.query, req.params], this.args.userPrimaryKey )?.toString(); if (!code) { throw { error: `Missing field: ${this.args.codeField}`, message: `The code field (${this.args.codeField}) is missing.`, statusCode: 400 }; } if (!userUID) { throw { error: `Missing field: ${this.args.userPrimaryKey}`, message: `The primary key (${this.args.userPrimaryKey}) is missing.`, statusCode: 400 }; } const token = await this.args.storage.get(code); if (!token || !(this.args.userPrimaryKey in token?.user) || !token?.user[this.args.userPrimaryKey] || token?.user[this.args.userPrimaryKey] !== userUID || token?.expiresIn <= Date.now()) { throw { error: "Invalid code", message: "Code does not exist, is already used or is expired.", statusCode: 400 }; } await this.args.storage.delete(code); return this.success(await this.callback(token.user, options)); } }; export { MagicCodeStrategy as Strategy }; //# sourceMappingURL=index.mjs.map