X-CSRF-TOKEN
At this point, we’re now authenticated - but there’s one thing missing. If we try to send a POST request, you’ll notice that the request still fails.
Here's an example of some code that won't work due to the X-CSRF-TOKEN
:
import requests
cookie = "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN"
session = requests.Session()
session.cookies[".ROBLOSECURITY"] = cookie
req = session.post(
url="https://auth.roblox.com/v2/login"
)
error = req.json()
print(req.status_code)
print("Error code:", error["code"])
print("Error message:", error["message"])
Requires the http.rb and json gems.
require "http"
require "json"
COOKIE = "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN"
response = HTTP.cookies({
".ROBLOSECURITY": COOKIE
}).post("https://auth.roblox.com")
error = JSON.parse(response)
puts response.status
puts "Error code: #{error['code']}"
puts "Error message: #{error['message']}"
If your runtime doesn't support native fetch, like for example pre-v21 Node.js, you may need to install a package like node-fetch.
const COOKIE = "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN";
const response = await fetch("https://auth.roblox.com", {
headers: {
Cookie: `.ROBLOSECURITY=${COOKIE};`,
"Content-Length": "0",
},
method: "POST",
});
const body = await response.json();
console.log(response.status);
console.log(`Error code: ${body.code}`)
console.log(`Error message: ${body.message}`)
Requires the reqwest, tokio and serde_json crates.
use reqwest::header::HeaderMap;
use reqwest::Client;
use serde_json::{from_str, Value};
const COOKIE: &str = "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN";
#[tokio::main]
async fn main() {
let client = Client::new();
let mut headers = HeaderMap::new();
headers.insert(
"Cookie",
format!(".ROBLOSECURITY={};", COOKIE).parse().unwrap(),
);
let response = client
.post("https://auth.roblox.com")
.headers(headers)
.send()
.await
.unwrap();
let status = response.status(); // get status here because .text() consumes the response
let body: Value = from_str(&response.text().await.unwrap()).unwrap();
println!("{}", status);
println!("Error code: {}", body["code"]);
println!("Error message: {}", body["message"]);
}
open System.Net
open System.Net.Http
open System.Text.Json
type Error = { code: int; message: string }
[<Literal>]
let COOKIE =
"_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN"
(task {
let cookieContainer = CookieContainer()
cookieContainer.Add(Cookie(".ROBLOSECURITY", COOKIE, Domain = ".roblox.com"))
use httpClient =
new HttpClient(new HttpClientHandler(UseCookies = true, CookieContainer = cookieContainer))
let! response = httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, "https://auth.roblox.com/"))
let! content = response.Content.ReadAsStreamAsync()
let! error = JsonSerializer.DeserializeAsync<Error>(content)
printfn "%d" (int response.StatusCode)
printfn "Error code: %d" error.code
printfn "Error message: %s" error.message
})
.Wait()
using System.Net;
using System.Net.Http;
using System.Text.Json;
const string COOKIE = "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN";
var cookieContainer = new CookieContainer();
cookieContainer.Add(new Cookie(".ROBLOSECURITY", COOKIE) { Domain = ".roblox.com" });
var client = new HttpClient(new HttpClientHandler() { UseCookies = true, CookieContainer = cookieContainer });
var response = await client.PostAsync("https://auth.roblox.com/", null);
dynamic body = JsonSerializer.Deserialize<dynamic>(
await response.Content.ReadAsStringAsync()
);
Console.WriteLine(((int)response.StatusCode).ToString());
Console.WriteLine($"Error code: {body.GetProperty("code")}");
Console.WriteLine($"Error message: {body.GetProperty("message")}");
Compile with iex -S mix
, then execute RobloxAPI.main
.
Dependencies: httpoison and poison
defmodule RobloxAPI do
@roblosecurity "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN"
def main do
headers = %{Cookie: ".ROBLOSECURITY=#{@roblosecurity};"}
with {:ok, response} <- HTTPoison.post("https://auth.roblox.com", "", headers),
{:ok, body} <- Poison.decode(response.body) do
IO.puts(response.status_code)
IO.puts("Error code: #{body["code"]}")
IO.puts("Error message: #{body["message"]}")
else
{:error, error} ->
IO.inspect(error)
end
end
end
This code should output something like the following:
403
Error code: 0
Error message: Token Validation Failed
The 403 Forbidden
status code is returned when the client "is not permitted access to the resource despite providing
authentication such as insufficient permissions of the authenticated account".
If you saw this while trying to write your own code to access the API, you might ask "why is this error coming up? My .ROBLOSECURITY token is correct, and it worked when I used the "Try it out!" button on the documentation page."
The truth is that this error message isn’t referring to "token" as in your .ROBLOSECURITY token - it’s actually referring
to a header that you have to supply to all requests that change data called the X-CSRF-TOKEN
. It is used to prevent a
cross-site request forgery attack, which would enable a
malicious website to send an authenticated request to the Roblox API if you have a logged-in session in your browser.
To handle this token, each time we send a request, we'll save the X-CSRF-TOKEN
- which is present in the response headers
- to a value. Then, if the request failed with a status code of 403, and one of the errors has the code 0, we'll send the
request again with the X-CSRF-TOKEN
we just got the first request as a request header.
# With the Session object, we can just store the token in the headers dictionary, but you can pass them directly to each request as well.
import requests
cookie = "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN"
session = requests.Session()
session.cookies[".ROBLOSECURITY"] = cookie
# send first request
req = session.post(
url="https://auth.roblox.com/"
)
if "X-CSRF-Token" in req.headers: # check if token is in response headers
session.headers["X-CSRF-Token"] = req.headers["X-CSRF-Token"] # store the response header in the session
# send second request
req2 = session.post(
url="https://auth.roblox.com/"
)
print("First:", req.status_code)
print("Second:", req2.status_code)
Requires the http.rb and json gems.
require "http"
require "json"
COOKIE = "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN"
client = HTTP.cookies({
".ROBLOSECURITY": COOKIE
})
first_response = client.post("https://auth.roblox.com")
client = client.headers({
"x-csrf-token": first_response.headers["x-csrf-token"]
})
second_response = client.post("https://auth.roblox.com")
puts "First: #{first_response.status}"
puts "Second: #{second_response.status}"
If your runtime doesn't support native fetch, like for example pre-v21 Node.js, you may need to install a package like node-fetch.
const COOKIE = "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN";
const firstResponse = await fetch("https://auth.roblox.com", {
headers: {
Cookie: `.ROBLOSECURITY=${COOKIE};`,
"Content-Length": "0",
},
method: "POST",
});
const secondResponse = await fetch("https://auth.roblox.com", {
headers: {
Cookie: `.ROBLOSECURITY=${COOKIE};`,
"x-csrf-token": firstResponse.headers.get("x-csrf-token"),
"Content-Length": "0",
},
method: "POST",
});
console.log(`First: ${firstResponse.status}`);
console.log(`Second: ${secondResponse.status}`);
Requires the reqwest and tokio crates.
use reqwest::header::HeaderMap;
use reqwest::Client;
const COOKIE: &str = "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN";
#[tokio::main]
async fn main() {
let client = Client::new();
let mut headers = HeaderMap::new();
headers.insert(
"Cookie",
format!(".ROBLOSECURITY={};", COOKIE).parse().unwrap(),
);
let first_response = client
.post("https://auth.roblox.com")
.headers(headers)
.send()
.await
.unwrap();
let mut headers = HeaderMap::new();
headers.insert(
"Cookie",
format!(".ROBLOSECURITY={};", COOKIE).parse().unwrap(),
);
headers.insert(
"x-csrf-token",
first_response
.headers()
.get("x-csrf-token")
.unwrap()
.to_str()
.unwrap()
.parse()
.unwrap(),
);
let second_response = client
.post("https://auth.roblox.com")
.headers(headers)
.send()
.await
.unwrap();
println!("First: {}", first_response.status());
println!("Second: {}", second_response.status());
}
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
const string COOKIE = "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN";
var cookieContainer = new CookieContainer();
cookieContainer.Add(new Cookie(".ROBLOSECURITY", COOKIE) { Domain = ".roblox.com" });
var httpClient = new HttpClient(new HttpClientHandler() { UseCookies = true, CookieContainer = cookieContainer });
var firstResponse = await httpClient.PostAsync("https://auth.roblox.com/", null);
httpClient.DefaultRequestHeaders.Add("x-csrf-token", firstResponse.Headers.GetValues("x-csrf-token").First());
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "https://auth.roblox.com/");
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var secondResponse = await httpClient.SendAsync(requestMessage);
Console.WriteLine($"First: {firstResponse.StatusCode}");
Console.WriteLine($"Second: {secondResponse.StatusCode}");
Compile with iex -S mix
, then execute RobloxAPI.main
.
Dependencies: httpoison and poison
defmodule RobloxAPI do
@roblosecurity "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN"
def main do
headers = %{Cookie: ".ROBLOSECURITY=#{@roblosecurity};"}
with {:ok, first_response} <- HTTPoison.post("https://auth.roblox.com", "", headers),
{:ok, second_response} <-
HTTPoison.post(
"https://auth.roblox.com",
"",
Map.put(
headers,
:"x-csrf-token",
# response.headers is a list of tuples, where the first element is the header name and the second the header's value
Enum.find_value(first_response.headers, fn {name, value} ->
if name == "x-csrf-token", do: value
end)
)
) do
IO.puts("First: #{first_response.status_code}")
IO.puts("Second: #{second_response.status_code}")
else
{:error, error} ->
IO.inspect(error)
end
end
end
This program will send one request, check if the X-CSRF-Token was present in the response, and if so will store it back into the session's headers. We then repeat the first request again, and then outputs the status codes from both requests.
This code should output something like the following:
First: 403
Second: 200
This solution works - but it doesn't scale well. If we want to properly do this, we’ll put all of this logic in a function that handles our requests for us and then call that when sending requests. This is (essentially) what the request wrappers in Roblox API wrapper libraries do.
Request function¶
Here's an example of a function that does what we need:
import requests
cookie = "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN"
session = requests.Session()
session.cookies[".ROBLOSECURITY"] = cookie
def rbx_request(method, url, **kwargs):
request = session.request(method, url, **kwargs)
method = method.lower()
if method in {"post", "put", "patch", "delete"}:
if "X-CSRF-TOKEN" in request.headers:
session.headers["X-CSRF-TOKEN"] = request.headers["X-CSRF-TOKEN"]
if request.status_code == 403:
body = request.body()
if body.get("code", -1) == 0: # Request failed, send it again
request = session.request(method, url, **kwargs)
return request
req = rbx_request("POST", "https://auth.roblox.com/")
print(req.status_code)
open System.Net
open System.Net.Http
open System.Text.Json
type Error = { code: int; message: string }
[<Literal>]
let COOKIE =
"_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN"
let cookieContainer = CookieContainer()
cookieContainer.Add(Cookie(".ROBLOSECURITY", COOKIE, Domain = ".roblox.com"))
let httpClient =
new HttpClient(new HttpClientHandler(UseCookies = true, CookieContainer = cookieContainer))
let rec rbxRequest (method: HttpMethod) (url: string) (body: 'a option) =
task {
let! response =
httpClient.SendAsync(
new HttpRequestMessage(
method,
url,
Content =
new StringContent(
JsonSerializer.Serialize(
body
|> Option.defaultValue Unchecked.defaultof<'a>
),
System.Text.Encoding.UTF8,
"application/json"
)
)
)
if response.StatusCode = HttpStatusCode.Forbidden then
let! content = response.Content.ReadAsStreamAsync()
let! error = JsonSerializer.DeserializeAsync<Error> content
if error.code = 0 then
httpClient.DefaultRequestHeaders.Add(
"x-csrf-token",
response.Headers.GetValues "x-csrf-token"
|> Seq.head
)
return! rbxRequest method url body
else
return Error response
else
return Ok response
}
(task {
let! result = rbxRequest HttpMethod.Get "https://auth.roblox.com/" None
match result with
| Ok response -> printfn "%d" (int response.StatusCode)
| Error error -> failwithf "Expected status code 200, got %d" (int error.StatusCode)
})
.Wait()
Requires the http.rb and json gems.
require "http"
require "json"
COOKIE = "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN"
METHODS = %i(post put patch delete)
module APIHelper
@client = HTTP.cookies({
:".ROBLOSECURITY" => COOKIE
})
def self.rbx_request(verb, url, *args)
response = @client.request(verb, url, *args)
if METHODS.include?(verb) and response.headers.include?("x-csrf-token")
@client = @client.headers({
"x-csrf-token": response.headers["x-csrf-token"]
})
if response.status == 403
body = JSON.parse(response.body)
if body["code"] == 0
response = rbx_request(verb, url, *args)
end
end
end
response
end
end
response = APIHelper.rbx_request(:post, "https://auth.roblox.com")
puts response.status
If your runtime doesn't support native fetch, like for example pre-v21 Node.js, you may need to install a package like node-fetch.
const COOKIE = "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN";
let xCsrfToken = "";
const rbxRequest = async (verb, url, body) => {
verb = verb.toUpperCase();
let response = await fetch(url, {
headers: {
Cookie: `.ROBLOSECURITY=${COOKIE};`,
"x-csrf-token": xCsrfToken,
"Content-Length": (body?.length ?? 0).toString(),
},
method: verb,
body: body || "",
});
if (
["POST", "PUT", "PATCH", "DELETE"].includes(verb) &&
response.headers.has("x-csrf-token")
) {
xCsrfToken = response.headers.get("x-csrf-token");
if (response.status == 403) {
const responseBody = await response.json();
if (responseBody.code === 0)
response = await rbxRequest(verb, url, body);
}
}
return response;
};
const response = await rbxRequest("POST", "https://auth.roblox.com");
console.log(response.status);
Requires the reqwest, tokio, serde_json and lazy_static crates.
use lazy_static::lazy_static;
use reqwest::{header::HeaderMap, Method};
use reqwest::{Client, Response, StatusCode};
use serde_json::{from_str, Value};
use std::sync::{Arc, Mutex};
const COOKIE: &str = "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN";
lazy_static! {
static ref HTTP_CLIENT: Client = Client::new();
static ref HEADERS: Arc<Mutex<HeaderMap>> = Arc::new(Mutex::new({
let mut headers = HeaderMap::new();
headers.insert(
"Cookie",
format!(".ROBLOSECURITY={};", COOKIE).parse().unwrap(),
);
headers
}));
}
async fn request(verb: Method, url: String, body: Option<Value>) -> Result<Response, ()> {
let arc_ref = HEADERS.clone(); // get reference to the arc here so it lives as long as headers
let mut headers = arc_ref.lock().unwrap();
let response = HTTP_CLIENT
.request(verb.clone(), url.clone())
.headers(headers.clone())
.json(&body)
.send()
.await
.unwrap();
// this is kinda botched because on the branch where .text() is called and the code is not 0, there's no response to
// return because it's consumed, you can upgrade it to return the errors instead of unit, see for example
// https://github.com/zmadie/oxid_roblox/blob/5fbe3553871c048158d54269e3e0d57dbc78ab97/src/util/api_helper.rs#L54-L67
if let Some(x_csrf_token) = response.headers().get("x-csrf-token").cloned() {
headers.insert("x-csrf-token", x_csrf_token);
if response.status() == StatusCode::FORBIDDEN {
let body: Value = from_str(&response.text().await.unwrap()).unwrap();
if body["code"].as_i64().unwrap() == 0 {
return Ok(HTTP_CLIENT
.request(verb, url)
.headers(headers.clone())
.json(&body)
.send()
.await
.unwrap());
} else {
return Err(());
}
}
}
Ok(response)
}
#[tokio::main]
async fn main() {
let response = request(
Method::GET,
"https://users.roblox.com/v1/users/1".to_string(),
None,
)
.await
.unwrap();
println!("{}", response.status());
}
using System.Net;
using System.Net.Http;
using System.Text.Json;
const string COOKIE = "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN";
var cookieContainer = new CookieContainer();
cookieContainer.Add(new Cookie(".ROBLOSECURITY", COOKIE) { Domain = ".roblox.com" });
var httpClient = new HttpClient(new HttpClientHandler() { UseCookies = true, CookieContainer = cookieContainer });
async Task<HttpResponseMessage> Request(HttpMethod method, string url, dynamic body = null)
{
var response = await httpClient.SendAsync(
new HttpRequestMessage(
method,
url
)
{
Content =
new StringContent(
JsonSerializer.Serialize(body ?? new { }),
Encoding.UTF8,
"application/json"
)
}
);
if (response.StatusCode == HttpStatusCode.Forbidden)
{
dynamic error = await JsonSerializer.DeserializeAsync<dynamic>(await response.Content.ReadAsStreamAsync());
if (error.GetProperty("code").GetInt32() == 0)
{
httpClient.DefaultRequestHeaders.Add("x-csrf-token", response.Headers.GetValues("x-csrf-token").First());
return await Request(method, url, body);
}
}
return response;
}
var response = await Request(HttpMethod.Post, "https://auth.roblox.com");
Console.WriteLine(response.StatusCode);
Compile with iex -S mix
, then execute RobloxAPI.main
.
Dependencies: httpoison and poison
defmodule RobloxAPI do
use Agent
@roblosecurity "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_TOKEN"
def start_link do
Agent.start_link(fn -> %{Cookie: ".ROBLOSECURITY=#{@roblosecurity}"} end, name: __MODULE__)
end
defp request(verb, url, body \\ %{}) do
headers = Agent.get(__MODULE__, & &1)
with {:ok, encoded_body} <- Poison.encode(body),
{:ok, %HTTPoison.Response{status_code: 403} = response} <-
HTTPoison.request(verb, url, encoded_body, headers),
{:ok, body} <-
Poison.decode(response.body) do
xcsrf_token =
Enum.find_value(response.headers, fn {name, value} ->
if name == "x-csrf-token", do: value
end)
if xcsrf_token == nil do
{:ok, response}
else
headers = Map.put(headers, :"x-csrf-token", xcsrf_token)
Agent.update(__MODULE__, fn _ -> headers end)
if body["code"] == 0 do
request(verb, url, body)
else
{:ok, response}
end
end
else
{:ok, response} ->
{:ok, response}
{:error, error} ->
{:error, error}
end
end
def main do
start_link()
case request(:post, "https://auth.roblox.com") do
{:ok, %HTTPoison.Response{status_code: status_code}} ->
IO.puts(status_code)
{:error, error} ->
IO.inspect(error)
end
end
end
This code should output something like the following:
200
Now that we’ve done this, sending any kind of requests to the API is boilerplate-less.