remix-auth-totp
Version:
A Time-Based One-Time Password (TOTP) Authentication Strategy for Remix-Auth.
107 lines (106 loc) • 2.93 kB
JavaScript
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.');
}
}