‘otplib’ library throws TypeError: crypto2.createHmac is not a function error
I’m trying to use
otplib
library in cloudflare workers but seems like it is not supported because of a crypto/crypto2
dependency, at least that’s what I understood from this error:
Error generating OTP: crypto2.createHmac is not a function
TypeError: crypto2.createHmac is not a function
at Object.createDigest (core:user:portunus-worker-dev:2616:30)
at hotpDigest (core:user:portunus-worker-dev:2991:24)
at hotpToken (core:user:portunus-worker-dev:2994:45)
at totpToken (core:user:portunus-worker-dev:3107:16)
at authenticatorToken (core:user:portunus-worker-dev:3254:16)
at Authenticator.generate (core:user:portunus-worker-dev:3267:18)
at module.exports.getOTP (core:user:portunus-worker-dev:5109:38)
at async Object.handle (core:user:portunus-worker-dev:2186:37)
at async jsonError (core:user:portunus-worker-dev:2149:18) {
stack: TypeError: crypto2.createHmac is not a function
…jsonError (core:user:portunus-worker-dev:2149:18),
message: crypto2.createHmac is not a function
}
Error generating OTP: crypto2.createHmac is not a function
TypeError: crypto2.createHmac is not a function
at Object.createDigest (core:user:portunus-worker-dev:2616:30)
at hotpDigest (core:user:portunus-worker-dev:2991:24)
at hotpToken (core:user:portunus-worker-dev:2994:45)
at totpToken (core:user:portunus-worker-dev:3107:16)
at authenticatorToken (core:user:portunus-worker-dev:3254:16)
at Authenticator.generate (core:user:portunus-worker-dev:3267:18)
at module.exports.getOTP (core:user:portunus-worker-dev:5109:38)
at async Object.handle (core:user:portunus-worker-dev:2186:37)
at async jsonError (core:user:portunus-worker-dev:2149:18) {
stack: TypeError: crypto2.createHmac is not a function
…jsonError (core:user:portunus-worker-dev:2149:18),
message: crypto2.createHmac is not a function
}
2 Replies
The code:
I tried looking for a supported library for the same task but unable to find one.
const otplib = require('otplib')
const { v4: uuidv4 } = require('uuid')
// Auth handlers
module.exports.getOTP = async ({ query, url, headers, cf = {} }) => {
const { user, origin } = query // user is email
if (!user || !EMAIL_REGEXP.test(user)) {
return respondError(new HTTPError('User email not supplied', 400))
}
let u = await fetchUser(user)
console.log(u.otp_secret)
console.log(u)
if (!u) {
u = await createUser(user)
} else if (!u.otp_secret) {
// legacy user without otp_secret
u.otp_secret = uuidv4()
if (u.otp_secret === u.jwt_uuid) {
// Note: this shouldn't happen anyway
throw new Error('OTP secret and JWT UUID are the same')
}
u.updated = new Date()
await updateUser(u)
}
let otp;
try {
otp = totp.generate(u.otp_secret);
} catch (err) {
console.error("Error generating OTP:", err.message);
throw err; // you can choose to rethrow the error, or handle it here and not throw
}
const expiresAt = new Date(Date.now() + totp.timeRemaining() * 1000)
// TODO: tricky for local dev as the origin is mapped to remote cloudflare worker
const { origin: _origin } = new URL(url)
const defaultOrigin = `${_origin}/login`
// obtain locale and timezone from request for email `expiresAt` formatting
const locale = (headers.get('Accept-Language') || '').split(',')[0] || 'en'
const timeZone = cf.timezone || 'UTC'
const otplib = require('otplib')
const { v4: uuidv4 } = require('uuid')
// Auth handlers
module.exports.getOTP = async ({ query, url, headers, cf = {} }) => {
const { user, origin } = query // user is email
if (!user || !EMAIL_REGEXP.test(user)) {
return respondError(new HTTPError('User email not supplied', 400))
}
let u = await fetchUser(user)
console.log(u.otp_secret)
console.log(u)
if (!u) {
u = await createUser(user)
} else if (!u.otp_secret) {
// legacy user without otp_secret
u.otp_secret = uuidv4()
if (u.otp_secret === u.jwt_uuid) {
// Note: this shouldn't happen anyway
throw new Error('OTP secret and JWT UUID are the same')
}
u.updated = new Date()
await updateUser(u)
}
let otp;
try {
otp = totp.generate(u.otp_secret);
} catch (err) {
console.error("Error generating OTP:", err.message);
throw err; // you can choose to rethrow the error, or handle it here and not throw
}
const expiresAt = new Date(Date.now() + totp.timeRemaining() * 1000)
// TODO: tricky for local dev as the origin is mapped to remote cloudflare worker
const { origin: _origin } = new URL(url)
const defaultOrigin = `${_origin}/login`
// obtain locale and timezone from request for email `expiresAt` formatting
const locale = (headers.get('Accept-Language') || '').split(',')[0] || 'en'
const timeZone = cf.timezone || 'UTC'
const plainEmail = [
`OTP: ${otp}`,
`Magic-Link: ${origin || defaultOrigin}?user=${user}&otp=${otp}`,
`Expires at: ${expiresAt.toLocaleString(locale, {
timeZone,
timeZoneName: 'long',
})}`,
].join('\n')
await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
authorization: `Bearer ${MAIL_PASS}`,
'content-type': 'application/json',
},
body: JSON.stringify({
personalizations: [{ to: [{ email: u.email }] }],
from: { email: '[email protected]' },
subject: 'OTP',
content: [
{
type: 'text/plain',
value: plainEmail,
},
],
}),
})
return respondJSON({ payload: { message: `OTP/Magic-Link sent to ${user}` } })
}
const plainEmail = [
`OTP: ${otp}`,
`Magic-Link: ${origin || defaultOrigin}?user=${user}&otp=${otp}`,
`Expires at: ${expiresAt.toLocaleString(locale, {
timeZone,
timeZoneName: 'long',
})}`,
].join('\n')
await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
authorization: `Bearer ${MAIL_PASS}`,
'content-type': 'application/json',
},
body: JSON.stringify({
personalizations: [{ to: [{ email: u.email }] }],
from: { email: '[email protected]' },
subject: 'OTP',
content: [
{
type: 'text/plain',
value: plainEmail,
},
],
}),
})
return respondJSON({ payload: { message: `OTP/Magic-Link sent to ${user}` } })
}
I was able to solve this using web crypto api and here is the gist: https://gist.github.com/ashishjullia/49e049688ac84b298fefbf0acd52246d
Gist
create-totp-cf-workers
create-totp-cf-workers. GitHub Gist: instantly share code, notes, and snippets.