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
JavaScript
"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