Search code examples
javascriptrequestroblox

Requests to an API with JavaScript not being authenticated


What I'm trying to do is execute JavaScript code in my browser to send a GET request to the Roblox API https://economy.roblox.com/v1/user/currency to grab the Robux (In-game currency of the game Roblox) amount in my Roblox account. The problem is that when I have a tab of roblox.com open and I execute the JavaScript code to grab the Robux amount, the requests to the API will not be authenticated. (missing .ROBLOSECURITY session cookie in the request)

The thing is, having a tab of economy.roblox.com open will allow the requests to /v1/user/currency to be authenticated, but I need to be on the roblox.com site to do the task that I have. Is there any way to send authenticated requests to economy.roblox.com while being in roblox.com?

And no, I can't just grab the .ROBLOSECURITY cookie of my account then manually include it in the JavaScript code to send it to the API, there's a reason why I can't do this.

One approach I had in mind was to make the JavaScript code redirect me or open a tab of economy.roblox.com for a split second to send the API request, then redirect back to the roblox.com url that I was in, but this didn't work as normal browser behaviour stops the rest of the JavaScript code from executing if I was redirected to another url.


Solution

  • I've been tinkering with a similar issue for the past few days and here is a solution I came up (full credit to Julli4n on Github for the bat generation code)

    javascript:(
    
    function(){
    
    function arrayBufferToBase64String(arrayBuffer){
        let res = "";
        const bytes = new Uint8Array(arrayBuffer);
        for (let i = 0; i < bytes.byteLength; i++) {
            res += String.fromCharCode(bytes[i]);
        };
        return btoa(res);
    };
    async function hashStringSha256(str){
        const uint8 = new TextEncoder().encode(str);
        const hashBuffer = await crypto.subtle.digest("SHA-256", uint8);
        return arrayBufferToBase64String(hashBuffer);
    };
    
    async function signWithKey(privateKey, data){
        const bufferResult = await crypto.subtle.sign(
            {name: "ECDSA",hash: { name: "SHA-256" }},
            privateKey,
            new TextEncoder().encode(data).buffer
        );
        return arrayBufferToBase64String(bufferResult);
    };
    
    async function getCryptoKeyPairFromDB(dbName,dbObjectName,dbObjectChildId){
        let targetVersion = 1;
        /*we want Roblox to create the DB on their end, so we do not want to interfere*/
        if ("databases" in indexedDB) {
            const databases = await indexedDB.databases();
            const database = databases.find((db) => db.name === dbName);
            if (!database) {
                return null;
            }
            if (database?.version) {
                targetVersion = database.version;
            }
        };
        return new Promise((resolve, reject) => {
            const request = indexedDB.open(dbName,targetVersion);
            request.onsuccess = () => {
                try {
                    const db = request.result;
                    const transaction = db.transaction(dbObjectName, "readonly");
                    const objectStore = transaction.objectStore(dbObjectName);
                    const get = objectStore.get(dbObjectChildId);
                    get.onsuccess = () => {
                        resolve(get.result);
                    };
                    get.onerror = () => {
                        reject(request.error);
                    };
                    transaction.oncomplete = () => {
                        db.close();
                    };
                } catch (err) {
                    reject(err);
                }
            };
            request.onerror = () => {
                reject(request.error);
            }
        })
    };
    
    
    async function generateBAT(body){
            const pair = await getCryptoKeyPairFromDB("hbaDB","hbaObjectStore","hba_keys");
            
            if (!pair?.privateKey) {
                return null;
            };
            const timestamp = Math.floor((Date.now()+1000) / 1000).toString();
            let strBody;
            if (typeof body === "object") {
                strBody = JSON.stringify(body);
            } else if (typeof body === "string") {
                strBody = body;
            };
    
            const hashedBody = await hashStringSha256(strBody);
            const payloadToSign = [hashedBody, timestamp].join("|");
            const signature = await signWithKey(pair.privateKey, payloadToSign);
    
            return [hashedBody, timestamp, signature].join("|");
        };
    
    
    /*-------------*/
    
    token = document.getElementsByName("csrf-token")[0].dataset.token;
    
    body = "";
    obody = "";
    
    headers = {
        "x-csrf-token":token,
        "x-bound-auth-token" : "",
        "accept":"application/json"};
    
    bat = generateBAT(body);
    bat.then((tok) => {
    headers["x-bound-auth-token"] = tok
    });
      
    const fetchPromise = fetch("https://economy.roblox.com/v1/user/currency",
      {"headers":headers,
      "method":"GET",
      "credentials": "include"}
    );
    
    fetchPromise
      .then((response) => {
        if (!response.ok) {
            heastr = "";
            
            token = response.headers.get("x-csrf-token");
          throw new Error(`HTTP error: ${response.status+response.statusText}`);
        };
        return response.json();
      })
      .then((data) => {
        alert(JSON.stringify(data));
      })
      .catch((error) => {
        alert(`Could not get... anything: ${error}`);
        alert(JSON.stringify(headers));
        alert(body);
      });
    
    }());
    

    Requests on Roblox that require user authentication now need a bat token in the headers, this is part of a new Roblox update to increase account security, read more about it here