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