cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

Used MFA with browser clickpath

Akli
Observer

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

class TOTP {
    /**
     * Generates a Time-based One-Time Password (TOTP).
     * @async
     * @param {string} key - The secret key for TOTP.
     * @param {Options} options - Optional parameters for TOTP.
     * @returns {Promise<{otp: string, expires: number}>} A promise that resolves to an object containing the OTP and its expiry time.
     */
    static async generate(key, options) {
        const _options = {
            digits: 6,
            algorithm: "SHA-1",
            encoding: "hex",
            period: 30,
            timestamp: Date.now(),
            ...options,
        };
        const epochSeconds = Math.floor(_options.timestamp / 1000);
        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(
            "raw",
            keyBuffer, {
                name: "HMAC",
                hash: {
                    name: _options.algorithm
                }
            },
            false, ["sign"]
        );
        const signature = await crypto.sign("HMAC", hmacKey, this.hex2buf(timeHex));

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

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

        return {
            otp,
            expires
        };
    }

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

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

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

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

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

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

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

    /**
     * Converts a hexadecimal string to an ArrayBuffer.
     * @param {string} hex - The hexadecimal string to convert.
     * @returns {ArrayBuffer} The ArrayBuffer representation of the hexadecimal string.
     */
    static hex2buf(hex) {
        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;
    }

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

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

const TOTP_KEY = "YOUR_SECRET_KEY_GOOGLE_AUTHENTIFICATOR";
const result = TOTP.generate(TOTP_KEY).then(function(result) {
    api.info(result);
    api.setValue("CODE_TOTP", result);
});

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

2 REPLIES 2

ChadTurner
DynaMight Legend
DynaMight Legend

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

-Chad

Cezary_Tomaszew
Dynatrace Participant
Dynatrace Participant

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:

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

 

Let me know if it helps you.  

 

Best regards,

Cezary

 

 

Featured Posts