FourPlay

Authentication

HMAC-SHA256, JWT, proteção por timestamp e validação de callbacks FourPlay

Visão Geral

Todas as comunicações entre o operador e a FourPlay — em ambos os sentidos — são autenticadas via HMAC-SHA256. O mesmo mecanismo protege tanto as chamadas que você faz ao FourPlay quanto os callbacks que o FourPlay faz ao seu servidor.

DireçãoMecanismo
Operador → FourPlayHMAC-SHA256 nos headers
FourPlay → Operador (callbacks)HMAC-SHA256 nos headers

Headers de Autenticação

Toda requisição ao FourPlay deve incluir estes três headers:

HeaderValor
X-Operator-IdSua API Key (op_live_...)
X-TimestampTimestamp atual em milissegundos (Date.now() / ms Unix)
X-SignatureAssinatura HMAC-SHA256 calculada com a Secret Key

Milissegundos, não segundos

O X-Timestamp deve ser em milissegundos (13 dígitos), não em segundos Unix (10 dígitos). Use Date.now() em Node.js, int(time.time() * 1000) em Python, microtime(true) * 1000 em PHP.

Fórmula HMAC

A assinatura é calculada assim:

data      = X-Operator-Id + X-Timestamp + JSON.stringify(requestBody)
signature = HMAC-SHA256(secretKey, data)
X-Signature: hex(signature)

Passo a passo:

  1. Concatene sua API Key + o timestamp em ms + o body JSON exato que será enviado
  2. Calcule HMAC-SHA256(secretKey, data) e encode em hexadecimal
  3. Envie o resultado no header X-Signature

O JSON.stringify(body) deve produzir exatamente o mesmo JSON enviado no body. Não adicione espaços extras nem altere a ordem das chaves entre o cálculo e o envio.

Gerando a Assinatura

const crypto = require('crypto');

const API_KEY    = process.env.FOURPLAY_API_KEY;    // op_live_...
const SECRET_KEY = process.env.FOURPLAY_SECRET_KEY; // sk_live_...

function buildAuthHeaders(body) {
  const timestamp = Date.now().toString(); // milissegundos
  const data      = API_KEY + timestamp + body;
  const signature = crypto.createHmac('sha256', SECRET_KEY).update(data).digest('hex');
  return {
    'X-Operator-Id': API_KEY,
    'X-Timestamp':   timestamp,
    'X-Signature':   signature,
    'Content-Type':  'application/json',
  };
}

// Uso:
const body    = JSON.stringify({ externalPlayerId: 'player_123', username: 'João' });
const headers = buildAuthHeaders(body);

const res = await fetch('https://mines.fourplay.studio/api/operator/session/create', {
  method: 'POST',
  headers,
  body,
});
<?php
$apiKey    = getenv('FOURPLAY_API_KEY');
$secretKey = getenv('FOURPLAY_SECRET_KEY');

function buildAuthHeaders(string $body, string $apiKey, string $secretKey): array {
    $timestamp = (string)(int)(microtime(true) * 1000); // ms
    $data      = $apiKey . $timestamp . $body;
    $signature = hash_hmac('sha256', $data, $secretKey);
    return [
        'X-Operator-Id: ' . $apiKey,
        'X-Timestamp: '   . $timestamp,
        'X-Signature: '   . $signature,
        'Content-Type: application/json',
    ];
}

$body    = json_encode(['externalPlayerId' => 'player_123', 'username' => 'João']);
$headers = buildAuthHeaders($body, $apiKey, $secretKey);

$ch = curl_init('https://mines.fourplay.studio/api/operator/session/create');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $body,
    CURLOPT_HTTPHEADER     => $headers,
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
import hmac, hashlib, time, json, os

API_KEY    = os.environ['FOURPLAY_API_KEY']
SECRET_KEY = os.environ['FOURPLAY_SECRET_KEY']

def build_auth_headers(body: str) -> dict:
    timestamp = str(int(time.time() * 1000))  # ms
    data      = API_KEY + timestamp + body
    signature = hmac.new(SECRET_KEY.encode(), data.encode(), hashlib.sha256).hexdigest()
    return {
        'X-Operator-Id': API_KEY,
        'X-Timestamp':   timestamp,
        'X-Signature':   signature,
        'Content-Type':  'application/json',
    }

import urllib.request

body    = json.dumps({'externalPlayerId': 'player_123', 'username': 'João'}, separators=(',', ':'))
headers = build_auth_headers(body)

req = urllib.request.Request(
    'https://mines.fourplay.studio/api/operator/session/create',
    data=body.encode(),
    headers=headers,
    method='POST',
)
with urllib.request.urlopen(req) as resp:
    data = json.loads(resp.read())
    print(data['token'])
