UNPKG

@daitanjs/security

Version:

A lightweight library for token generation and verification.

8 lines (7 loc) 14.5 kB
{ "version": 3, "sources": ["../src/index.js"], "sourcesContent": ["// security/src/index.js\n/**\n * @file Security utilities including OTP and JWT generation/verification.\n * @module @daitanjs/security\n *\n * @description\n * This library provides lightweight, focused security utilities for common tasks:\n * - **OTP (One-Time Password) Generation**: Create numeric or alphanumeric OTPs\n * with configurable length and validity.\n * - **OTP Verification**: A conceptual helper for verifying submitted OTPs against stored ones.\n * (Actual storage and retrieval of OTPs are handled by the consuming application).\n * - **JWT (JSON Web Token) Operations**: Generate, verify, and decode JWTs using\n * the `jsonwebtoken` library. Includes default expiry and automatic `jti` (JWT ID) generation.\n *\n * All cryptographic operations rely on Node.js `crypto` module or the `jsonwebtoken` library.\n */\n\nimport crypto from 'crypto';\nimport jwt from 'jsonwebtoken';\nimport { v4 as uuidv4 } from 'uuid'; // For generating JWT IDs (jti)\nimport { getLogger } from '@daitanjs/development'; // For internal logging, if needed\nimport { DaitanInvalidInputError, DaitanOperationError } from '@daitanjs/error'; // For specific errors\n\nconst logger = getLogger('daitan-security'); // Logger for this module\n\n// --- OTP (One-Time Password) Functionality ---\n\nconst DEFAULT_OTP_LENGTH = 6;\nconst DEFAULT_OTP_VALIDITY_MS = 5 * 60 * 1000; // 5 minutes\n\n/**\n * @typedef {Object} OTPGenerationResult\n * @property {string} otp - The generated One-Time Password.\n * @property {number} expiresAt - Timestamp (in milliseconds since epoch) when the OTP expires.\n */\n\n/**\n * @typedef {Object} GenerateOTPOptions\n * @property {number} [length] - The desired length of the OTP.\n * @property {number} [validityMs] - The validity period of the OTP in milliseconds.\n */\n\n/**\n * Generates a cryptographically secure numeric One-Time Password (OTP).\n *\n * @public\n * @param {GenerateOTPOptions} [options={}] - Options for OTP generation.\n * @returns {OTPGenerationResult} An object containing the OTP string and its expiry timestamp.\n * @throws {DaitanInvalidInputError} If `length` is not a positive integer or `validityMs` is not a positive number.\n */\nexport const generateNumericOTP = ({\n length = DEFAULT_OTP_LENGTH,\n validityMs = DEFAULT_OTP_VALIDITY_MS,\n} = {}) => {\n if (!Number.isInteger(length) || length <= 0 || length > 10) {\n // Max length 10 for practical numeric OTPs\n throw new DaitanInvalidInputError(\n 'OTP length must be a positive integer, typically between 4 and 10.'\n );\n }\n if (typeof validityMs !== 'number' || validityMs <= 0) {\n throw new DaitanInvalidInputError(\n 'OTP validityMs must be a positive number.'\n );\n }\n\n // crypto.randomInt(max) generates an int in [0, max-1]. For length N, max is 10^N.\n // A more direct way is to generate `length` random digits.\n let otp = '';\n for (let i = 0; i < length; i++) {\n otp += crypto.randomInt(0, 10).toString();\n }\n\n const expiresAt = Date.now() + validityMs;\n logger.debug(\n `Generated numeric OTP (length ${length}), expires at ${new Date(\n expiresAt\n ).toISOString()}`\n );\n return { otp, expiresAt };\n};\n\n/**\n * Generates a cryptographically secure alphanumeric One-Time Password (OTP).\n *\n * @public\n * @param {GenerateOTPOptions} [options={length: 8, validityMs: DEFAULT_OTP_VALIDITY_MS}] - Options for OTP generation.\n * @returns {OTPGenerationResult} An object containing the OTP string and its expiry timestamp.\n * @throws {DaitanInvalidInputError} If `length` is not a positive integer or `validityMs` is not a positive number.\n */\nexport const generateAlphanumericOTP = ({\n length = 8,\n validityMs = DEFAULT_OTP_VALIDITY_MS,\n} = {}) => {\n if (!Number.isInteger(length) || length <= 0 || length > 32) {\n // Max length for practical OTPs\n throw new DaitanInvalidInputError(\n 'Alphanumeric OTP length must be a positive integer, typically between 6 and 32.'\n );\n }\n if (typeof validityMs !== 'number' || validityMs <= 0) {\n throw new DaitanInvalidInputError(\n 'OTP validityMs must be a positive number.'\n );\n }\n\n const characters =\n 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n let otp = '';\n // Generate cryptographically secure random bytes\n const randomBytes = crypto.randomBytes(length);\n for (let i = 0; i < length; i++) {\n // Map byte to character index\n otp += characters[randomBytes[i] % characters.length];\n }\n const expiresAt = Date.now() + validityMs;\n logger.debug(\n `Generated alphanumeric OTP (length ${length}), expires at ${new Date(\n expiresAt\n ).toISOString()}`\n );\n return { otp, expiresAt };\n};\n\n/**\n * @typedef {Object} VerifyOTPOptions\n * @property {string} submittedOTP - The OTP submitted by the user.\n * @property {string} storedOTP - The OTP that was generated and stored by the application.\n * @property {number} storedOTPExpiresAt - The expiry timestamp (milliseconds since epoch) of the stored OTP.\n */\n\n/**\n * Verifies a submitted OTP against a stored OTP and its expiry time.\n * This is a conceptual helper. The actual storage and retrieval of `storedOTP`\n * and `storedOTPExpiresAt` is the responsibility of the consuming application.\n *\n * @public\n * @param {VerifyOTPOptions} options - Parameters for OTP verification.\n * @returns {boolean} True if the OTP is valid (matches and not expired), false otherwise.\n */\nexport const verifyOTP = ({ submittedOTP, storedOTP, storedOTPExpiresAt }) => {\n if (\n typeof submittedOTP !== 'string' ||\n !submittedOTP ||\n typeof storedOTP !== 'string' ||\n !storedOTP ||\n typeof storedOTPExpiresAt !== 'number'\n ) {\n logger.warn('verifyOTP: Invalid parameters received for OTP verification.');\n return false;\n }\n\n if (Date.now() > storedOTPExpiresAt) {\n logger.debug('OTP verification failed: OTP has expired.');\n return false; // OTP has expired\n }\n\n // Constant-time comparison is not strictly necessary for OTPs of typical length,\n // but good practice if maximum security is paramount. For simplicity, direct comparison is used.\n const isValid = crypto.timingSafeEqual(\n Buffer.from(submittedOTP),\n Buffer.from(storedOTP)\n );\n\n if (!isValid) {\n logger.debug(\n 'OTP verification failed: Submitted OTP does not match stored OTP.'\n );\n } else {\n logger.debug('OTP verification successful.');\n }\n return isValid;\n};\n\n// --- JWT (JSON Web Token) Functionality ---\n\nconst DEFAULT_JWT_EXPIRY = '1h'; // Default expiry time for JWTs\n\n/**\n * @typedef {Object} GenerateJWTOptions\n * @property {object} payload - The payload to include in the JWT.\n * @property {string | Buffer} secretOrPrivateKey - The secret or private key for signing.\n * @property {jwt.SignOptions} [options] - Options for JWT signing (e.g., `expiresIn`).\n */\n\n/**\n * Generates a JSON Web Token (JWT).\n *\n * @public\n * @param {GenerateJWTOptions} params - Parameters for JWT generation.\n * @returns {string} The generated JWT string.\n * @throws {DaitanInvalidInputError} If `payload` or `secretOrPrivateKey` is missing or invalid.\n * @throws {DaitanOperationError} If `jsonwebtoken.sign` fails.\n */\nexport const generateJWT = ({ payload, secretOrPrivateKey, options = {} }) => {\n if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {\n throw new DaitanInvalidInputError(\n 'JWT payload must be a non-null plain object.'\n );\n }\n if (!secretOrPrivateKey) {\n throw new DaitanInvalidInputError(\n 'JWT secret or private key is required for signing.'\n );\n }\n\n const jwtOptions = {\n expiresIn: DEFAULT_JWT_EXPIRY,\n jwtid: uuidv4(), // JWT ID (jti) claim, useful for revocation or tracking\n ...options, // User-provided options override defaults\n };\n\n try {\n const token = jwt.sign(payload, secretOrPrivateKey, jwtOptions);\n logger.debug(\n `Generated JWT with JTI: ${jwtOptions.jwtid}, expires: ${jwtOptions.expiresIn}.`\n );\n return token;\n } catch (error) {\n logger.error(`Error generating JWT: ${error.message}`, {\n errorName: error.name,\n });\n throw new DaitanOperationError(\n `JWT signing failed: ${error.message}`,\n {},\n error\n );\n }\n};\n\n/**\n * @typedef {Object} VerifyJWTOptions\n * @property {string} token - The JWT string to verify.\n * @property {string | Buffer} secretOrPublicKey - The secret or public key for verification.\n * @property {jwt.VerifyOptions} [options] - Options for JWT verification.\n */\n\n/**\n * Verifies a JSON Web Token (JWT).\n *\n * @public\n * @param {VerifyJWTOptions} params - Parameters for JWT verification.\n * @returns {object | string} The decoded payload if verification is successful.\n * @throws {DaitanInvalidInputError} If `token` or `secretOrPublicKey` is missing.\n * @throws {jwt.JsonWebTokenError | jwt.TokenExpiredError | jwt.NotBeforeError} Throws specific errors from `jsonwebtoken` on failure.\n */\nexport const verifyJWT = ({ token, secretOrPublicKey, options = {} }) => {\n if (!token || typeof token !== 'string') {\n throw new DaitanInvalidInputError(\n 'JWT token string is required for verification.'\n );\n }\n if (!secretOrPublicKey) {\n throw new DaitanInvalidInputError(\n 'JWT secret or public key is required for verification.'\n );\n }\n\n try {\n const decoded = jwt.verify(token, secretOrPublicKey, options);\n logger.debug('JWT verified successfully.', {\n jti: decoded.jti,\n sub: decoded.sub,\n });\n return decoded;\n } catch (error) {\n logger.warn(`JWT verification failed: ${error.message}`, {\n errorName: error.name,\n tokenPreview: token.substring(0, 20) + '...',\n });\n throw error;\n }\n};\n\n/**\n * @typedef {Object} DecodeJWTOptions\n * @property {string} token - The JWT string to decode.\n * @property {jwt.DecodeOptions} [options] - Options for decoding.\n */\n\n/**\n * Decodes a JSON Web Token (JWT) without verifying its signature.\n *\n * @public\n * @param {DecodeJWTOptions} params - Parameters for JWT decoding.\n * @returns {null | string | { [key: string]: any }} The decoded payload or null if malformed.\n */\nexport const decodeJWT = ({ token, options = {} }) => {\n if (!token || typeof token !== 'string') {\n logger.warn('decodeJWT: Invalid or missing token string provided.');\n return null;\n }\n try {\n const decoded = jwt.decode(token, options);\n if (decoded === null) {\n logger.warn('decodeJWT: Token was malformed and could not be decoded.', {\n tokenPreview: token.substring(0, 20) + '...',\n });\n } else {\n logger.debug('JWT decoded (signature NOT VERIFIED).');\n }\n return decoded;\n } catch (error) {\n logger.error(`Error during JWT decode (unexpected): ${error.message}`, {\n errorName: error.name,\n });\n return null;\n }\n};\n"], "mappings": "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", "names": ["crypto", "uuidv4", "jwt"] }