Skip to content

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//"
)
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, 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"} and "X-CSRF-TOKEN" in request.headers:
        session.headers["X-CSRF-TOKEN"] = request.headers["X-CSRF-TOKEN"]
        if request.status_code == 403: # 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

[<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 CSRF_METHODS =
    [ HttpMethod.Post
    HttpMethod.Put
    HttpMethod.Patch
    HttpMethod.Delete ]

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
            List.contains method CSRF_METHODS
            && response.Headers.Contains("x-csrf-token")
        then
            httpClient.DefaultRequestHeaders.Add("x-csrf-token", response.Headers.GetValues("x-csrf-token"))

        match response.StatusCode with
        | HttpStatusCode.OK -> return Ok response
        | HttpStatusCode.Forbidden -> return! rbxRequest method url body
        | _ -> return Error response
    }

(task {
    let! result = rbxRequest HttpMethod.Post "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".freeze
METHODS = %i[post put patch delete].freeze

module APIHelper
  @client = HTTP.cookies({
      ".ROBLOSECURITY": COOKIE
  })

  def self.request(verb, url, *args)
      response = @client.request(verb, url, *args)

      if METHODS.include?(verb) && response.headers.include?("x-csrf-token")
      @client = @client.headers({
          "x-csrf-token": response.headers["x-csrf-token"]
      })

      response = request(verb, url, *args) if response.status == 403
      end

      response
  end
end

response = APIHelper.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)
            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::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>) -> 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 mut response = HTTP_CLIENT
        .request(verb.clone(), url.clone())
        .headers(headers.clone())
        .json(&body)
        .send()
        .await
        .unwrap();

    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 {
            response = HTTP_CLIENT
                .request(verb, url)
                .headers(headers.clone())
                .json(&body)
                .send()
                .await
                .unwrap();
        }
    }
    response
}

#[tokio::main]
async fn main() {
    let response = request(
        Method::POST,
        "https://auth.roblox.com//".to_string(),
        None,
    )
    .await;

    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.Headers.Contains("x-csrf-token"))
    {
        httpClient.DefaultRequestHeaders.Add("x-csrf-token", response.Headers.GetValues("x-csrf-token").First());

        if (response.StatusCode == HttpStatusCode.Forbidden)
            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"

  # Can do something like read the cookie from config.exs or a .env file in your supervisor, then pass it to this module
  # through the children list in https://hexdocs.pm/elixir/Supervisor.html#init/2
  def start_link(_initial_value) 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, response} <-
          HTTPoison.request(verb, url, encoded_body, headers) 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 response.status_code == 403 do
          request(verb, url, body)
        else
          {:ok, response}
        end
      end
    end
  end

  def main do
    start_link(nil)

    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.