UNPKG

remix-auth-totp

Version:

A Time-Based One-Time Password (TOTP) Authentication Strategy for Remix-Auth.

107 lines (106 loc) 2.93 kB
import base32Encode from 'base32-encode'; /** * TOTP Generation. */ export function generateSecret() { const randomBytes = new Uint8Array(32); crypto.getRandomValues(randomBytes); return base32Encode(randomBytes, 'RFC4648').toString(); } // https://github.com/sindresorhus/uint8array-extras/blob/main/index.js#L222 const hexToDecimalLookupTable = { 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, a: 10, b: 11, c: 12, d: 13, e: 14, f: 15, A: 10, B: 11, C: 12, D: 13, E: 14, F: 15, }; function hexToUint8Array(hexString) { if (hexString.length % 2 !== 0) { throw new Error('Invalid Hex string length.'); } const resultLength = hexString.length / 2; const bytes = new Uint8Array(resultLength); for (let index = 0; index < resultLength; index++) { const highNibble = hexToDecimalLookupTable[hexString[index * 2]]; const lowNibble = hexToDecimalLookupTable[hexString[index * 2 + 1]]; if (highNibble === undefined || lowNibble === undefined) { throw new Error(`Invalid Hex character encountered at position ${index * 2}`); } bytes[index] = (highNibble << 4) | lowNibble; } return bytes; } /** * Redirect. */ export function redirect(url, init = 302) { let responseInit = init; if (typeof responseInit === 'number') { responseInit = { status: responseInit }; } else if (typeof responseInit.status === 'undefined') { responseInit.status = 302; } const headers = new Headers(responseInit.headers); headers.set('Location', url); return new Response(null, { ...responseInit, headers }); } /** * Miscellaneous. */ export function asJweKey(secret) { if (!/^[0-9a-fA-F]{64}$/.test(secret)) { throw new Error('Secret must be a string with 64 hex characters.'); } return hexToUint8Array(secret); } export function coerceToOptionalString(value) { if (typeof value !== 'string' && value !== undefined) { throw new Error('Value must be a string or undefined.'); } return value; } export function coerceToOptionalNonEmptyString(value) { if (typeof value === 'string' && value.length > 0) return value; return undefined; } export function coerceToOptionalTotpSessionData(value) { if (typeof value === 'object' && value !== null && 'jwe' in value && typeof value.jwe === 'string' && 'attempts' in value && typeof value.attempts === 'number') { return value; } return undefined; } export function assertTOTPData(obj) { if (typeof obj !== 'object' || obj === null || !('secret' in obj) || typeof obj.secret !== 'string' || !('createdAt' in obj) || typeof obj.createdAt !== 'number') { throw new Error('Invalid totp data.'); } }