X-CSRF-Token

From RoAPI

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

session = requests.Session()
session.cookies[".ROBLOSECURITY"] = cookie
req = session.post(
    url="https://auth.roblox.com/"
)

print(req.status_code)  # output the status code
print(req.json()["errors"][0]["message"])  # output the error message
# Uses the http.rb gem. Run "gem install http" on your terminal to install it
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")

puts response.status # Print the status code
puts JSON.parse(response)["errors"][0]["message"] # Print the error message
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(body["errors"][0]["message"]);
// npm install node-fetch
import fetch from "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(body["errors"][0]["message"]);
/*
    Cargo.toml dependencies:
    reqwest = { version = "0.11.4" }
    tokio = { version = "1.11.0", features = ["macros", "rt-multi-thread"]}
    serde_json = { vesion = "1.0.67"}
*/
use reqwest::header::{HeaderMap, HeaderValue};
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",
        HeaderValue::from_str(&format!(".ROBLOSECURITY={};", COOKIE)).unwrap(),
    );
    headers.insert("Content-Length", HeaderValue::from_str("0").unwrap());

    let response = client
        .post("https://auth.roblox.com")
        .headers(headers)
        .send()
        .await
        .unwrap();

    let status = response.status(); // that feel when .text() consumes the response
    let body: Value = from_str(&response.text().await.unwrap()).unwrap();

    println!("{}", status);
    println!("{}", body["errors"][0]["message"]);
}
local HttpService = game:GetService("HttpService")

-- when inputting your cookie, remove the _|  |_ part
local COOKIE = ""
local headers = {
	Cookie = string.format(".ROBLOSECURITY=%s", COOKIE)
}

local request = HttpService:RequestAsync({
	Method = "POST",
	Url = "https://rprxy.deta.dev/auth", -- public proxy that should not be used in production
	Headers = headers
})

print(request.StatusCode)
print(HttpService:JSONDecode(request.Body).errors[1].message)
// Uses the Netwonsoft.Json package. Run "dotnet add package Newtonsoft.Json --version 13.0.1" on your terminal to install it.
using System.Net;
using Newtonsoft.Json;

const string roblosecurity = "cookie";

CookieContainer cookies = new CookieContainer();
cookies.Add(new Cookie(".ROBLOSECURITY", roblosecurity, "/", "roblox.com"));
HttpClient httpClient = new HttpClient(new HttpClientHandler()
{
    CookieContainer = cookies
});

HttpResponseMessage response = await httpClient.PostAsync("https://auth.roblox.com", null);
dynamic body = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()) ?? throw new Exception("Unable to deserialize body");

Console.WriteLine(response.StatusCode);
Console.WriteLine(body["errors"][0]["message"]);
# Run with "iex -S mix", "RobloxAPI.main()"
# Dependencies:
#  [
#    {:httpoison, "~> 1.8.0"},
#    {:poison, "~> 5.0.0"}
#  ]

defmodule RobloxAPI do
  @roblosecurity "cookie"

  def main() do
    headers = %{Cookie: ".ROBLOSECURITY=#{@roblosecurity};"}

    case HTTPoison.post("https://auth.roblox.com", "", headers) do
      {:ok, response} ->
        case Poison.decode(response.body) do
          {:ok, body} ->
            IO.puts(response.status_code)
            List.first(body["errors"])["message"] |> IO.puts()

          {:error, error} ->
            IO.inspect(error)
        end

      {:error, error} ->
        IO.inspect(error)
    end
  end

This code should output something like the following:

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

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)
# Uses the http.rb gem. Run "gem install http" on your terminal to install it
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}"
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}`);
// npm install node-fetch
import fetch from "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}`);
/*
    Cargo.toml dependencies:
    reqwest = { version = "0.11.4" }
    tokio = { version = "1.11.0", features = ["macros", "rt-multi-thread"]}
*/
use reqwest::header::{HeaderMap, HeaderValue};
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",
        HeaderValue::from_str(&format!(".ROBLOSECURITY={};", COOKIE)).unwrap(),
    );
    headers.insert("Content-Length", HeaderValue::from_str("0").unwrap());

    let first_response = client
        .post("https://auth.roblox.com")
        .headers(headers)
        .send()
        .await
        .unwrap();

    let mut headers = HeaderMap::new();
    headers.insert(
        "Cookie",
        HeaderValue::from_str(&format!(".ROBLOSECURITY={};", COOKIE)).unwrap(),
    );
    headers.insert("Content-Length", HeaderValue::from_str("0").unwrap());
    headers.insert(
        "x-csrf-token",
        HeaderValue::from_str(
            first_response
                .headers()
                .get("x-csrf-token")
                .unwrap()
                .to_str()
                .unwrap(),
        )
        .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());
}
--!strict
local HttpService = game:GetService("HttpService")

