UNPKG

nhb-toolbox

Version:

A versatile collection of smart, efficient, and reusable utility functions, classes and types for everyday development needs.

450 lines (449 loc) 17.1 kB
import type { GenericObject } from '../object/types'; import type { Maybe } from '../types/index'; import type { DecodedToken, SignetPayload, SignOptions, TokenString, VerifiedToken, VerifyOptions } from './types'; /** * @class A lightweight, secure implementation of JWT-like tokens using `HMAC-SHA256` signatures. * - This class provides methods to create, verify, and decode tokens with a simple API similar to JSON Web Tokens (`JWT`) * but with a smaller footprint and zero dependencies. * * @remarks * - **Features:** * - `HMAC-SHA256` signatures for security * - Time-based claims (expiration, not-before) * - Standard claims (audience, issuer, subject) * - Constant-time signature comparison to prevent timing attacks * - Automatic date conversion for timestamp claims * - `Base64` URL-safe encoding (standard `Base64` in this implementation) * * - **Security considerations:** * - Keep the secret key secure and rotate periodically * - Use appropriate token expiration times * - Validate all claims relevant to your application * - Store tokens securely (HTTP-only cookies recommended for web) * * @example * ```typescript * // Create a token signer * const signet = new Signet('my-secret-key'); * * // Sign a token with custom payload and options * const token = signet.sign( * { userId: 123, role: 'admin' }, * { * expiresIn: '1h', * audience: 'my-app', * issuer: 'auth-service' * } * ); * * // Verify a token * const result = signet.verify(token, { * audience: 'my-app', * issuer: 'auth-service' * }); * * if (result.isValid) { * console.log('Valid token for user:', result.payload.userId); * } else { * console.log('Invalid token:', result.error); * } * * // Decode without verification * const decoded = signet.decode(token); * console.log('Token payload:', decoded.payload); * ``` */ export declare class Signet { #private; /** * * Creates a new `Signet` instance with the specified secret key. * * @param secret - The secret key used for signing and verifying tokens. * Must be a non-empty string. * * @throws If the secret is not a non-empty string. * * @remarks * - The secret is converted to `UTF-8` bytes and stored internally. * - Choose a strong secret (at least 32 characters) and store it securely. * - For production, consider using key rotation strategies. * * @example * ```typescript * // Initialize with a secret key * const signet = new Signet('super-secret-key-123'); * * // Use environment variable for the secret * const signet = new Signet(process.env.JWT_SECRET!); * ``` */ constructor(secret: string); /** * * Creates and signs a new token with the given payload and options. * * @param payload - Custom data to include in the token payload. * Must be a `non-empty object`. * @param options - Optional configuration for token claims and expiration. * * @returns A signed token string in the format `header.payload.signature`. * * @throws If payload is not a valid object. * * @remarks * - **The token structure follows JWT format:** * - Header: Contains algorithm (`HS256`) and token type (`SIGNET+JWT`) * - Payload: Includes standard claims (`iat`, `exp`, `nbf`, `aud`, `sub`, `iss`) plus custom data * - Signature: `HMAC-SHA256(signingInput, secret)` (the result of the hash) * - Signing Inputs: `base64(header) + "." + base64(payload)` (the string that gets hashed) * * - **Automatic claims added:** * - `iat` (issued at): Current time in seconds * - `iatDate`: Current time as Date object * - If `expiresIn` is provided: `exp` and `expDate` * - If `notBefore` is provided: `nbf` and `nbfDate` * * @example * ```typescript * // Basic token with custom data * const token = signet.sign({ userId: 123, name: 'John' }); * * // Token with expiration and claims * const token = signet.sign( * { userId: 123 }, * { * expiresIn: '2h', * audience: 'api.example.com', * issuer: 'auth-service', * subject: 'user-123' * } * ); * * // Token valid after 5 minutes * const token = signet.sign( * { action: 'reset-password' }, * { notBefore: '5m' } * ); * ``` */ sign(payload: GenericObject, options?: SignOptions): TokenString; /** * * Decodes a token without verifying its signature. * * @typeParam T - Type of custom data in the token payload. * @param token - The token string to decode. * * @returns The decoded token parts including header, payload, and signatures. * * @throws If the token is malformed, empty, or cannot be parsed. * * @remarks * - Use this method when you need to inspect token contents without verification. * - **Warning:** This does not validate the signature, so the data may have been tampered with. * - Always use {@link verify} method for security-critical operations. * - The payload includes both timestamp values (numbers) and {@link Date} objects for convenience. * * @example * ```typescript * // Decode token to inspect contents * const decoded = signet.decode(token); * console.log('Header:', decoded.header); * console.log('Payload:', decoded.payload); * console.log('Signature:', decoded.signature); * * // Access custom payload data with type safety * const decoded = signet.decode<{ userId: number }>(token); * const userId = decoded.payload.userId; // Type: number * ``` */ decode<T extends GenericObject = GenericObject>(token: string): DecodedToken<T>; /** * * Checks if a token has expired based on its `exp` claim. * * @param token - The token to check. * * @returns `true` if the token has an `exp` claim and current time is past it, * `false` if token has no expiration or is still valid. * * @throws If the token is malformed or cannot be decoded. * * @remarks * - Tokens without `exp` claim are considered non-expiring (returns `false`) * - Uses current system time for comparison ({@link Date.now()}) * - Does not verify the signature (use only with trusted tokens or after verification) * * @example * ```typescript * // Check expiration * if (signet.hasExpired(token)) { * console.log('Token has expired'); * // Prompt user to re-authenticate * } * * // Use with other validation * const isValid = !signet.hasExpired(token) && !signet.isTooEarly(token); * ``` */ hasExpired(token: string): boolean; /** * * Checks if a token's `nbf` (not-before) claim indicates it's too early to use. * * @param token - The token to check. * * @returns `true` if the token has an `nbf` claim and current time is before it, * `false` if token has no `nbf` claim or is already valid. * * @throws If the token is malformed or cannot be decoded. * * @remarks * - Useful for implementing time-based access control, like activation links that shouldn't be used until a certain time. * - Uses current system time for comparison ({@link Date.now()}) * - Does not verify the signature (use only with trusted tokens or after verification) * * @example * ```typescript * // Check if token is active yet * if (signet.isTooEarly(token)) { * console.log('Token not valid yet'); * // Wait before using * } * ``` */ isTooEarly(token: string): boolean; /** * * Validates a token's `iss` (issuer) claim against an expected value. * * @param token - The token to check. * @param expected - The expected issuer value. If `undefined`, always returns `false`. * * @returns `true` if the token has an `iss` claim that doesn't match the expected value, * `false` if issuer matches, token has no issuer claim, or expected issuer is undefined. * * @throws If the token is malformed or cannot be decoded. * * @remarks Use this to ensure tokens come from trusted sources in multi-issuer scenarios. * * @example * ```typescript * // Validate issuer * if (signet.isInvalidIssuer(token, 'auth-service')) { * console.log('Token from unexpected issuer'); * // Reject token * } * * // With optional issuer check * const issuer = process.env.EXPECTED_ISSUER; * if (issuer && signet.isInvalidIssuer(token, issuer)) { * throw new Error('Invalid issuer'); * } * ``` */ isInvalidIssuer(token: string, expected: Maybe<string>): boolean; /** * * Validates a token's `aud` (audience) claim against expected values. * * @param token - The token to check. * @param expected - The expected audience(s). Can be a string or array of strings. * If `undefined`, always returns `false`. * * @returns `true` if the token has an `aud` claim and none of its values match any of the expected audiences, `false` otherwise. * * @throws If the token is malformed or cannot be decoded. * * @remarks * - Tokens can have single audience (string) or multiple audiences (string[]) * - Returns `false` (valid) if at least one audience matches * - Useful for multi-tenant or multi-service architectures * * @example * ```typescript * // Single audience check * if (signet.isInvalidAudience(token, 'api.example.com')) { * console.log('Token not intended for this audience'); * } * * // Multiple allowed audiences * const validAudiences = ['web-app', 'mobile-app', 'admin-panel']; * if (signet.isInvalidAudience(token, validAudiences)) { * throw new Error('Invalid audience'); * } * * // Token with multiple audiences * // Token payload: { aud: ['web-app', 'mobile-app'] } * // Check if at least one matches * const isValid = !signet.isInvalidAudience(token, ['web-app', 'admin-panel']); * // Returns false (valid) because 'web-app' matches * ``` */ isInvalidAudience(token: string, expected: Maybe<string | string[]>): boolean; /** * * Validates a token's `sub` (subject) claim against an expected value. * * @param token - The token to check. * @param expected - The expected subject value. If `undefined`, always returns `false`. * * @returns `true` if the token has a `sub` claim that doesn't match the expected value, * `false` if subject matches, token has no subject claim, or expected subject is undefined. * * @throws If the token is malformed or cannot be decoded. * * @remarks * - Use this to ensure tokens are being used by the intended user/entity. * - Common for authorization checks where tokens should be user-specific. * * @example * ```typescript * // Validate subject * const userId = 'user-123'; * if (signet.isInvalidSubject(token, userId)) { * console.log('Token not for this user'); * // Reject request * } * * // Optional subject validation * const expectedSubject = getExpectedSubjectFromRequest(); * if (expectedSubject && signet.isInvalidSubject(token, expectedSubject)) { * return response.status(403).send('Invalid subject'); * } * ``` */ isInvalidSubject(token: string, expected: Maybe<string>): boolean; /** * * Verifies a token's signature and validates all claims. * * @typeParam T - Type of custom data in the token payload. * @param token - The token string to verify. * @param options - Optional validation criteria for token claims. * * @returns A {@link VerifiedToken} object indicating success or failure. * - If valid: `{ isValid: true, payload: SignetPayload<T> }` * - If invalid: `{ isValid: false, error: string }` * * @remarks * - **Performs the following checks in order:** * - Token structure (3 parts separated by dots) * - Base64 decoding of header and payload * - JSON parsing of header and payload * - Signature verification (constant-time comparison) * - Expiration check (if `exp` claim exists) * - Not-before check (if `nbf` claim exists) * - Issuer validation (if provided in options) * - Audience validation (if provided in options) * - Subject validation (if provided in options) * * - **This is the recommended method for most token validation scenarios.** * * @example * ```typescript * // Basic verification * const result = signet.verify(token); * if (result.isValid) { * console.log('Valid token:', result.payload); * } else { * console.log('Invalid token:', result.error); * } * * // With claim validation * const result = signet.verify(token, { * audience: 'api.example.com', * issuer: 'auth-service', * subject: 'user-123' * }); * * // Type-safe custom payload * interface UserToken { * userId: number; * role: string; * } * const result = signet.verify<UserToken>(token); * if (result.isValid) { * const { userId, role } = result.payload; * // userId and role are typed * } * ``` */ verify<T extends GenericObject = GenericObject>(token: string, options?: VerifyOptions): VerifiedToken<T>; /** * * Verifies a token and throws an error if invalid. * * @typeParam T - Type of custom data in the token payload. * @param token - The token string to verify. * @param options - Optional validation criteria for token claims. * * @returns A valid {@link VerifiedToken} with `isValid: true`. * * @throws If the token is invalid, with a message describing the failure. * * @remarks * - This method is a convenience wrapper around {@link verify} that throws instead of returning an error object. * - Useful for `express`-style middleware or when you want to handle authentication failures with exceptions. * - The thrown error message is the same as the `error` property in the invalid result from {@link verify}. * * @example * ```typescript * // Use in middleware/guard * function authMiddleware(req, res, next) { * const token = req.headers.authorization?.replace('Bearer ', ''); * * try { * const result = signet.verifyOrThrow(token, { * audience: 'api.example.com' * }); * req.user = result.payload; * next(); * } catch (error) { * res.status(401).json({ error: error.message }); * } * } * * // In application code * try { * const result = signet.verifyOrThrow(token); * // Token is guaranteed valid here * processUserRequest(result.payload); * } catch (error) { * handleAuthError(error); * } * ``` */ verifyOrThrow<T extends GenericObject = GenericObject>(token: string, options?: VerifyOptions): VerifiedToken<T>; /** * * Extracts only the payload from a token without full verification. * * @typeParam T - Type of custom data in the token payload. * @param token - The token string to decode. * * @returns The token payload including standard claims and custom data. * * @throws If the token is malformed, empty, or cannot be parsed. * * @remarks * - This is a convenience method equivalent to `decode(token).payload`. * * - **Security Note:** This method does NOT verify the token signature. * * - **Only use it when:** * - You've already verified the token elsewhere * - The token comes from a trusted source * - You're debugging or logging * - The operation is not security-critical * * - For security-sensitive operations, always use {@link verify} or {@link verifyOrThrow} method. * * @example * ```typescript * // Quick payload extraction for non-critical operations * const payload = signet.decodePayload(token); * console.log('User ID:', payload.userId); * console.log('Issued at:', payload.iatDate); * * // Type-safe with custom interface * interface AppToken { * userId: number; * permissions: string[]; * } * const payload = signet.decodePayload<AppToken>(token); * const canDelete = payload.permissions.includes('delete'); * ``` */ decodePayload<T extends GenericObject = GenericObject>(token: string): SignetPayload<T>; }