using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

var apiKey    = Environment.GetEnvironmentVariable("FOURPLAY_API_KEY")!;
var secretKey = Environment.GetEnvironmentVariable("FOURPLAY_SECRET_KEY")!;

Dictionary<string, string> BuildAuthHeaders(string body)
{
    var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
    var data      = apiKey + timestamp + body;
    using var mac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey));
    var signature = Convert.ToHexString(mac.ComputeHash(Encoding.UTF8.GetBytes(data))).ToLower();
    return new Dictionary<string, string>
    {
        ["X-Operator-Id"] = apiKey,
        ["X-Timestamp"]   = timestamp,
        ["X-Signature"]   = signature,
    };
}

var body    = JsonSerializer.Serialize(new { externalPlayerId = "player_123", username = "João" });
var headers = BuildAuthHeaders(body);

using var http    = new HttpClient();
var request       = new HttpRequestMessage(HttpMethod.Post,
    "https://mines.fourplay.studio/api/operator/session/create")
{
    Content = new StringContent(body, Encoding.UTF8, "application/json")
};
foreach (var (k, v) in headers) request.Headers.Add(k, v);

var resp = await http.SendAsync(request);
var json = await resp.Content.ReadAsStringAsync();
Console.WriteLine(json); // {"token":"eyJ..."}
#!/bin/bash
API_KEY="op_live_..."
SECRET_KEY="sk_live_..."

BODY='{"externalPlayerId":"player_123","username":"João"}'
TIMESTAMP=$(node -e "console.log(Date.now())")
# Alternativa bash pura (apenas em Linux com date GNU):
# TIMESTAMP=$(date +%s%3N)

DATA="${API_KEY}${TIMESTAMP}${BODY}"
SIGNATURE=$(echo -n "$DATA" | openssl dgst -sha256 -hmac "$SECRET_KEY" | cut -d' ' -f2)

curl -s -X POST "https://mines.fourplay.studio/api/operator/session/create" \
  -H "Content-Type: application/json" \
  -H "X-Operator-Id: $API_KEY" \
  -H "X-Timestamp: $TIMESTAMP" \
  -H "X-Signature: $SIGNATURE" \
  -d "$BODY"

Validando Callbacks no Seu Servidor

Quando a FourPlay chama seus endpoints de wallet, ela inclui os mesmos headers X-Operator-Id, X-Timestamp e X-Signature. Você deve validar a assinatura em todas as requisições recebidas.

Obrigatório

Nunca processe um callback sem validar a assinatura. Rejeite requisições não autenticadas com HTTP 401 imediatamente.

Algoritmo de validação:

  1. Extraia X-Operator-Id, X-Timestamp e X-Signature dos headers
  2. Verifique que |Date.now() - parseInt(X-Timestamp)| <= 300000 (5 minutos em ms)
  3. Reconstrua: data = X-Operator-Id + X-Timestamp + JSON.stringify(requestBody)
  4. Calcule HMAC-SHA256(secretKey, data) e compare com X-Signature
const crypto    = require('crypto');
const SECRET_KEY = process.env.FOURPLAY_SECRET_KEY;

function validateCallback(req) {
  const apiKey    = req.headers['x-operator-id'];
  const timestamp = req.headers['x-timestamp'];
  const received  = req.headers['x-signature'];

  if (!apiKey || !timestamp || !received) return false;

  // Proteção por timestamp: rejeitar se fora de ±5 minutos
  if (Math.abs(Date.now() - parseInt(timestamp, 10)) > 300_000) return false;

  const body     = JSON.stringify(req.body); // body já parseado
  const data     = apiKey + timestamp + body;
  const expected = crypto.createHmac('sha256', SECRET_KEY).update(data).digest('hex');

  return crypto.timingSafeEqual(Buffer.from(received, 'hex'), Buffer.from(expected, 'hex'));
}

