Skip to main content

Changelog

Updates, changes, and improvements at Retool.

Refer to the stable and edge release notes for detailed information about self-hosted releases.

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:

  1. On the Resources page, open a REST API, GraphQL, OpenAPI, or gRPC resource that uses Custom Auth.
  2. Review the code in each JavaScript step of the authentication flow.
  3. Look for require() calls that load a preinstalled library, for example require('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​

note

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.

LibraryReplacement
jsonwebtokencrypto.sign() or crypto.createHmac() with Buffer
josecrypto.sign() or crypto.createHmac() with Buffer
crypto-jscrypto.createHmac(), crypto.createHash(), and Buffer
moment and moment-timezoneDate and Intl.DateTimeFormat
uuidcrypto.randomUUID()
atobBuffer.from(value, 'base64').toString('latin1')
request and axiosThe 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:

Before
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.

After
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():

Before
const jwt = require('jsonwebtoken');

return jwt.sign({ sub: userId }, jwtSecret, { expiresIn: '1h' });
After
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.

After
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:

Before
const jwt = require('jsonwebtoken');

return jwt.decode(token);
After
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-jsNode.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.

momentJavaScript
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.

After
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():

Before
const { v4: uuidv4 } = require('uuid');

return uuidv4();
After
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:

Before
const axios = require('axios');

const response = await axios.post('https://example.com/oauth/token', { userId });

return { status: response.status, body: response.data };
After
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 401 or 500. Check status in your step, or in a later step, before you use the response body.
  • It doesn't follow redirects. If your endpoint responds with a 3xx status, res.headers.location contains 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:

  1. Click Save changes.
  2. Click Test auth workflow and complete any form steps.
  3. 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.
  4. 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.