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ção | Mecanismo |
|---|---|
| Operador → FourPlay | HMAC-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:
| Header | Valor |
|---|---|
X-Operator-Id | Sua API Key (op_live_...) |
X-Timestamp | Timestamp atual em milissegundos (Date.now() / ms Unix) |
X-Signature | Assinatura 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:
- Concatene sua API Key + o timestamp em ms + o body JSON exato que será enviado
- Calcule
HMAC-SHA256(secretKey, data)e encode em hexadecimal - 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:
- Extraia
X-Operator-Id,X-TimestampeX-Signaturedos headers - Verifique que
|Date.now() - parseInt(X-Timestamp)| <= 300000(5 minutos em ms) - Reconstrua:
data = X-Operator-Id + X-Timestamp + JSON.stringify(requestBody) - Calcule
HMAC-SHA256(secretKey, data)e compare comX-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:
| Claim | Tipo | Descrição |
|---|---|---|
playerId | string | UUID interno do usuário no banco do Mines (não é o externalPlayerId) |
operatorId | string | UUID interno do operador no banco do Mines |
username | string? | Nome do jogador (se enviado na criação) |
isDemo | bool? | true apenas em sessões de demonstração |
iat | number | Unix timestamp de emissão |
exp | number | Unix 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çãoProteçã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+5minMantenha 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):
- Acesse Portal → Credenciais → Gerar nova API Key (ou nova Secret Key)
- Atualize as variáveis de ambiente no seu servidor com a nova chave
- Faça deploy
- 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.