oauth2: flux automat token ANAF (FR-1..FR-11) + arhivare fisiere vechi
- index.php: schimb code->token cu tratare completa erori, sesiune PHP pentru legare state (CSRF), stocare atomica tokenuri in fisier - pick.php: endpoint polling pentru ROACONT (preluare automata token) - oauth2_tokens mutat in oauth2/tokens/ + .htaccess Require all denied (locatia initiala "extra-docroot" era de fapt in interiorul public_html, deci web-accesibila fara aceasta protectie) - index1.php/index2.php/info.php (versiuni vechi) arhivate in _arhiva_oauth2/ cu .htaccess care blocheaza accesul HTTP Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2
_arhiva_oauth2/.htaccess
Normal file
2
_arhiva_oauth2/.htaccess
Normal file
@@ -0,0 +1,2 @@
|
||||
# Interzice accesul HTTP la fisierele arhivate (belt-and-braces, langa extra-docroot)
|
||||
Require all denied
|
||||
456
oauth2/index.php
456
oauth2/index.php
@@ -1,136 +1,384 @@
|
||||
<?php
|
||||
// oAuth2 JWT Token
|
||||
// index.php — OAuth2 ANAF eFactura: obtinere token automat + compatibilitate retro
|
||||
// FR-1 (propagare state), FR-2 (sesiune primara), FR-3 (stocare tokenuri),
|
||||
// FR-4 (retro-compat fara state), FR-10 (securitate), FR-11 (protectie stocare)
|
||||
//
|
||||
// Modificat: 2026-07-07 — implementare flux automat (stocare fisier + pagina succes)
|
||||
|
||||
$get = $_REQUEST;
|
||||
$query_string = '';
|
||||
foreach ($get as $key => $value) {
|
||||
$query_string .= $key . '=' . $value . '&';
|
||||
}
|
||||
// echo($query_string);
|
||||
parse_str($query_string);
|
||||
// === Credentiale ANAF (pastrate din versiunea anterioara) ===
|
||||
$client_id = 'f4c59081e488ac326f32f8ab52620023e996dd82d7d01163';
|
||||
$client_secret = 'e65b9127acac254c5b9bfbbe68b9a6ca07b0d6df6dbb0023e996dd82d7d01163';
|
||||
$redirect_uri = 'https://romfast.ro/oauth2/';
|
||||
|
||||
$client_id = 'f4c59081e488ac326f32f8ab52620023e996dd82d7d01163';
|
||||
$client_secret = 'e65b9127acac254c5b9bfbbe68b9a6ca07b0d6df6dbb0023e996dd82d7d01163';
|
||||
$redirect_uri = 'https://romfast.ro/oauth2/';
|
||||
// === Director tokenuri (FR-11) ===
|
||||
// oauth2/tokens/ — protejat explicit cu .htaccess (Require all denied), deoarece
|
||||
// se afla in interiorul docroot-ului (public_html/oauth2/tokens), nu deasupra lui.
|
||||
// Codul creaza directorul si .htaccess-ul daca lipsesc (ex. prima instalare)
|
||||
$tokens_dir = __DIR__ . '/tokens';
|
||||
if (!is_dir($tokens_dir)) {
|
||||
@mkdir($tokens_dir, 0700, true);
|
||||
}
|
||||
$tokens_htaccess = $tokens_dir . '/.htaccess';
|
||||
if (!file_exists($tokens_htaccess)) {
|
||||
@file_put_contents($tokens_htaccess, "Require all denied\n");
|
||||
}
|
||||
|
||||
// REFRESH TOKEN se apeleaza din ROA cu parametrul ?refresh_token=REFRESH_TOKEN pentru prelungirea valabilitatii tokenului
|
||||
if (!empty($refresh_token)){
|
||||
$url = 'https://logincert.anaf.ro/anaf-oauth2/v1/token';
|
||||
// ===================================================================
|
||||
// Functii helper
|
||||
// ===================================================================
|
||||
|
||||
//The data you want to send via POST
|
||||
$fields = [
|
||||
'client_id' => $client_id,
|
||||
'client_secret' => $client_secret,
|
||||
'refresh_token' => $refresh_token,
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'grant_type' => 'refresh_token'
|
||||
];
|
||||
// Validare format state: lungime fixa 64, doar [A-Za-z0-9] (protectie path traversal - NFR-1)
|
||||
function validState($state) {
|
||||
return is_string($state)
|
||||
&& strlen($state) === 64
|
||||
&& (bool)preg_match('/^[A-Za-z0-9]+$/', $state);
|
||||
}
|
||||
|
||||
//url-ify the data for the POST
|
||||
$fields_string = http_build_query($fields);
|
||||
// Afisare pagina HTML simpla (§3.3: UTF-8 cu diacritice, H1 mare scanabil,
|
||||
// font >=16px, contrast inalt, un singur job per pagina, fara linkuri/butoane)
|
||||
function showPage($page_title, $h1_text, $h1_color, $body_text) {
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
echo '<!DOCTYPE html>' . "\n";
|
||||
echo '<html lang="ro"><head>' . "\n";
|
||||
echo '<meta charset="utf-8">' . "\n";
|
||||
echo '<meta name="viewport" content="width=device-width,initial-scale=1">' . "\n";
|
||||
echo '<title>' . htmlspecialchars($page_title, ENT_QUOTES, 'UTF-8') . '</title>' . "\n";
|
||||
echo '<style>' . "\n";
|
||||
echo 'body{font-family:Arial,Helvetica,sans-serif;font-size:18px;margin:3em auto;' . "\n";
|
||||
echo ' max-width:640px;padding:0 1.5em;background:#f7f7f7;color:#222}' . "\n";
|
||||
echo 'h1{font-size:2em;color:' . $h1_color . ';margin-bottom:0.5em}' . "\n";
|
||||
echo 'p{font-size:1em;line-height:1.7;margin:0}' . "\n";
|
||||
echo '.brand{font-size:0.8em;color:#999;margin-bottom:2.5em}' . "\n";
|
||||
echo '</style>' . "\n";
|
||||
echo '</head><body>' . "\n";
|
||||
echo '<p class="brand">ROA Romfast</p>' . "\n";
|
||||
echo '<h1>' . htmlspecialchars($h1_text, ENT_QUOTES, 'UTF-8') . '</h1>' . "\n";
|
||||
echo '<p>' . htmlspecialchars($body_text, ENT_QUOTES, 'UTF-8') . '</p>' . "\n";
|
||||
echo '</body></html>' . "\n";
|
||||
}
|
||||
|
||||
//open connection
|
||||
$ch = curl_init();
|
||||
// ===================================================================
|
||||
// Citire explicita parametri GET (FR-10: elimina parse_str care injecta variabile)
|
||||
// ===================================================================
|
||||
$refresh_token = isset($_GET['refresh_token']) ? (string)$_GET['refresh_token'] : '';
|
||||
$code = isset($_GET['code']) ? (string)$_GET['code'] : '';
|
||||
$state_param = isset($_GET['state']) ? (string)$_GET['state'] : '';
|
||||
$error_param = isset($_GET['error']) ? (string)$_GET['error'] : '';
|
||||
$error_description = isset($_GET['error_description']) ? (string)$_GET['error_description'] : '';
|
||||
|
||||
//set the url, number of POST vars, POST data
|
||||
curl_setopt($ch,CURLOPT_URL, $url);
|
||||
curl_setopt($ch,CURLOPT_POST, true);
|
||||
curl_setopt($ch,CURLOPT_POSTFIELDS, $fields_string);
|
||||
// ===================================================================
|
||||
// Ramura REFRESH TOKEN — neatinsa functional (FR-4)
|
||||
// Apelata din ROA direct cu ?refresh_token=... pentru prelungirea tokenului
|
||||
// ===================================================================
|
||||
if (!empty($refresh_token)) {
|
||||
$url = 'https://logincert.anaf.ro/anaf-oauth2/v1/token';
|
||||
$fields = [
|
||||
'client_id' => $client_id,
|
||||
'client_secret' => $client_secret,
|
||||
'refresh_token' => $refresh_token,
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'grant_type' => 'refresh_token',
|
||||
];
|
||||
$fields_string = http_build_query($fields);
|
||||
|
||||
//So that curl_exec returns the contents of the cURL; rather than echoing it
|
||||
curl_setopt($ch,CURLOPT_RETURNTRANSFER, true);
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $fields_string);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
|
||||
//execute post
|
||||
$jsonobj = curl_exec($ch);
|
||||
$jsonobj = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
// $arr = json_decode($jsonobj, true);
|
||||
header('Content-Type: application/json');
|
||||
// La eroare curl: corp gol, identic cu comportamentul vechi (FR-4: ramura neatinsa)
|
||||
echo ($jsonobj !== false) ? $jsonobj : '';
|
||||
exit();
|
||||
}
|
||||
|
||||
/* $json = json_encode($jsonobj, JSON_UNESCAPED_SLASHES);
|
||||
if ($json === false) {
|
||||
// Avoid echo of empty string (which is invalid JSON), and
|
||||
// JSONify the error message instead:
|
||||
$json = json_encode(["error" => json_last_error_msg()]);
|
||||
if ($json === false) {
|
||||
// This should not happen, but we go all the way now:
|
||||
$json = '{"error":"unknown"}';
|
||||
}
|
||||
// Set HTTP response status code to: 500 - Internal Server Error
|
||||
http_response_code(500);
|
||||
}
|
||||
*/
|
||||
header("Content-Type: application/json");
|
||||
echo $jsonobj;
|
||||
// ===================================================================
|
||||
// Configurare cookie de sesiune PHP (FR-2)
|
||||
// session_set_cookie_params TREBUIE sa preceada session_start pe AMBELE intrari:
|
||||
// - intrarea initiala (fara ?code): browser deschis de ROACONT
|
||||
// - callback-ul ANAF (cu ?code): navigare top-level cross-site
|
||||
//
|
||||
// SameSite=Lax este obligatoriu: callback-ul ANAF este o navigare top-level dintr-un
|
||||
// alt domeniu (logincert.anaf.ro -> romfast.ro) — cu SameSite=Strict cookie-ul nu ar
|
||||
// fi trimis in callback si legarea sesiune ar esua silentios.
|
||||
// ===================================================================
|
||||
if (PHP_VERSION_ID >= 70300) {
|
||||
// Sintaxa cu array: disponibila incepand cu PHP 7.3
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0,
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
} else {
|
||||
// Fallback PHP < 7.3: SameSite injectat ca sufix al parametrului path
|
||||
session_set_cookie_params(0, '/; SameSite=Lax', '', true, true);
|
||||
}
|
||||
session_start();
|
||||
|
||||
// Close handle
|
||||
//curl_close($ch);
|
||||
// ===================================================================
|
||||
// Callback de eroare ANAF (?error=...) — FR-10
|
||||
// Tratare explicita; nu se re-redirecteaza la authorize (ar produce bucla)
|
||||
// ===================================================================
|
||||
if (!empty($error_param)) {
|
||||
// Extrage state-ul din sesiune pentru a scrie fisierul de eroare
|
||||
$sess_state = null;
|
||||
if (!empty($_SESSION['roa_state']) && is_array($_SESSION['roa_state'])) {
|
||||
$sess = $_SESSION['roa_state'];
|
||||
if (!empty($sess['value']) && (time() - (int)$sess['ts']) < 900) {
|
||||
$sess_state = $sess['value'];
|
||||
}
|
||||
}
|
||||
unset($_SESSION['roa_state']);
|
||||
session_write_close();
|
||||
|
||||
exit();
|
||||
}
|
||||
// Scrie fisier de eroare pentru state-ul din sesiune (daca exista si valid)
|
||||
if ($sess_state !== null && validState($sess_state)) {
|
||||
$fname = hash('sha256', $sess_state);
|
||||
$token_file = $tokens_dir . '/' . $fname . '.json';
|
||||
$tmp_file = $tokens_dir . '/' . $fname . '.tmp';
|
||||
// Anti-suprascriere: nu suprascrie un fisier de succes existent (FR-10)
|
||||
if (!file_exists($token_file)) {
|
||||
$err_data = json_encode([
|
||||
'error' => $error_param,
|
||||
'error_description' => $error_description,
|
||||
]);
|
||||
if ($err_data !== false && file_put_contents($tmp_file, $err_data) !== false) {
|
||||
@rename($tmp_file, $token_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// COD AUTORIZARE se apeleaza din ROA fara parametri pentru obtinerea codului de autorizare de la ANAF. ANAF apeleaza aceasta pagina (callback) cu codul de autorizare
|
||||
if (empty($code)) {
|
||||
// Obtinere cod autorizare
|
||||
$url = 'https://logincert.anaf.ro/anaf-oauth2/v1/authorize';
|
||||
$url .='?client_id='.$client_id;
|
||||
$url .='&client_secret='.$client_secret;
|
||||
$url .='&response_type=code';
|
||||
$url .='&redirect_uri='.$redirect_uri;
|
||||
$url .='&token_content_type=jwt';
|
||||
// Pagina de eroare (§3.3: un job, fara retry link, fara re-redirect)
|
||||
$cause = !empty($error_description) ? $error_description : $error_param;
|
||||
showPage(
|
||||
'Autorizare nereușită',
|
||||
'Autorizarea nu a reușit',
|
||||
'#c0392b',
|
||||
$cause . ' Reveniți în aplicația ROA — de acolo puteți relua operația sau folosi completarea manuală.'
|
||||
);
|
||||
exit();
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Intrare INITIALA — nicio sesiune, fara ?code: redirect catre ANAF authorize
|
||||
// ===================================================================
|
||||
if (empty($code)) {
|
||||
if (!empty($state_param)) {
|
||||
// Validare format state inainte de orice operatie (protectie path traversal)
|
||||
if (!validState($state_param)) {
|
||||
session_write_close();
|
||||
// L1: H1 conform §3.3 ("Autorizarea nu a reușit" pentru toate paginile de eroare)
|
||||
showPage(
|
||||
'Autorizare nereușită',
|
||||
'Autorizarea nu a reușit',
|
||||
'#c0392b',
|
||||
'Parametru de sesiune invalid. Reluați operația din aplicația ROA.'
|
||||
);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Redirectionez la pagina de autorizare ANAF pentru obtinerea codului de autorizare
|
||||
// echo $url;
|
||||
header('Location: '.$url);
|
||||
exit();
|
||||
}
|
||||
else {
|
||||
// NEW TOKEN
|
||||
// callback ANAF: https://romfast.ro/oauth2/?code=COD_AUTORIZARE
|
||||
// Salveaza state in sesiune cu timestamp (mecanism PRIMAR de legare)
|
||||
$_SESSION['roa_state'] = ['value' => $state_param, 'ts' => time()];
|
||||
session_write_close();
|
||||
|
||||
// Obtinere token pe baza codului de autorizare
|
||||
// print_r('Codul de autorizare este: '.$code);
|
||||
} else {
|
||||
// FR-2 igienă sesiune: fara state in URL = flux manual/retro-compat
|
||||
// Curata orice roa_state ramas dintr-o tentativa automata anterioara esuata
|
||||
// (altfel, la callback, am afisa pagina de succes fara tokenuri = fund de sac)
|
||||
unset($_SESSION['roa_state']);
|
||||
session_write_close();
|
||||
}
|
||||
|
||||
$url = 'https://logincert.anaf.ro/anaf-oauth2/v1/token';
|
||||
// URL authorize ANAF — client_secret NU se trimite la authorize (FR-10)
|
||||
// client_secret apartine doar schimbului code->token (POST, fara URL)
|
||||
$url_auth = 'https://logincert.anaf.ro/anaf-oauth2/v1/authorize';
|
||||
$url_auth .= '?client_id=' . urlencode($client_id);
|
||||
$url_auth .= '&response_type=code';
|
||||
$url_auth .= '&redirect_uri=' . urlencode($redirect_uri);
|
||||
$url_auth .= '&token_content_type=jwt';
|
||||
// FR-1: propagare state la ANAF daca exista
|
||||
// Nota: doc ANAF (pag.23) spune "State se lasa necompletat" — propagarea e incerta
|
||||
// dar trimitem oricum; legarea primara ramane sesiunea PHP
|
||||
if (!empty($state_param)) {
|
||||
$url_auth .= '&state=' . urlencode($state_param);
|
||||
}
|
||||
|
||||
//The data you want to send via POST
|
||||
$fields = [
|
||||
'client_id' => $client_id,
|
||||
'client_secret' => $client_secret,
|
||||
'code' => $code,
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'grant_type' => 'authorization_code',
|
||||
'token_content_type' => 'jwt'
|
||||
];
|
||||
header('Location: ' . $url_auth);
|
||||
exit();
|
||||
}
|
||||
|
||||
//url-ify the data for the POST
|
||||
$fields_string = http_build_query($fields);
|
||||
// ===================================================================
|
||||
// Callback cu cod de autorizare (?code=...) — schimb code -> token
|
||||
// ===================================================================
|
||||
|
||||
//open connection
|
||||
$ch = curl_init();
|
||||
// FR-2: identifica state-ul din sesiune (mecanismul PRIMAR de legare)
|
||||
// state-ul din sesiune = cel generat de ROACONT si salvat la intrarea initiala
|
||||
$sess_state = null;
|
||||
if (!empty($_SESSION['roa_state']) && is_array($_SESSION['roa_state'])) {
|
||||
$sess = $_SESSION['roa_state'];
|
||||
if (!empty($sess['value']) && (time() - (int)$sess['ts']) < 900) {
|
||||
// roa_state valid (sub 15 minute)
|
||||
$sess_state = $sess['value'];
|
||||
}
|
||||
// roa_state mai vechi de 15 minute se ignora (si se sterge mai jos)
|
||||
}
|
||||
// Consuma state-ul din sesiune (indiferent de validitate — evita re-utilizare)
|
||||
unset($_SESSION['roa_state']);
|
||||
|
||||
//set the url, number of POST vars, POST data
|
||||
curl_setopt($ch,CURLOPT_URL, $url);
|
||||
curl_setopt($ch,CURLOPT_POST, true);
|
||||
curl_setopt($ch,CURLOPT_POSTFIELDS, $fields_string);
|
||||
// FR-2: validare CSRF oportunistica
|
||||
// Daca ANAF a propagat state in callback (comportament incert), trebuie sa coincida cu sesiunea
|
||||
// Nepotrivire = posibil CSRF sau sesiune gresita -> eroare, fara stocare tokenuri
|
||||
if (!empty($state_param) && $sess_state !== null) {
|
||||
if ($state_param !== $sess_state) {
|
||||
session_write_close();
|
||||
showPage(
|
||||
'Eroare de securitate',
|
||||
'Autorizarea nu a reușit',
|
||||
'#c0392b',
|
||||
'Identificatorul de sesiune nu corespunde. Operația a fost anulată din motive de securitate. ' .
|
||||
'Reveniți în aplicația ROA și reluați de acolo.'
|
||||
);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
//So that curl_exec returns the contents of the cURL; rather than echoing it
|
||||
curl_setopt($ch,CURLOPT_RETURNTRANSFER, true);
|
||||
// FR-11 / C10: elibereaza lock-ul de sesiune inainte de curl
|
||||
// ANAF poate raspunde lent; nu tinem lock-ul de sesiune blocat in acest timp
|
||||
session_write_close();
|
||||
|
||||
//execute post
|
||||
$jsonobj = curl_exec($ch);
|
||||
//echo $jsonobj;
|
||||
// Schimb cod de autorizare -> token (FR-10: cu timeouts + tratare erori)
|
||||
$url_token = 'https://logincert.anaf.ro/anaf-oauth2/v1/token';
|
||||
$fields_token = [
|
||||
'client_id' => $client_id,
|
||||
'client_secret' => $client_secret,
|
||||
'code' => $code,
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'grant_type' => 'authorization_code',
|
||||
'token_content_type' => 'jwt',
|
||||
];
|
||||
$fields_string = http_build_query($fields_token);
|
||||
|
||||
$arr = json_decode($jsonobj, true);
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url_token);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $fields_string);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // FR-10: timeout conectare
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30); // FR-10: timeout total
|
||||
|
||||
echo '<p>Copiati ANAF Access Token si Refresh Token de pe liniile urmatoare si completati-le in aplicatie</p>';
|
||||
echo '<p><b>Access Token:</b></p><p>'.$arr["access_token"].'</p>';
|
||||
$jsonobj = curl_exec($ch);
|
||||
$curl_errno = curl_errno($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
echo '<p><b>Refresh Token:</b></p><p>'.$arr["refresh_token"].'</p>';
|
||||
// FR-10: tratare explicita erori curl / HTTP != 200 / JSON de eroare
|
||||
// (inainte: un esec afisa tokenuri goale)
|
||||
$error_msg = null; // text uman (pagina browser + error_description)
|
||||
$err_code = null; // cod scurt ASCII citit de ROACONT (loJson.error -> M12)
|
||||
$token_data = null;
|
||||
|
||||
// Close handle
|
||||
curl_close($ch);
|
||||
if ($curl_errno !== 0) {
|
||||
$err_code = 'curl_error';
|
||||
$error_msg = 'Eroare de rețea la contactarea ANAF (curl errno: ' . (int)$curl_errno . ').';
|
||||
} elseif ($http_code !== 200) {
|
||||
$err_code = 'http_error';
|
||||
$error_msg = 'ANAF a răspuns cu codul HTTP ' . (int)$http_code . '.';
|
||||
} else {
|
||||
$token_data = json_decode((string)$jsonobj, true);
|
||||
if ($token_data === null) {
|
||||
$err_code = 'json_invalid';
|
||||
$error_msg = 'Răspunsul ANAF nu a putut fi decodificat (JSON invalid).';
|
||||
} elseif (!empty($token_data['error'])) {
|
||||
$err_code = $token_data['error']; // cod brut ANAF: invalid_grant, access_denied etc.
|
||||
$err_desc = !empty($token_data['error_description']) ? $token_data['error_description'] : '';
|
||||
$error_msg = 'ANAF: ' . $err_code . ($err_desc ? ' — ' . $err_desc : '') . '.';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// ===================================================================
|
||||
// Scriere fisier tokenuri (FR-3, FR-11)
|
||||
// Se efectueaza DOAR daca sesiunea avea un state valid (flux automat)
|
||||
// ===================================================================
|
||||
if ($sess_state !== null && validState($sess_state)) {
|
||||
$fname = hash('sha256', $sess_state);
|
||||
$token_file = $tokens_dir . '/' . $fname . '.json';
|
||||
$tmp_file = $tokens_dir . '/' . $fname . '.tmp';
|
||||
|
||||
if ($error_msg !== null) {
|
||||
// Eroare: scrie fisier de eroare
|
||||
// FR-10 anti-suprascriere: nu suprascrie un token de succes existent
|
||||
// (ex. refresh pe pagina callback dupa succes: code expirat -> eroare -> nu stergem tokenul bun)
|
||||
if (!file_exists($token_file)) {
|
||||
$err_data = json_encode([
|
||||
'error' => $err_code, // cod scurt ASCII citit de ROACONT (fara mojibake)
|
||||
'error_description' => $error_msg, // text uman UTF-8 (diagnostic)
|
||||
]);
|
||||
if ($err_data !== false && file_put_contents($tmp_file, $err_data) !== false) {
|
||||
@rename($tmp_file, $token_file);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Succes: scriere atomica a tokenurilor
|
||||
$json_out = json_encode($token_data, JSON_UNESCAPED_SLASHES);
|
||||
$write_ok = false;
|
||||
if ($json_out !== false && file_put_contents($tmp_file, $json_out) !== false) {
|
||||
// @rename() pe acelasi filesystem = atomic (pick.php nu poate citi JSON partial)
|
||||
if (@rename($tmp_file, $token_file)) {
|
||||
$write_ok = true;
|
||||
} else {
|
||||
@unlink($tmp_file); // curata .tmp la esec rename; TTL il curata oricum
|
||||
}
|
||||
}
|
||||
if (!$write_ok) {
|
||||
// Scriere esuata — afisam eroare (nu succes fals); ROACONT va detecta timeout
|
||||
$error_msg = 'Tokenul ANAF a fost primit dar nu a putut fi salvat temporar pe server. Reveniți în aplicație și reluați operația.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
// ===================================================================
|
||||
// Afisare pagina rezultat (§3.3)
|
||||
// ===================================================================
|
||||
if ($sess_state !== null) {
|
||||
// Flux automat (state in sesiune): afiseaza succes sau eroare
|
||||
// Tokenurile NU apar in pagina — sunt ridicate de pick.php
|
||||
if ($error_msg !== null) {
|
||||
showPage(
|
||||
'Autorizare nereușită',
|
||||
'Autorizarea nu a reușit',
|
||||
'#c0392b',
|
||||
$error_msg .
|
||||
' Reveniți în aplicația ROA — de acolo puteți relua operația sau folosi completarea manuală.'
|
||||
);
|
||||
} else {
|
||||
// §3.3 pagina succes: H1 verde, corp cu instructiunea de revenire in ROA
|
||||
showPage(
|
||||
'Autorizare reușită',
|
||||
'Autorizare reușită',
|
||||
'#27ae60',
|
||||
'Reveniți în aplicația ROA — tokenul se preia automat. Puteți închide această pagină.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// FR-4 retro-compat: fara state in sesiune = versiune veche ROACONT sau sesiune expirata
|
||||
// Comportamentul ACTUAL ramas neschimbat: tokenurile apar in pagina pentru copiere manuala
|
||||
if ($error_msg !== null) {
|
||||
echo '<p>Eroare la obtinerea tokenului: ' . htmlspecialchars($error_msg, ENT_QUOTES, 'UTF-8') . '</p>';
|
||||
} else {
|
||||
echo '<p>Copiati ANAF Access Token si Refresh Token de pe liniile urmatoare si completati-le in aplicatie</p>';
|
||||
echo '<p><b>Access Token:</b></p><p>';
|
||||
echo htmlspecialchars((string)(isset($token_data['access_token']) ? $token_data['access_token'] : ''), ENT_QUOTES, 'UTF-8');
|
||||
echo '</p>';
|
||||
echo '<p><b>Refresh Token:</b></p><p>';
|
||||
echo htmlspecialchars((string)(isset($token_data['refresh_token']) ? $token_data['refresh_token'] : ''), ENT_QUOTES, 'UTF-8');
|
||||
echo '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
62
oauth2/pick.php
Normal file
62
oauth2/pick.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
// pick.php — Endpoint polling preluare automata token OAuth2 ANAF eFactura
|
||||
// Apelat de ROACONT cu POST state=<64 hex>; raspunde JSON:
|
||||
// {"status":"pending"} cat timp tokenul nu a sosit (intotdeauna HTTP 200, nu 404),
|
||||
// continutul fisierului de token (succes sau eroare ANAF) o SINGURA data, apoi il sterge.
|
||||
// Fara session_start() — evita serializarea pe lock-ul de sesiune PHP.
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// Director tokenuri (acelasi ca in index.php) — protejat cu .htaccess
|
||||
$tokens_dir = __DIR__ . '/tokens';
|
||||
if (!is_dir($tokens_dir)) {
|
||||
@mkdir($tokens_dir, 0700, true);
|
||||
}
|
||||
$tokens_htaccess = $tokens_dir . '/.htaccess';
|
||||
if (!file_exists($tokens_htaccess)) {
|
||||
@file_put_contents($tokens_htaccess, "Require all denied\n");
|
||||
}
|
||||
|
||||
// Validare stricta format state: 64 caractere [A-Za-z0-9] (protectie path traversal)
|
||||
$state = isset($_POST['state']) ? (string)$_POST['state'] : '';
|
||||
if (strlen($state) !== 64 || !preg_match('/^[A-Za-z0-9]+$/', $state)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'invalid_state']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// TTL: sterge fisierele de tokenuri (si .tmp orfane) mai vechi de 10 minute
|
||||
// (aliniat cu timeout-ul de polling din ROACONT)
|
||||
$now = time();
|
||||
foreach (array_merge((array)glob($tokens_dir . '/*.json'), (array)glob($tokens_dir . '/*.tmp')) as $f) {
|
||||
if (($now - filemtime($f)) > 600) {
|
||||
@unlink($f);
|
||||
}
|
||||
}
|
||||
|
||||
// Numele fisierului = SHA-256(state): secretul nu apare in listinguri de directoare
|
||||
$token_file = $tokens_dir . '/' . hash('sha256', $state) . '.json';
|
||||
|
||||
if (!file_exists($token_file)) {
|
||||
echo json_encode(['status' => 'pending']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Claim atomic prin rename: din doua cereri concurente una singura primeste tokenul,
|
||||
// cealalta "pending"; previne si citirea unui JSON partial (index.php scrie .tmp + rename)
|
||||
$claim_file = $tokens_dir . '/' . hash('sha256', $state) . '_claim_' . getmypid() . '.json';
|
||||
if (!@rename($token_file, $claim_file)) {
|
||||
echo json_encode(['status' => 'pending']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Citire INAINTE de stergere; la esec nu stergem (TTL curata fisierul claim ramas)
|
||||
$content = file_get_contents($claim_file);
|
||||
if ($content === false || $content === '') {
|
||||
echo json_encode(['error' => 'read_error']);
|
||||
exit();
|
||||
}
|
||||
@unlink($claim_file);
|
||||
|
||||
// Continutul JSON exact cum a fost salvat de index.php (tokenuri sau eroare ANAF)
|
||||
echo $content;
|
||||
1
oauth2/tokens/.htaccess
Normal file
1
oauth2/tokens/.htaccess
Normal file
@@ -0,0 +1 @@
|
||||
Require all denied
|
||||
Reference in New Issue
Block a user