-- when inputting your cookie, remove the _|  |_ part
local COOKIE = ""
local headers = {
	Cookie = string.format(".ROBLOSECURITY=%s", COOKIE)
}

local response1 = HttpService:RequestAsync({
	Method = "POST",
	Url = "https://rprxy.deta.dev/auth", -- public proxy that should not be used in production
	Headers = headers
})

headers["x-csrf-token"] = response1.Headers["x-csrf-token"]

local response2 = HttpService:RequestAsync({
	Method = "POST",
	Url = "https://rprxy.deta.dev/auth", -- public proxy that should not be used in production
	Headers = headers
})

print("First: " .. response1.StatusCode)
print("Second: " .. response2.StatusCode)
using System.Net;

const string roblosecurity = "cookie";

CookieContainer cookies = new CookieContainer();
cookies.Add(new Cookie(".ROBLOSECURITY", roblosecurity, "/", "roblox.com"));
HttpClient httpClient = new HttpClient(new HttpClientHandler()
{
    CookieContainer = cookies
});

HttpResponseMessage firstResponse = await httpClient.PostAsync("https://auth.roblox.com", null);
httpClient.DefaultRequestHeaders.Add("x-csrf-token", firstResponse.Headers.GetValues("x-csrf-token").First());

HttpResponseMessage secondResponse = await httpClient.PostAsync("https://auth.roblox.com", null);

Console.WriteLine($"First: {firstResponse.StatusCode}");
Console.WriteLine($"Second: {secondResponse.StatusCode}");
# Run with "iex -S mix", "RobloxAPI.main()"
# Dependencies:
#  [
#    {:httpoison, "~> 1.8.0"},
#  ]

defmodule RobloxAPI do
  @roblosecurity "cookie"

  def main() do
    headers = %{Cookie: ".ROBLOSECURITY=#{@roblosecurity};"}

    case HTTPoison.post("https://auth.roblox.com", "", headers) do
      {:ok, first_response} ->
        headers =
          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)
          )

        case HTTPoison.post("https://auth.roblox.com", "", headers) do
          {:ok, second_response} ->
            IO.puts("First: #{first_response.status_code}")
            IO.puts("Second: #{second_response.status_code}")

          {:error, error} ->
            IO.inspect(error)
        end

      {: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[edit | edit source]

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:  # 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)
# Uses the http.rb gem. Run "gem install http" on your terminal to install it
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"

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

  def self.rbx_request(verb, url, *args)
    response = @client.request(verb, url, *args)
    return response if response.status == 200
        
    if response.status == 403
      if response.headers.include?("x-csrf-token")
        @client = @client.headers({
          :"x-csrf-token" => response.headers["x-csrf-token"]
        })
        return rbx_request(verb, url, args, kwargs)
      end
    end

    response
  end
end

response = HTTPClient.rbx_request(:post, "https://auth.roblox.com")

puts response.status
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: string,
    url: string,
    body?: string
): Promise<Response> => {
    const response = await fetch(url, {
        headers: {
            Cookie: `.ROBLOSECURITY=${COOKIE};`,
            "x-csrf-token": xCsrfToken,
            "Content-Length": body?.length.toString() || "0",
        },
        method: verb,
        body: body || "",
    });

    if (response.status == 403) {
        if (response.headers.has("x-csrf-token")) {
            xCsrfToken = response.headers.get("x-csrf-token")!;
            return rbxRequest(verb, url, body);
        }
    }

    return response;
};

const response = await rbxRequest("POST", "https://auth.roblox.com");

