passport-magic-code
Version:
A passwordless passport strategy to send a magic code (One time password) to let the user authenticate themselves.
1 lines • 11.9 kB
Source Map (JSON)
{"version":3,"sources":["../src/strategy.ts","../src/lib/lookup.ts","../src/lib/memoryStorage.ts","../src/lib/schemas.ts","../src/lib/testUtils.ts","../src/types.ts"],"sourcesContent":["import { randomInt } from \"crypto\";\nimport { Request } from \"express\";\nimport PassportStrategy from \"passport-strategy\";\n\nimport { z } from \"zod\";\nimport { lookup, OptionsSchema, StrategySchema } from \"./lib\";\nimport { TestStrategySchema } from \"./lib/testUtils\";\nimport { Args, CallbackFunction, Options, SendCodeFunction } from \"./types\";\n\nclass MagicCodeStrategy extends PassportStrategy.Strategy {\n name: string;\n args: Args;\n sendCode: SendCodeFunction;\n callback: CallbackFunction;\n\n constructor(\n args: Args,\n sendCode: SendCodeFunction,\n callback: CallbackFunction\n ) {\n const isTest = process.env.NODE_ENV === \"test\";\n const parsedArguments = (\n isTest ? TestStrategySchema : StrategySchema\n ).safeParse([args, sendCode, callback]);\n\n if (!parsedArguments.success) {\n throw new Error(parsedArguments.error.message);\n }\n\n super();\n\n this.name = \"magic-code\";\n this.args = parsedArguments.data[0] as Args;\n this.sendCode = parsedArguments.data[1] satisfies SendCodeFunction;\n this.callback = parsedArguments.data[2] satisfies CallbackFunction;\n }\n\n /* passport-strategy does not expect new express Request type from ^5.0.0 */\n /* @ts-ignore */\n async authenticate(req: Request, options: Options) {\n const parsedOptions = OptionsSchema.safeParse(options);\n\n if (!parsedOptions.success) {\n throw new Error(parsedOptions.error.message);\n }\n\n options = parsedOptions.data;\n\n if (options.action === \"callback\") {\n return this.acceptCode(req, options);\n }\n\n if (options.action === \"register\" || options.action === \"login\") {\n return this.requestCode(req, options);\n }\n\n return this.error(new Error(\"Unknown action\"));\n }\n\n async requestCode(req: Request, options: Options) {\n const parsedBody = z\n .object({\n [this.args.userPrimaryKey]: z.string(),\n })\n .safeParse(req.body);\n\n if (!parsedBody.success) this.fail(parsedBody.error, 400);\n\n let user = req.body;\n\n const code = randomInt(\n 10 ** (this.args.codeLength - 1),\n 10 ** this.args.codeLength - 1\n );\n\n /* Defined if error occured, else null */\n const error = await this.sendCode(user, code, options);\n\n if (error) {\n throw error;\n }\n\n /* await */\n await this.args.storage.set(code.toString(), {\n expiresIn: (Date.now() + this.args.expiresIn * 60 * 1000) / 1,\n user:\n options.action === \"register\"\n ? user\n : {\n [this.args.userPrimaryKey]: user[this.args.userPrimaryKey],\n },\n });\n\n this.pass();\n return user;\n }\n\n async acceptCode(req: Request, options: Options) {\n const code = lookup(\n [req.body, req.query, req.params],\n this.args.codeField\n )?.toString();\n\n const userUID = lookup(\n [req.body, req.query, req.params],\n this.args.userPrimaryKey\n )?.toString();\n\n if (!code) {\n throw {\n error: `Missing field: ${this.args.codeField}`,\n message: `The code field (${this.args.codeField}) is missing.`,\n statusCode: 400,\n };\n }\n if (!userUID) {\n throw {\n error: `Missing field: ${this.args.userPrimaryKey}`,\n message: `The primary key (${this.args.userPrimaryKey}) is missing.`,\n statusCode: 400,\n };\n }\n\n /* await */\n const token = await this.args.storage.get(code);\n\n if (\n !token ||\n !(this.args.userPrimaryKey in token?.user) ||\n !token?.user[this.args.userPrimaryKey as keyof typeof token.user] ||\n token?.user[this.args.userPrimaryKey as keyof typeof token.user] !==\n userUID ||\n token?.expiresIn <= Date.now()\n ) {\n throw {\n error: \"Invalid code\",\n message: \"Code does not exist, is already used or is expired.\",\n statusCode: 400,\n };\n }\n\n /* await */\n await this.args.storage.delete(code);\n\n return this.success(await this.callback(token.user, options));\n }\n}\n\nexport { Options as AuthenticationOptions, MagicCodeStrategy as Strategy };\n","export const lookup = (objects: any[], key: string) => {\n for (let i = 0; i < objects.length; i++) {\n if (objects[i] && key in objects[i]) {\n const obj = objects[i];\n return obj[key as keyof typeof obj];\n }\n }\n};\n","import { MemoryStorage } from \"../types\";\n\nexport const memoryStorage: MemoryStorage = {\n codes: {},\n get: async (key) => {\n return (await memoryStorage.codes[key]) as ReturnType<MemoryStorage[\"get\"]>;\n },\n set: async (key, value) => {\n return await void (memoryStorage.codes[key] = value);\n },\n delete: async (key) => {\n return await void delete memoryStorage.codes[key];\n },\n};\n","import { CallbackFunction, MemoryStorage, SendCodeFunction } from \"src/types\";\nimport z from \"zod\";\nimport { memoryStorage } from \"./memoryStorage\";\n\nexport const TokenSchema = z.object({\n expiresIn: z.number(),\n user: z.any(),\n});\n\nexport const MemoryStorageSchema = z.object({\n set: z.custom<MemoryStorage[\"set\"]>(),\n get: z.custom<MemoryStorage[\"get\"]>(),\n delete: z.custom<MemoryStorage[\"delete\"]>(),\n codes: z.record(z.string(), TokenSchema).default({}),\n});\n\nexport const ArgsSchema = z.object({\n secret: z.string().min(16, {\n message: \"Secret must be at least 16 characters long\",\n }),\n codeLength: z.number().gte(4).default(4),\n storage: MemoryStorageSchema.default(\n memoryStorage as z.infer<typeof MemoryStorageSchema>\n ),\n expiresIn: z.number().default(30) /* Minutes */,\n codeField: z.string().default(\"code\"),\n userPrimaryKey: z.string().default(\"email\"),\n});\n\nexport const OptionsSchema = z.object({\n action: z.enum([\"callback\", \"login\", \"register\"] as const),\n});\n\nexport const SendCodeSchema = z.custom<SendCodeFunction>();\nexport const CallbackSchema = z.custom<CallbackFunction>();\n\nexport const StrategySchema = z.tuple([\n ArgsSchema,\n SendCodeSchema, // SendCodeSchema\n CallbackSchema, // CallbackSchema\n]);\n","// testMemoryStorageSchema.ts (only for testing!)\nimport { z } from \"zod\";\nimport { memoryStorage } from \"./memoryStorage\";\nimport { ArgsSchema } from \"./schemas\";\n\nexport const TestMemoryStorageSchema = z\n .object({\n set: z.any(),\n get: z.any(),\n delete: z.any(),\n codes: z.record(z.string(), z.any()).default({}),\n })\n .default(memoryStorage);\n\nconst TestArgsSchema = ArgsSchema.extend({\n storage: TestMemoryStorageSchema,\n});\n\nexport const TestStrategySchema = z.tuple([TestArgsSchema, z.any(), z.any()]);\n\nexport type TestArgs = z.infer<typeof TestArgsSchema>;\n","import z from \"zod\";\nimport { ArgsSchema, OptionsSchema, TokenSchema } from \"./lib\";\n\nexport type Token = z.infer<typeof TokenSchema>;\n\nconst _ArgsSchema = ArgsSchema.required({\n secret: true,\n});\nexport type Args = z.output<typeof _ArgsSchema> & {\n storage: MemoryStorage;\n};\n\nexport type SendCodeFunction = (\n user: any,\n expiresIn: number,\n options: Options\n) => any | Promise<any>;\n\nexport type CallbackFunction = (\n user: any,\n options: Options\n) => any | Promise<any>;\n\nexport type Options = z.infer<typeof OptionsSchema>;\n\nexport type MemoryStorage = {\n set: (key: string, value: Token) => Promise<void | any>;\n get: (key: string) => Promise<Token | undefined>;\n delete: (key: string) => Promise<void | any>;\n codes: Record<string, z.infer<Token>>;\n};\n"],"mappings":"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","names":["z","z","z","z"]}