# Used MFA with browser clickpath

**URL:** <https://community.dynatrace.com/t/used-mfa-with-browser-clickpath/48940>\
**Category:** Synthetic Monitoring\
**Tags:** browser-monitors, synthetic-monitoring\
**Created:** [November 18, 2024, 3:28pm UTC](https://community.dynatrace.com/t/used-mfa-with-browser-clickpath/48940 "2024-11-18T15:28:58Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![Akli](https://avatars.discourse-cdn.com/v4/letter/a/82dd89/32.png) [@Akli](https://community.dynatrace.com/u/Akli)\
**Post date:** [November 18, 2024, 3:28pm UTC](https://community.dynatrace.com/t/used-mfa-with-browser-clickpath/48940/1 "2024-11-18T15:28:58Z")

</div>

Hello  
we need to use MFA in our browser clickpath  
here is the topic:  
The application uses authentication via OKTA portal  
Google authtificator is used as a software based authenticator  
we need a script that is used by the scenario to generate a TOTP code that can be placed in the okta page to validate the authentication of Google athtifcator to authenticate with the application.

I managed to get this code to work on a browser but it doesn’t work on Dynatrace

const crypto = (globalThis.crypto || require(“crypto”).webcrypto).subtle;

const base32 = {  
&nbsp; &nbsp; 50: 26,  
&nbsp; &nbsp; 51: 27,  
&nbsp; &nbsp; 52: 28,  
&nbsp; &nbsp; 53: 29,  
&nbsp; &nbsp; 54: 30,  
&nbsp; &nbsp; 55: 31,  
&nbsp; &nbsp; 65: 0,  
&nbsp; &nbsp; 66: 1,  
&nbsp; &nbsp; 67: 2,  
&nbsp; &nbsp; 68: 3,  
&nbsp; &nbsp; 69: 4,  
&nbsp; &nbsp; 70: 5,  
&nbsp; &nbsp; 71: 6,  
&nbsp; &nbsp; 72: 7,  
&nbsp; &nbsp; 73: 8,  
&nbsp; &nbsp; 74: 9,  
&nbsp; &nbsp; 75: 10,  
&nbsp; &nbsp; 76: 11,  
&nbsp; &nbsp; 77: 12,  
&nbsp; &nbsp; 78: 13,  
&nbsp; &nbsp; 79: 14,  
&nbsp; &nbsp; 80: 15,  
&nbsp; &nbsp; 81: 16,  
&nbsp; &nbsp; 82: 17,  
&nbsp; &nbsp; 83: 18,  
&nbsp; &nbsp; 84: 19,  
&nbsp; &nbsp; 85: 20,  
&nbsp; &nbsp; 86: 21,  
&nbsp; &nbsp; 87: 22,  
&nbsp; &nbsp; 88: 23,  
&nbsp; &nbsp; 89: 24,  
&nbsp; &nbsp; 90: 25,  
};

class TOTP {  
&nbsp; &nbsp; /\*\*  
&nbsp; &nbsp; &nbsp;\* Generates a Time-based One-Time Password (TOTP).  
&nbsp; &nbsp; &nbsp;\* @async  
&nbsp; &nbsp; &nbsp;\* @param {string} key - The secret key for TOTP.  
&nbsp; &nbsp; &nbsp;\* @param {Options} options - Optional parameters for TOTP.  
&nbsp; &nbsp; &nbsp;\* @returns {Promise\<{otp: string, expires: number}\>} A promise that resolves to an object containing the OTP and its expiry time.  
&nbsp; &nbsp; &nbsp;\*/  
&nbsp; &nbsp; static async generate(key, options) {  
&nbsp; &nbsp; &nbsp; &nbsp; const \_options = {  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; digits: 6,  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; algorithm: “SHA-1”,  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; encoding: “hex”,  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; period: 30,  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; timestamp: Date.now(),  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; …options,  
&nbsp; &nbsp; &nbsp; &nbsp; };  
&nbsp; &nbsp; &nbsp; &nbsp; const epochSeconds = Math.floor(\_options.timestamp / 1000);  
&nbsp; &nbsp; &nbsp; &nbsp; const timeHex = this.dec2hex(Math.floor(epochSeconds / \_options.period)).padStart(16, “0”);

const keyBuffer = \_options.encoding === “hex” ? this.base32ToBuffer(key) : this.asciiToBuffer(key);

const hmacKey = await crypto.importKey(  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; “raw”,  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; keyBuffer, {  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; name: “HMAC”,  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; hash: {  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; name: \_options.algorithm  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; },  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; false, [“sign”]  
&nbsp; &nbsp; &nbsp; &nbsp; );  
&nbsp; &nbsp; &nbsp; &nbsp; const signature = await crypto.sign(“HMAC”, hmacKey, this.hex2buf(timeHex));

const signatureHex = this.buf2hex(signature);  
&nbsp; &nbsp; &nbsp; &nbsp; const offset = this.hex2dec(signatureHex.slice(-1)) \* 2;  
&nbsp; &nbsp; &nbsp; &nbsp; const masked = this.hex2dec(signatureHex.slice(offset, offset + 8)) & 0x7fffffff;  
&nbsp; &nbsp; &nbsp; &nbsp; const otp = masked.toString().slice(-\_options.digits);

const period = \_options.period \* 1000;  
&nbsp; &nbsp; &nbsp; &nbsp; const expires = Math.ceil((\_options.timestamp + 1) / period) \* period;

return {  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; otp,  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; expires  
&nbsp; &nbsp; &nbsp; &nbsp; };  
&nbsp; &nbsp; }

/\*\*  
&nbsp; &nbsp; &nbsp;\* Converts a hexadecimal string to a decimal number.  
&nbsp; &nbsp; &nbsp;\* @param {string} hex - The hex string.  
&nbsp; &nbsp; &nbsp;\* @returns {number} The decimal representation.  
&nbsp; &nbsp; &nbsp;\*/  
&nbsp; &nbsp; static hex2dec(hex) {  
&nbsp; &nbsp; &nbsp; &nbsp; return parseInt(hex, 16);  
&nbsp; &nbsp; }

/\*\*  
&nbsp; &nbsp; &nbsp;\* Converts a decimal number to a hexadecimal string.  
&nbsp; &nbsp; &nbsp;\* @param {number} dec - The decimal number.  
&nbsp; &nbsp; &nbsp;\* @returns {string} The hex representation.  
&nbsp; &nbsp; &nbsp;\*/  
&nbsp; &nbsp; static dec2hex(dec) {  
&nbsp; &nbsp; &nbsp; &nbsp; return (dec \< 15.5 ? “0” : “”) + Math.round(dec).toString(16);  
&nbsp; &nbsp; }

/\*\*  
&nbsp; &nbsp; &nbsp;\* Converts a base32 encoded string to an ArrayBuffer.  
&nbsp; &nbsp; &nbsp;\* @param {string} str - The base32 encoded string to convert.  
&nbsp; &nbsp; &nbsp;\* @returns {ArrayBuffer} The ArrayBuffer representation of the base32 encoded string.  
&nbsp; &nbsp; &nbsp;\*/  
&nbsp; &nbsp; static base32ToBuffer(str) {  
&nbsp; &nbsp; &nbsp; &nbsp; str = str.toUpperCase();  
&nbsp; &nbsp; &nbsp; &nbsp; let length = str.length;  
&nbsp; &nbsp; &nbsp; &nbsp; while (str.charCodeAt(length - 1) === 61) length--; // Remove pads

const bufferSize = (length \* 5) / 8; // Estimate buffer size  
&nbsp; &nbsp; &nbsp; &nbsp; const buffer = new Uint8Array(bufferSize);  
&nbsp; &nbsp; &nbsp; &nbsp; let value = 0,  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bits = 0,  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; index = 0;

for (let i = 0; i \< length; i++) {  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const charCode = base32[str.charCodeAt(i)];  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (charCode === undefined) throw new Error(“Invalid base32 character in key”);  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; value = (value \<\< 5) | charCode;  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bits += 5;

if (bits \>= 😎 buffer[index++] = value \>\>\> (bits -= 8);  
&nbsp; &nbsp; &nbsp; &nbsp; }  
&nbsp; &nbsp; &nbsp; &nbsp; return buffer.buffer;  
&nbsp; &nbsp; }

/\*\*  
&nbsp; &nbsp; &nbsp;\* Converts an ASCII string to an ArrayBuffer.  
&nbsp; &nbsp; &nbsp;\* @param {string} str - The ASCII string to convert.  
&nbsp; &nbsp; &nbsp;\* @returns {ArrayBuffer} The ArrayBuffer representation of the ASCII string.  
&nbsp; &nbsp; &nbsp;\*/  
&nbsp; &nbsp; static asciiToBuffer(str) {  
&nbsp; &nbsp; &nbsp; &nbsp; const buffer = new Uint8Array(str.length);  
&nbsp; &nbsp; &nbsp; &nbsp; for (let i = 0; i \< str.length; i++) {  
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; buffer[i] = str.charCodeAt(i);  
&nbsp; &nbsp; &nbsp; &nbsp; }  
&nbsp; &nbsp; &nbsp; &nbsp; return buffer.buffer;  
&nbsp; &nbsp; }

/\*\*  
&nbsp; &nbsp; &nbsp;\* Converts a hexadecimal string to an ArrayBuffer.  
&nbsp; &nbsp; &nbsp;\* @param {string} hex - The hexadecimal string to convert.  
&nbsp; &nbsp; &nbsp;\* @returns {ArrayBuffer} The ArrayBuffer representation of the hexadecimal string.  
&nbsp; &nbsp; &nbsp;\*/  
&nbsp; &nbsp; static hex2buf(hex) {  
&nbsp; &nbsp; &nbsp; &nbsp; const buffer = new Uint8Array(hex.length / 2);

for (let i = 0, j = 0; i \< hex.length; i += 2, j++) buffer[j] = this.hex2dec(hex.slice(i, i + 2));

return buffer.buffer;  
&nbsp; &nbsp; }

/\*\*  
&nbsp; &nbsp; &nbsp;\* Converts an ArrayBuffer to a hexadecimal string.  
&nbsp; &nbsp; &nbsp;\* @param {ArrayBuffer} buffer - The ArrayBuffer to convert.  
&nbsp; &nbsp; &nbsp;\* @returns {string} The hexadecimal string representation of the buffer.  
&nbsp; &nbsp; &nbsp;\*/  
&nbsp; &nbsp; static buf2hex(buffer) {  
&nbsp; &nbsp; &nbsp; &nbsp; return […new Uint8Array(buffer)].map((x) =\> x.toString(16).padStart(2, “0”)).join(“”);  
&nbsp; &nbsp; }

/\*\*  
&nbsp; &nbsp; &nbsp;\* The cryptographic interface used for HMAC operations.  
&nbsp; &nbsp; &nbsp;\* Chooses the Web Crypto API if available, otherwise falls back to Node’s crypto module.  
&nbsp; &nbsp; &nbsp;\* @type {SubtleCrypto}  
&nbsp; &nbsp; &nbsp;\*/  
}

const TOTP\_KEY = “YOUR\_SECRET\_KEY\_GOOGLE\_AUTHENTIFICATOR”;  
const result = TOTP.generate(TOTP\_KEY).then(function(result) {  
&nbsp; &nbsp; api.info(result);  
&nbsp; &nbsp; api.setValue(“CODE\_TOTP”, result);  
});

has anyone managed to get MFA working with Google Authenticator in Browser clipart ?

---

<div class="post-metadata">

**Author:** ![ChadTurner](https://dub1.discourse-cdn.com/dynatrace/user_avatar/community.dynatrace.com/chadturner/32/161_2.png) [@ChadTurner](https://community.dynatrace.com/u/ChadTurner)\
**Post date:** [December 31, 2024, 7:46pm UTC](https://community.dynatrace.com/t/used-mfa-with-browser-clickpath/48940/2 "2024-12-31T19:46:31Z")

</div>

Once a vendor achieves this, they will strike gold. I don’t know of any Synthetic vendor that offers MFA, 2FA etc…

---

<div class="post-metadata">

**Author:** ![Cezary\_Tomaszew](https://avatars.discourse-cdn.com/v4/letter/c/f07891/32.png) [@Cezary\_Tomaszew](https://community.dynatrace.com/u/Cezary_Tomaszew)\
**Post date:** [January 9, 2025, 8:34am UTC](https://community.dynatrace.com/t/used-mfa-with-browser-clickpath/48940/3 "2025-01-09T08:34:58Z")

</div>

Hi,  
If you use a TOTP token for authentication, the JavaScript step is currently the only way to achieve it. We’re working on better support for it.

1. I do not know why your code does not work, but try this one:

```javascript
async function getOTP(secretKeyArrayBuffer) {
// Define the time step and T0. T0 is commonly 0 in Unix epoch.
const epoch = Math.floor(Date.now() / 1000);
const timeStep = 30;
const counter = Math.floor(epoch / timeStep);

// Convert the counter to an ArrayBuffer (8 bytes, big-endian)
const counterBuffer = new ArrayBuffer(8);
const counterView = new DataView(counterBuffer);
counterView.setUint32(4, counter); // Set the last 4 bytes as the counter

// Import the secret key for HMAC-SHA-1
const key = await window.crypto.subtle.importKey(
'raw', // raw format of the key
secretKeyArrayBuffer, // the key
{
name: 'HMAC',
hash: {
name: 'SHA-1'
}
}, // algorithm details
false, // not extractable
['sign'] // only need to sign
);

// Generate HMAC-SHA-1 of the counter using the secret key
const hmacBuffer = await window.crypto.subtle.sign('HMAC', key, counterBuffer);

// Convert HMAC result into a Uint8Array for easy byte manipulation
const hmac = new Uint8Array(hmacBuffer);

// Dynamic Truncation to get a 4-byte string and then a 31-bit number
const offset = hmac[hmac.length - 1] & 0xf;
const binary = ((hmac[offset] & 0x7f) << 24) | (hmac[offset + 1] << 16) | (hmac[offset + 2] << 😎 | (hmac[offset + 3]);

// Modulo 1,000,000 to get the final OTP
const otp = binary % 1000000;

// Ensure the OTP is a 6-digit number
return otp.toString().padStart(6, '0');
}

function convertBase32ToArrayBuffer(base32) {
const base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
let bits = "";
let paddingCount = 0;

base32 = base32.toUpperCase();

for (let i = 0; i < base32.length; i++) {
const char = base32.charAt(i);
if (char === "=") { // Padding character, stop processing
break;
}
const charIndex = base32Alphabet.indexOf(char);
if (charIndex === -1) {
throw new Error("Invalid Base32 character encountered.");
}
bits += charIndex.toString(2).padStart(5, '0');
}

// Convert bits to bytes
const bytes = [];
for (let i = 0; i < bits.length; i += 😎 {
const byte = bits.substring(i, i + 8);
// Incomplete byte (less than 8 bits) ignored
if (byte.length === 😎 {
bytes.push(parseInt(byte, 2));
}
}

// Convert byte array to ArrayBuffer
const arrayBuffer = new ArrayBuffer(bytes.length);
const uint8Array = new Uint8Array(arrayBuffer);
for (let i = 0; i < bytes.length; i++) {
uint8Array[i] = bytes[i];
}

return arrayBuffer;
}

var totpSecret = api.getCredential("CREDENTIALS_VAULT-XXXXXXXXXXX", "token");
api.startAsyncSyntheticEvent();
getOTP(convertBase32ToArrayBuffer(totpSecret)).then(otp => {
api.setValue('totp_token', otp);
api.finish();
});

```

2. In the action where you put the code in yout input text, don’t forget to pass the variable:

 ![Cezary_Tomaszew_0-1736411435819.png](https://global.discourse-cdn.com/dynatrace/original/3X/e/e/eef5ebaee4c4eaeb3b66dc2e4e615c62d951304f.png)

Let me know if it helps you.

Best regards,

Cezary

---

<div class="post-metadata">

**Author:** ![Rob\_Doce](https://avatars.discourse-cdn.com/v4/letter/r/ecd19e/32.png) [@Rob\_Doce](https://community.dynatrace.com/u/Rob_Doce)\
**Post date:** [October 1, 2026, 12:18pm UTC](https://community.dynatrace.com/t/used-mfa-with-browser-clickpath/48940/4 "2026-10-01T12:18:27Z")

</div>

Thank you very much. I had some problems with your code (it was 99,9% perfect), but now is working. I had to bypass CSP in the advenced setup settings and there were also some syntax errors in this line:

```auto
const binary = ((hmac[offset] & 0x7f) << 24) | (hmac[offset + 1] << 16) | (hmac[offset + 2] << | (hmac[offset + 3]);

```

I attach the script:

```javascript
async function getOTP(secretKeyArrayBuffer) {

    const epoch = Math.floor(Date.now() / 1000);
    const counter = Math.floor(epoch / 30);

    // 8 bytes, Big Endian
    const counterBuffer = new ArrayBuffer(8);
    const counterView = new DataView(counterBuffer);

    counterView.setUint32(0, 0);
    counterView.setUint32(4, counter);

    // Importar clave para HMAC-SHA1
    const key = await crypto.subtle.importKey(
        "raw",
        secretKeyArrayBuffer,
        {
            name: "HMAC",
            hash: "SHA-1"
        },
        false,
        ["sign"]
    );

    // HMAC
    const hmacBuffer = await crypto.subtle.sign(
        "HMAC",
        key,
        counterBuffer
    );

    const hmac = new Uint8Array(hmacBuffer);

    // Dynamic truncation
    const offset = hmac[hmac.length - 1] & 0x0f;

    const binary =
        ((hmac[offset] & 0x7f) << 24) |
        ((hmac[offset + 1] & 0xff) << 16) |
        ((hmac[offset + 2] & 0xff) << 8) |
        (hmac[offset + 3] & 0xff);

    const otp = binary % 1000000;

    return otp.toString().padStart(6, "0");
}

function base32ToArrayBuffer(base32) {

    const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

    base32 = base32
        .toUpperCase()
        .replace(/[\s=]/g, "");

    let bits = "";

    for (let i = 0; i < base32.length; i++) {

        const index = alphabet.indexOf(base32[i]);

        if (index === -1) {
            throw new Error(
                "Invalid Base32 character: " + base32[i]
            );
        }

        bits += index.toString(2).padStart(5, "0");
    }

    const byteCount = Math.floor(bits.length / 8);
    const bytes = new Uint8Array(byteCount);

    for (let i = 0; i < byteCount; i++) {

        const start = i * 8;
        const byteBits = bits.substring(start, start + 8);

        bytes[i] = parseInt(byteBits, 2);
    }

    return bytes.buffer;
}

// Obtener secreto desde Dynatrace Credential Vault
const totpSecret = api.getCredential(
    "CREDENTIALS_VAULT-XXXXXXXXXXX",
    "token"
);

api.startAsyncSyntheticEvent();

getOTP(
    base32ToArrayBuffer(totpSecret)
)
.then(function(otp) {

    api.info("Generated TOTP: " + otp);

    api.setValue(
        "totp_token",
        otp
    );

    api.finish();

})
.catch(function(error) {

    api.error(
        "TOTP generation error: " + error.message
    );

    api.fail(
        "Unable to generate TOTP"
    );

});

```

For me, this script is working 100%

Thank you&nbsp;Cezary\_Tomaszew!!!
