fix(security): isolate NextAuth email provider from Nodemailer
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* NextAuth email provider configuration for PEDW.
|
||||
*
|
||||
* PEDW supplies its own sendVerificationRequest implementation using
|
||||
* GOV.UK Notify, so the default Nodemailer SMTP sender is unnecessary.
|
||||
*
|
||||
* Preserve the NextAuth v4 email provider configuration contract.
|
||||
*/
|
||||
const EmailProvider = (options = {}) => ({
|
||||
id: "email",
|
||||
type: "email",
|
||||
name: "Email",
|
||||
server: {
|
||||
host: "localhost",
|
||||
port: 25,
|
||||
auth: {
|
||||
user: "",
|
||||
pass: ""
|
||||
}
|
||||
},
|
||||
from: "NextAuth <no-reply@example.com>",
|
||||
maxAge: 24 * 60 * 60,
|
||||
async sendVerificationRequest() {
|
||||
throw new Error(
|
||||
"PEDW requires a custom GOV.UK Notify verification sender"
|
||||
);
|
||||
},
|
||||
options
|
||||
});
|
||||
|
||||
export default EmailProvider;
|
||||
Generated
+2
-1
@@ -44,7 +44,6 @@
|
||||
"next-redux-wrapper": "^7.0.5",
|
||||
"next-translate": "^2.6.2",
|
||||
"next-translate-plugin": "^2.6.2",
|
||||
"nodemailer": "7.0.13",
|
||||
"notifications-node-client": "^7.0.6",
|
||||
"openapi-types": "^12.1.3",
|
||||
"ospoint": "^0.2.1",
|
||||
@@ -7323,6 +7322,8 @@
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz",
|
||||
"integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==",
|
||||
"license": "MIT-0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
|
||||
+1
-2
@@ -66,7 +66,6 @@
|
||||
"next-redux-wrapper": "^7.0.5",
|
||||
"next-translate": "^2.6.2",
|
||||
"next-translate-plugin": "^2.6.2",
|
||||
"nodemailer": "7.0.13",
|
||||
"notifications-node-client": "^7.0.6",
|
||||
"openapi-types": "^12.1.3",
|
||||
"ospoint": "^0.2.1",
|
||||
@@ -110,4 +109,4 @@
|
||||
"eslint-plugin-jsdoc": "^48.2.3",
|
||||
"prisma": "^5.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import { PrismaAdapter } from "@next-auth/prisma-adapter";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import NextAuth from "next-auth";
|
||||
import EmailProvider from "next-auth/providers/email";
|
||||
import EmailProvider from "../../../lib/auth/email-provider";
|
||||
import { consoleLogger, redactSensitive } from "../../../actions/core/logger";
|
||||
import { getPortalLogin } from "../../../actions/services/accountService";
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const test = require("node:test");
|
||||
const vm = require("node:vm");
|
||||
|
||||
const root = path.resolve(__dirname, "../..");
|
||||
|
||||
const source = fs.readFileSync(
|
||||
path.join(root, "lib/auth/email-provider.js"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
const EmailProvider = vm.runInNewContext(
|
||||
source.replace("export default EmailProvider;", "") + "\nEmailProvider;",
|
||||
{}
|
||||
);
|
||||
|
||||
const OriginalEmailProvider = require("next-auth/providers/email").default;
|
||||
|
||||
const customSender = async () => {};
|
||||
|
||||
const options = {
|
||||
maxAge: 2 * 60 * 60,
|
||||
sendVerificationRequest: customSender
|
||||
};
|
||||
|
||||
const normalize = (provider) => {
|
||||
const { options: userOptions, ...defaults } = provider;
|
||||
|
||||
return {
|
||||
...defaults,
|
||||
...userOptions
|
||||
};
|
||||
};
|
||||
|
||||
test("local provider preserves NextAuth email provider defaults", () => {
|
||||
const original = OriginalEmailProvider(options);
|
||||
const replacement = EmailProvider(options);
|
||||
|
||||
for (const property of ["id", "type", "name", "server", "from", "maxAge"]) {
|
||||
assert.deepEqual(
|
||||
JSON.parse(JSON.stringify(replacement[property])),
|
||||
JSON.parse(JSON.stringify(original[property])),
|
||||
`Mismatch in ${property}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("local provider preserves the user options contract", () => {
|
||||
const provider = EmailProvider(options);
|
||||
|
||||
assert.equal(provider.options.maxAge, 7200);
|
||||
assert.equal(provider.options.sendVerificationRequest, customSender);
|
||||
});
|
||||
|
||||
test("normalized provider retains two-hour token expiry", () => {
|
||||
const original = normalize(OriginalEmailProvider(options));
|
||||
const replacement = normalize(EmailProvider(options));
|
||||
|
||||
assert.equal(original.maxAge, 7200);
|
||||
assert.equal(replacement.maxAge, original.maxAge);
|
||||
});
|
||||
|
||||
test("normalized provider uses the custom verification sender", () => {
|
||||
const provider = normalize(EmailProvider(options));
|
||||
|
||||
assert.equal(provider.sendVerificationRequest, customSender);
|
||||
});
|
||||
|
||||
test("provider retains NextAuth email identity", () => {
|
||||
const provider = normalize(EmailProvider(options));
|
||||
|
||||
assert.equal(provider.id, "email");
|
||||
assert.equal(provider.type, "email");
|
||||
assert.equal(provider.name, "Email");
|
||||
});
|
||||
|
||||
test("default sender fails explicitly without PEDW override", async () => {
|
||||
const provider = EmailProvider();
|
||||
|
||||
await assert.rejects(
|
||||
provider.sendVerificationRequest(),
|
||||
/requires a custom GOV\.UK Notify verification sender/
|
||||
);
|
||||
});
|
||||
|
||||
test("local provider does not import Nodemailer", () => {
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/(?:require\s*\(|from\s+|import\s*\()\s*["']nodemailer/
|
||||
);
|
||||
});
|
||||
|
||||
test("authentication handler imports the local provider", () => {
|
||||
const authSource = fs.readFileSync(
|
||||
path.join(root, "pages/api/auth/[...nextauth].js"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
authSource,
|
||||
/import EmailProvider from ["']\.\.\/\.\.\/\.\.\/lib\/auth\/email-provider["']/
|
||||
);
|
||||
|
||||
assert.doesNotMatch(authSource, /from ["']next-auth\/providers\/email["']/);
|
||||
|
||||
assert.match(authSource, /EmailProvider\s*\(\s*\{/);
|
||||
|
||||
assert.match(authSource, /async sendVerificationRequest\s*\(/);
|
||||
|
||||
assert.match(authSource, /notifyClient\.sendEmail\s*\(/);
|
||||
});
|
||||
Reference in New Issue
Block a user