@raddiamond/nexauth-core
Version:
Core authentication plugin supporting Local, AD authentication
286 lines (285 loc) • 14.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AuthService = void 0;
const server_1 = require("@simplewebauthn/server");
const base64url_1 = __importDefault(require("base64url"));
const otplib_1 = require("otplib");
const index_1 = require("../index");
const StepAuthEngine_1 = require("./StepAuthEngine");
// In-memory challenge store for demo (replace with DB/session in production)
const webauthnChallengeStore = new Map();
class AuthService {
constructor(context) {
this.context = context;
this.provider = (0, index_1.createIdentityProvider)(context);
}
buildSteps(input) {
const { authSteps = [{ type: 'password' }] } = this.context;
return authSteps.map((cfg, idx) => {
if (cfg.type === 'password') {
return {
name: authSteps.filter(s => s.type === cfg.type).length > 1 ? `${cfg.type}-${idx + 1}` : cfg.type,
isRequired: () => true,
validate: async (password, user) => {
if (!user && input.username) {
const found = await this.provider.getUser(input.username);
user = found === null ? undefined : found;
if (!user)
return false;
}
if (!user)
return false;
return this.provider.validatePassword(user, password);
},
getPrompt: () => 'Enter your password',
};
}
else if (cfg.type === 'otp') {
return {
name: authSteps.filter(s => s.type === cfg.type).length > 1 ? `${cfg.type}-${idx + 1}` : cfg.type,
isRequired: (user) => {
const required = typeof cfg.isOTPRequired === 'function' ? cfg.isOTPRequired(user) : !!(user && user.otpSecret);
return required;
},
validate: async (otp, user) => {
if (!user) {
return false;
}
if (typeof cfg.validator === 'function') {
const result = await cfg.validator(otp, user);
return result;
}
const secret = user.otpSecret;
if (!secret) {
return false;
}
const valid = otplib_1.authenticator.check(otp, secret);
return valid;
},
getPrompt: () => 'Enter your OTP',
};
}
else if (cfg.type === 'securityQuestion') {
return {
name: authSteps.filter(s => s.type === cfg.type).length > 1 ? `${cfg.type}-${idx + 1}` : cfg.type,
isRequired: () => true,
validate: async (answer, user) => {
if (!user || typeof user.securityAnswer === 'undefined')
return false;
return answer === user.securityAnswer;
},
getPrompt: () => 'What is your security question?',
};
}
else if (cfg.type === 'webauthn') {
return {
name: authSteps.filter(s => s.type === cfg.type).length > 1 ? `${cfg.type}-${idx + 1}` : cfg.type,
isRequired: (user) => {
if (typeof cfg.isWebAuthnRequired === 'function')
return cfg.isWebAuthnRequired(user);
// Require if not enrolled
return true;
},
validate: async (value, user, fullInput) => {
if (!user)
return false;
const username = user.email || user.username;
const rpID = this.context.domain || 'localhost';
// Registration flow
if (fullInput && fullInput.registration && value && value.response && value.response.attestationObject) {
const expectedChallenge = webauthnChallengeStore.get(username);
if (!expectedChallenge)
return false;
try {
const verification = await (0, server_1.verifyRegistrationResponse)({
response: value,
expectedChallenge,
expectedOrigin: `https://${rpID}`,
expectedRPID: rpID,
});
if (verification.verified && verification.registrationInfo && verification.registrationInfo.credential) {
const newCred = {
id: verification.registrationInfo.credential.id,
publicKey: base64url_1.default.encode(Buffer.from(verification.registrationInfo.credential.publicKey)),
counter: verification.registrationInfo.credential.counter,
transports: value.response.transports,
};
if (!user.webauthnCredentials)
user.webauthnCredentials = [];
user.webauthnCredentials.push(newCred);
return true;
}
return false;
}
catch (err) {
return false;
}
}
// ...existing authentication flow...
if (!user.webauthnCredentials || user.webauthnCredentials.length === 0) {
// Not enrolled, must register
return false;
}
if (!value) {
// No assertion provided, so generate challenge and return false to trigger nextStep
return false;
}
const expectedChallenge = webauthnChallengeStore.get(username);
if (!expectedChallenge)
return false;
const credRecord = user.webauthnCredentials.find((c) => c.id === value.id);
if (!credRecord)
return false;
try {
const verification = await (0, server_1.verifyAuthenticationResponse)({
response: value,
expectedChallenge,
expectedOrigin: `https://${rpID}`,
expectedRPID: rpID,
credential: {
id: credRecord.id,
publicKey: base64url_1.default.toBuffer(credRecord.publicKey),
counter: credRecord.counter,
transports: credRecord.transports,
},
});
if (verification.verified) {
credRecord.counter = verification.authenticationInfo.newCounter;
return true;
}
return false;
}
catch (err) {
return false;
}
},
getPrompt: async (user) => {
const username = user?.email || user?.username;
// Use rpID from context
const rpID = this.context.domain || 'localhost';
if (!user || !user.webauthnCredentials || user.webauthnCredentials.length === 0) {
// Not enrolled, return registration challenge
const options = await Promise.resolve().then(() => __importStar(require('@simplewebauthn/server'))).then(m => m.generateRegistrationOptions({
rpName: 'NexAuth',
rpID,
userID: Buffer.from(String(user?.id || username), 'utf8'),
userName: username,
attestationType: 'none',
authenticatorSelection: { userVerification: 'preferred' },
}));
webauthnChallengeStore.set(username, options.challenge);
return JSON.stringify({ prompt: 'Register your security key', registrationRequired: true, challenge: options });
}
const options = await (0, server_1.generateAuthenticationOptions)({
rpID,
allowCredentials: user.webauthnCredentials.map((cred) => ({
id: cred.id,
transports: cred.transports,
})),
userVerification: 'preferred',
});
webauthnChallengeStore.set(username, options.challenge);
return JSON.stringify({ prompt: 'Use your security key or biometric device', challenge: options });
},
};
}
else if (cfg.type === 'otp') {
return {
name: authSteps.filter(s => s.type === cfg.type).length > 1 ? `${cfg.type}-${idx + 1}` : cfg.type,
isRequired: (user) => {
return true;
},
validate: async (otp, user) => {
if (!user)
return false;
const secret = user.otpSecret;
if (!secret) {
// Not enrolled, must register
return false;
}
const valid = otplib_1.authenticator.check(otp, secret);
return valid;
},
getPrompt: async (user) => {
if (!user || !user.otpSecret) {
// Not enrolled, generate secret
const secret = otplib_1.authenticator.generateSecret();
const otpAuthUrl = otplib_1.authenticator.keyuri(user?.email || '', 'NexAuth', secret);
return JSON.stringify({ prompt: 'Set up your authenticator app', registrationRequired: true, otpSecret: secret, otpAuthUrl });
}
return 'Enter your OTP';
},
};
}
else {
throw new Error(`Unknown step type: ${cfg.type}`);
}
});
}
async handleAuth(input) {
let user = undefined;
if (input.username) {
const found = await this.provider.getUser(input.username);
user = found === null ? undefined : found;
}
const steps = this.buildSteps(input);
const engine = new StepAuthEngine_1.StepAuthEngine(steps);
const result = await engine.authenticate(user, input);
if (result.success && result.user) {
const token = (0, index_1.IssueToken)({
userId: result.user.id,
email: result.user.email,
roles: result.user.roles ?? [],
});
return { success: true, user: result.user, token };
}
if (result.nextStep) {
const nextStepObj = steps.find(s => s.name === result.nextStep);
const prompt = nextStepObj?.getPrompt ? await nextStepObj.getPrompt(user) : undefined;
return {
success: false,
nextStep: result.nextStep,
prompt,
error: result.error,
};
}
return { success: false, error: result.error };
}
}
exports.AuthService = AuthService;