UNPKG

@raddiamond/nexauth-core

Version:

Core authentication plugin supporting Local, AD authentication

286 lines (285 loc) 14.4 kB
"use strict"; 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;