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