// Express middleware:
app.use('/wallet', (req, res, next) => {
  if (!validateCallback(req)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  next();
});
<?php
function validateCallback(array $headers, string $rawBody): bool {
    $secretKey = getenv('FOURPLAY_SECRET_KEY');
    $apiKey    = $headers['X-Operator-Id'] ?? $headers['x-operator-id'] ?? '';
    $timestamp = $headers['X-Timestamp']   ?? $headers['x-timestamp']   ?? '';
    $received  = $headers['X-Signature']   ?? $headers['x-signature']   ?? '';

    if (!$apiKey || !$timestamp || !$received) return false;

    // Proteção por timestamp (5 minutos em ms)
    if (abs((int)(microtime(true) * 1000) - (int)$timestamp) > 300000) return false;

    $data     = $apiKey . $timestamp . $rawBody;
    $expected = hash_hmac('sha256', $data, $secretKey);
    return hash_equals($expected, $received);
}

// Uso em endpoint de callback:
$rawBody = file_get_contents('php://input');
$headers = getallheaders();

if (!validateCallback($headers, $rawBody)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

$payload = json_decode($rawBody, true);
// processar $payload...
import hmac, hashlib, time, json, os
SECRET_KEY = os.environ['FOURPLAY_SECRET_KEY']

def validate_callback(headers: dict, raw_body: bytes) -> bool:
    api_key   = headers.get('X-Operator-Id', headers.get('x-operator-id', ''))
    timestamp = headers.get('X-Timestamp',   headers.get('x-timestamp', ''))
    received  = headers.get('X-Signature',   headers.get('x-signature', ''))

    if not all([api_key, timestamp, received]):
        return False

    # Proteção por timestamp (5 minutos em ms)
    if abs(int(time.time() * 1000) - int(timestamp)) > 300_000:
        return False

    data     = (api_key + timestamp).encode() + raw_body
    expected = hmac.new(SECRET_KEY.encode(), data, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, received)

# Flask example:
from flask import Flask, request, jsonify
app = Flask(__name__)

@app.route('/wallet/debit', methods=['POST'])
def wallet_debit():
    raw_body = request.get_data()
    if not validate_callback(dict(request.headers), raw_body):
        return jsonify({'error': 'Invalid signature'}), 401
    payload = request.get_json()
    # processar...
    return jsonify({'balance': 49000, 'transactionId': 'tx_001'})
using System;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Http;

public static class CallbackValidator
{
    private static readonly string SecretKey = Environment.GetEnvironmentVariable("FOURPLAY_SECRET_KEY")!;

    public static bool Validate(HttpRequest req, string rawBody)
    {
        var apiKey    = req.Headers["X-Operator-Id"].ToString();
        var timestamp = req.Headers["X-Timestamp"].ToString();
        var received  = req.Headers["X-Signature"].ToString();

        if (string.IsNullOrEmpty(apiKey) || string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(received))
            return false;

        // Proteção por timestamp (5 minutos em ms)
        if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - long.Parse(timestamp)) > 300_000)
            return false;

        var data    = apiKey + timestamp + rawBody;
        using var mac = new HMACSHA256(Encoding.UTF8.GetBytes(SecretKey));
        var expected  = Convert.ToHexString(mac.ComputeHash(Encoding.UTF8.GetBytes(data))).ToLower();

        return CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(expected),
            Encoding.UTF8.GetBytes(received));
    }
}

// Minimal API endpoint:
app.MapPost("/wallet/debit", async (HttpContext ctx) =>
{
    var rawBody = await new StreamReader(ctx.Request.Body).ReadToEndAsync();
    if (!CallbackValidator.Validate(ctx.Request, rawBody))
        return Results.Unauthorized();
    // processar...
    return Results.Ok(new { balance = 49000, transactionId = "tx_001" });
});

JWT: O Token de Sessão

O token retornado ao criar uma sessão é um JWT (JSON Web Token) assinado com HS256. Você pode decodificar o payload para ler os claims.

Claims do token:

ClaimTipoDescrição
playerIdstringUUID interno do usuário no banco do Mines (não é o externalPlayerId)
operatorIdstringUUID interno do operador no banco do Mines
usernamestring?Nome do jogador (se enviado na criação)
isDemobool?true apenas em sessões de demonstração
iatnumberUnix timestamp de emissão
expnumberUnix timestamp de expiração (24 horas após emissão)

Expiração: 24 horas após a criação.

// Decodificar sem verificar assinatura (apenas para leitura):
const [, payloadB64] = token.split('.');
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
console.log(payload.playerId);   // UUID interno
console.log(payload.operatorId); // UUID interno
console.log(new Date(payload.exp * 1000)); // expiração

Proteção contra Replay

A FourPlay implementa proteção por timestamp: qualquer requisição cujo X-Timestamp esteja a mais de 5 minutos (300.000 ms) do horário atual do servidor é rejeitada com { "message": "Request expired" }.

|------ aceito ------| rejeitado |
   T-5min    T    T+5min

Mantenha o NTP ativo no seu servidor para evitar drift de relógio.

Rotação de Chaves

Para rotacionar suas chaves via Portal (https://admin.fourplay.studio):

  1. Acesse Portal → Credenciais → Gerar nova API Key (ou nova Secret Key)
  2. Atualize as variáveis de ambiente no seu servidor com a nova chave
  3. Faça deploy
  4. A chave antiga para de funcionar imediatamente após a rotação

Se suspeitar de vazamento da Secret Key, rotacione imediatamente. A Secret Key nunca deve ser exposta em logs, código-fonte ou repositórios.

On this page