Deprecation of preinstalled libraries in custom authentication JavaScript steps
As part of our continued efforts to keep Retool secure and reduce vulnerabilities, Retool is deprecating several preinstalled third-party libraries, such as jsonwebtoken, crypto-js, moment, and uuid, currently available in custom authentication JavaScript steps. Retool plans to remove these libraries from cloud and self-hosted instances in Q2 2027.
If your custom authentication steps use any of these libraries, you can migrate to Node.js built-in equivalents, such as crypto and Buffer, well ahead of the removal date. Migrating before then avoids any disruption to your authentication flows.
Only REST API, GraphQL, OpenAPI, and gRPC resources that use Custom Auth with a JavaScript step that loads a preinstalled library are affected. JavaScript steps that use Node.js built-in modules, such as crypto, continue to work without changes.
Who is affected
Any customers who have resources that use Custom Auth as its authentication method and one of its JavaScript steps loads a preinstalled library. Custom authentication steps that only use Form (modal), API Request, or Define a variable steps aren't affected, and neither are JavaScript steps that don't call require().
What to do
To check whether a resource is affected:
- On the Resources page, open a REST API, GraphQL, OpenAPI, or gRPC resource that uses Custom Auth.
- Review the code in each JavaScript step of the authentication flow.
- Look for
require()calls that load a preinstalled library, for examplerequire('jsonwebtoken').
Calls that load Node.js built-in modules, such as require('crypto'), don't need any changes. For the full list of preinstalled libraries, refer to Custom authentication JavaScript libraries.
Migrate to a supported replacement library
The following migration guide only applies when using inline custom authentication steps.
Each library that custom authentication JavaScript steps commonly load has a replacement that is built in to Node.js or JavaScript. The following table lists the recommended replacement for each.
| Library | Replacement |
|---|---|
jsonwebtoken | crypto.sign() or crypto.createHmac() with Buffer |
jose | crypto.sign() or crypto.createHmac() with Buffer |
crypto-js | crypto.createHmac(), crypto.createHash(), and Buffer |
moment and moment-timezone | Date and Intl.DateTimeFormat |
uuid | crypto.randomUUID() |
atob | Buffer.from(value, 'base64').toString('latin1') |
request and axios | The https module, or an API Request step in the authentication flow |
JavaScript steps don't have access to the global fetch() function. To make HTTP requests, use the https module or add a separate API Request step. For more information, refer to "Replace request and axios" below.
If your step uses a library that isn't listed here, contact Retool Support for help identifying a replacement.
Replace jsonwebtoken
Most custom authentication flows use jsonwebtoken to sign a JSON Web Token (JWT) with jwt.sign(). A JWT is three base64url-encoded segments joined with periods: a header, a payload, and a signature. You can build it with the crypto module and Buffer.
The following examples use userId, jwtPrivateKey, and jwtSecret as placeholders. Replace them with the variables, form values, or configuration variables that your flow already uses.
Sign a token with RS256
The following step signs a token with an RSA private key using jsonwebtoken:
const jwt = require('jsonwebtoken');
return jwt.sign({ sub: userId }, jwtPrivateKey, {
algorithm: 'RS256',
expiresIn: '1h',
});
The following step produces the same token using crypto.sign(). jwtPrivateKey must be a PEM-encoded private key, the same format that jsonwebtoken accepts.
const crypto = require('crypto');
const base64url = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64url');
const now = Math.floor(Date.now() / 1000);
const header = base64url({ alg: 'RS256', typ: 'JWT' });
const payload = base64url({ sub: userId, iat: now, exp: now + 3600 });
const signingInput = `${header}.${payload}`;
const signature = crypto
.sign('sha256', Buffer.from(signingInput), jwtPrivateKey)
.toString('base64url');
return `${signingInput}.${signature}`;
jsonwebtoken automatically adds an iat (issued at) claim and converts expiresIn into an exp claim. When you build the token yourself, set both claims explicitly in seconds since the Unix epoch. In the example, now + 3600 matches expiresIn: '1h'. Add any other claims your API requires, such as iss or aud, to the payload object.
To use RS384 or RS512, change alg in the header and replace sha256 with sha384 or sha512.
To use PS256, set alg to PS256 and pass Probabilistic Signature Scheme (PSS) padding options with the key:
const signature = crypto
.sign('sha256', Buffer.from(signingInput), {
key: jwtPrivateKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
})
.toString('base64url');
To use EdDSA with an Ed25519 key, set alg to EdDSA and pass null as the algorithm: crypto.sign(null, Buffer.from(signingInput), jwtPrivateKey).
Sign a token with HS256
If your step signs tokens with a shared secret, use crypto.createHmac():
const jwt = require('jsonwebtoken');
return jwt.sign({ sub: userId }, jwtSecret, { expiresIn: '1h' });
const crypto = require('crypto');
const base64url = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64url');
const now = Math.floor(Date.now() / 1000);
const header = base64url({ alg: 'HS256', typ: 'JWT' });
const payload = base64url({ sub: userId, iat: now, exp: now + 3600 });
const signingInput = `${header}.${payload}`;
const signature = crypto
.createHmac('sha256', jwtSecret)
.update(signingInput)
.digest('base64url');
return `${signingInput}.${signature}`;
Sign a token with ES256
For elliptic curve keys, pass dsaEncoding: 'ieee-p1363' to crypto.sign(). JWTs require this signature format, and Node.js uses a different format by default.
const crypto = require('crypto');
const base64url = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64url');
const now = Math.floor(Date.now() / 1000);
const header = base64url({ alg: 'ES256', typ: 'JWT' });
const payload = base64url({ sub: userId, iat: now, exp: now + 3600 });
const signingInput = `${header}.${payload}`;
const signature = crypto
.sign('sha256', Buffer.from(signingInput), {
key: jwtPrivateKey,
dsaEncoding: 'ieee-p1363',
})
.toString('base64url');
return `${signingInput}.${signature}`;
Read the claims in a token
If your step uses jwt.decode() to read the claims in a token returned by your API, decode the payload segment with Buffer:
const jwt = require('jsonwebtoken');
return jwt.decode(token);
const [, payload] = token.split('.');
return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
Like jwt.decode(), this reads the claims without verifying the signature.
Replace jose
jose builds the same tokens as jsonwebtoken. If your step passes an HS, RS, PS, ES, or EdDSA algorithm to setProtectedHeader(), replace calls such as new SignJWT(...).sign(key) with the matching example in "Replace jsonwebtoken" above. You don't need an equivalent of importPKCS8(), because crypto.sign() accepts a PEM-encoded key directly.
If your step uses other jose features, such as encrypted tokens (JWE), contact Retool Support for help identifying a replacement.
Replace crypto-js
Custom authentication flows typically use crypto-js to compute an HMAC signature or hash for a request, or to encode values as base64. The crypto module and Buffer produce identical output.
| crypto-js | Node.js |
|---|---|
CryptoJS.HmacSHA256(message, secret).toString() | crypto.createHmac('sha256', secret).update(message).digest('hex') |
CryptoJS.HmacSHA256(message, secret).toString(CryptoJS.enc.Base64) | crypto.createHmac('sha256', secret).update(message).digest('base64') |
CryptoJS.SHA256(message).toString() | crypto.createHash('sha256').update(message).digest('hex') |
CryptoJS.MD5(message).toString() | crypto.createHash('md5').update(message).digest('hex') |
CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(text)) | Buffer.from(text).toString('base64') |
CryptoJS.enc.Base64.parse(encoded).toString(CryptoJS.enc.Utf8) | Buffer.from(encoded, 'base64').toString('utf8') |
For other hash algorithms, replace sha256 with the algorithm name, such as sha1 or sha512. Add const crypto = require('crypto'); at the start of any step that uses the crypto module.
If your step encrypts or decrypts data with CryptoJS.AES, use crypto.createCipheriv() and crypto.createDecipheriv() with an explicit key and initialization vector, as described in the Node.js cipher documentation. When you pass a passphrase string to CryptoJS.AES, it derives the key in a way that crypto doesn't replicate. Confirm the key, initialization vector, and cipher mode your API expects before you migrate.
Replace moment
Authentication flows typically use moment to generate timestamps. Native Date methods cover these cases.
| moment | JavaScript |
|---|---|
moment().toISOString() | new Date().toISOString() |
moment().unix() | Math.floor(Date.now() / 1000) |
moment().valueOf() | Date.now() |
moment().add(1, 'hour').toISOString() | new Date(Date.now() + 60 * 60 * 1000).toISOString() |
moment.utc().format('YYYY-MM-DD') | new Date().toISOString().slice(0, 10) |
moment.utc().format('YYYY-MM-DDTHH:mm:ss[Z]') | new Date().toISOString().split('.')[0] + 'Z' |
To format a date in a specific time zone, which moment-timezone provides, use Intl.DateTimeFormat with the timeZone option. For all formatting options, refer to the MDN Intl.DateTimeFormat reference.
return new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
dateStyle: 'short',
timeStyle: 'long',
}).format(new Date());
Replace uuid
Replace uuid version 4 identifiers with crypto.randomUUID():
const { v4: uuidv4 } = require('uuid');
return uuidv4();
const crypto = require('crypto');
return crypto.randomUUID();
crypto.randomUUID() only generates version 4 identifiers. If your API requires another UUID version, contact Retool Support.
Replace request and axios
If your step makes an HTTP request with request or axios, you can move the request into a separate API Request step and reference its response in later steps. This keeps the request out of your JavaScript code.
If the request needs to stay in the JavaScript step, use the https module. The following example sends a JSON POST request and returns the status code and parsed response body:
const axios = require('axios');
const response = await axios.post('https://example.com/oauth/token', { userId });
return { status: response.status, body: response.data };
const https = require('https');
const body = JSON.stringify({ userId });
return await new Promise((resolve, reject) => {
const req = https.request(
'https://example.com/oauth/token',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
},
},
(res) => {
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
const data = Buffer.concat(chunks).toString('utf8');
try {
resolve({ status: res.statusCode, body: data ? JSON.parse(data) : null });
} catch (err) {
reject(err);
}
});
}
);
req.on('error', reject);
req.write(body);
req.end();
});
The https module behaves differently from axios in two ways:
- It doesn't reject the promise for error status codes such as
401or500. Checkstatusin your step, or in a later step, before you use the response body. - It doesn't follow redirects. If your endpoint responds with a
3xxstatus,res.headers.locationcontains the redirect URL. Send the request to the final URL instead of the one that redirects.
If the response isn't JSON, return data instead of JSON.parse(data).
The code executor blocks requests from JavaScript steps to link-local addresses and some private network ranges. To call an internal service, use an API Request step instead.
Test your migrated flow
After you update each JavaScript step, confirm that the authentication flow still works:
- Click Save changes.
- Click Test auth workflow and complete any form steps.
- Review the JavaScript step output in the log and confirm that it returns the value your API expects. For a JWT, you can decode the token to compare its header and claims with the token the previous step produced.
- Run a query that uses the resource to confirm that your API accepts the new credentials.
If a step fails with an error such as Cannot find module 'jsonwebtoken', the step still loads a preinstalled library. Replace the remaining require() call using the examples in this post.