console.log(response.status);
// npm install node-fetch
import fetch from "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) => {
    const response = await fetch(url, {
        headers: {
            Cookie: `.ROBLOSECURITY=${COOKIE};`,
            "x-csrf-token": xCsrfToken,
            "Content-Length": body?.length.toString() || "0",
        },
        method: "POST",
        body: body || "",
    });

    if (response.status == 403) {
        if (response.headers.has("x-csrf-token")) {
            xCsrfToken = response.headers.get("x-csrf-token");
            return rbxRequest(verb, url, body);
        }
    }

    return response;
};

const response = await rbxRequest("POST", "https://auth.roblox.com");

console.log(response.status);
--!strict
type Table = {[any]: any}

local HttpService = game:GetService("HttpService")

-- when inputting your cookie, remove the _|  |_ part
local COOKIE = ""
local headers = {
	Cookie = string.format(".ROBLOSECURITY=%s", COOKIE)
}

local function rbxRequest(method: string, url: string, body: Table?)
	method = string.upper(method)
	
	local response = HttpService:RequestAsync({
		Method = method,
		Url = url,
		Body = body,
		Headers = headers
	})
	
	local statusCode = response.StatusCode
	if statusCode == 401 then
		error("A valid .ROBLOSECURITY cookie is required")
	elseif statusCode == 403 then
		local responseHeaders = response.Headers
		if responseHeaders["x-csrf-token"] then
			headers["x-csrf-token"] = responseHeaders["x-csrf-token"]
			return rbxRequest(method, url, body)
		end
	end
	
	return response
end

local response = rbxRequest("POST", "https://auth.roblox.com")
print(response.StatusCode)
// Uses the Netwonsoft.Json package. Run "dotnet add package Newtonsoft.Json --version 13.0.1" on your terminal to install it.
using System.Net;
using System.Text;
using Newtonsoft.Json;

const string roblosecurity = "cookie";

CookieContainer cookies = new CookieContainer();
cookies.Add(new Cookie(".ROBLOSECURITY", roblosecurity, "/", "roblox.com"));
HttpClient httpClient = new HttpClient(new HttpClientHandler()
{
    CookieContainer = cookies
});

async Task<HttpResponseMessage> Request(HttpMethod method, Uri url, dynamic? body = null)
{
    HttpResponseMessage response = await httpClient.SendAsync(
        new HttpRequestMessage(method, url)
        { Content = new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json") }
    );

    switch (response.StatusCode)
    {
        case HttpStatusCode.Unauthorized:
            throw new Exception("A valid .ROBLOSECURITY cookie is required");

        case HttpStatusCode.Forbidden:
            httpClient.DefaultRequestHeaders.Add("x-csrf-token", response.Headers.GetValues("x-csrf-token").First());
            return await Request(method, url, body);
    }

    return response;
}

HttpResponseMessage response = await Request(HttpMethod.Post, new Uri("https://auth.roblox.com"));

Console.WriteLine(response.StatusCode);
# Run with "iex -S mix", "RobloxAPI.main()"
# Dependencies:
#  [
#    {:httpoison, "~> 1.8.0"},
#    {:poison, "~> 5.0.0"}
#  ]

defmodule RobloxAPI do
  @roblosecurity "cookie"

  defp request(verb, url, headers, body \\ %{}) do
    case Poison.encode(body) do
      {:ok, encoded_body} ->
        case HTTPoison.request(verb, url, encoded_body, headers) do
          {:ok, response} ->
            case response.status_code do
              200 ->
                {:ok, response}

              401 ->
                {:error, "Authentication is required for this action"}

              403 ->
                xcsrf_token =
                  Enum.find_value(response.headers, fn {name, value} ->
                    if name == "x-csrf-token", do: value
                  end)

                headers = Map.merge(headers, %{"x-csrf-token": xcsrf_token})
                request(verb, url, headers, body)

              _ ->
                {:error, response}
            end

          {:error, error} ->
            {:error, error}
        end

      {:error, error} ->
        {:error, error}
    end
  end

  def main() do
    headers = %{Cookie: ".ROBLOSECURITY=#{@roblosecurity}"}

    case request(:post, "https://auth.roblox.com", headers) do
      {:ok, response} ->
        IO.puts(response.status_code)

      {:error, %HTTPoison.Response{status_code: status_code}} ->
        IO.puts("Unhandled status code: #{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, it makes it marginally easier to send requests to the API.
This article is a part of the Accessing the Roblox API series.
< Authentication | X-CSRF-Token