UNPKG

passport-magic-code

Version:

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

217 lines (209 loc) 7.41 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { Strategy: () => MagicCodeStrategy }); module.exports = __toCommonJS(index_exports); // src/strategy.ts var import_crypto = require("crypto"); var import_passport_strategy = __toESM(require("passport-strategy")); var import_zod3 = require("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 var import_zod = __toESM(require("zod")); var TokenSchema = import_zod.default.object({ expiresIn: import_zod.default.number(), user: import_zod.default.any() }); var MemoryStorageSchema = import_zod.default.object({ set: import_zod.default.custom(), get: import_zod.default.custom(), delete: import_zod.default.custom(), codes: import_zod.default.record(import_zod.default.string(), TokenSchema).default({}) }); var ArgsSchema = import_zod.default.object({ secret: import_zod.default.string().min(16, { message: "Secret must be at least 16 characters long" }), codeLength: import_zod.default.number().gte(4).default(4), storage: MemoryStorageSchema.default( memoryStorage ), expiresIn: import_zod.default.number().default(30), codeField: import_zod.default.string().default("code"), userPrimaryKey: import_zod.default.string().default("email") }); var OptionsSchema = import_zod.default.object({ action: import_zod.default.enum(["callback", "login", "register"]) }); var SendCodeSchema = import_zod.default.custom(); var CallbackSchema = import_zod.default.custom(); var StrategySchema = import_zod.default.tuple([ ArgsSchema, SendCodeSchema, // SendCodeSchema CallbackSchema // CallbackSchema ]); // src/lib/testUtils.ts var import_zod2 = require("zod"); var TestMemoryStorageSchema = import_zod2.z.object({ set: import_zod2.z.any(), get: import_zod2.z.any(), delete: import_zod2.z.any(), codes: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.any()).default({}) }).default(memoryStorage); var TestArgsSchema = ArgsSchema.extend({ storage: TestMemoryStorageSchema }); var TestStrategySchema = import_zod2.z.tuple([TestArgsSchema, import_zod2.z.any(), import_zod2.z.any()]); // src/types.ts var _ArgsSchema = ArgsSchema.required({ secret: true }); // src/strategy.ts var MagicCodeStrategy = class extends import_passport_strategy.default.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 = import_zod3.z.object({ [this.args.userPrimaryKey]: import_zod3.z.string() }).safeParse(req.body); if (!parsedBody.success) this.fail(parsedBody.error, 400); let user = req.body; const code = (0, import_crypto.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)); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Strategy }); //# sourceMappingURL=index.cjs.map