true, 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => 'Lax']); function authorisationReply($status, $data) { session_write_close(); http_response_code($status); header('Content-Type: application/json; charset=utf-8'); echo json_encode($data); exit; } // Keep result references briefly so a refresh or lost HTTP response can recover. // GoFile itself expires abandoned authorisation journeys after twenty minutes. foreach (($_SESSION['gofile_attempts'] ?? []) as $id => $attempt) { if (($attempt['retain_until'] ?? 0) <= time()) unset($_SESSION['gofile_attempts'][$id]); } $_SESSION['gofile_csrf'] = $_SESSION['gofile_csrf'] ?? bin2hex(random_bytes(32)); $stage = 'starting authorisation'; try { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') { if (!is_string($_POST['csrf'] ?? null) || !hash_equals($_SESSION['gofile_csrf'], $_POST['csrf'])) { authorisationReply(403, ['error' => 'This page has expired. Reload it and try again.']); } // The browser supplies only a local reference, never a GoFile session ID // or customer ID. Resolve the GoFile session from this authenticated session. $id = is_string($_POST['attempt'] ?? null) ? $_POST['attempt'] : ''; $attempt = $_SESSION['gofile_attempts'][$id] ?? null; if (!$attempt || $attempt['service'] !== $service) { authorisationReply(404, ['error' => 'This authorisation attempt is unavailable. Reload to start again.']); } $action = $_POST['action'] ?? ''; $reply = null; if ($action === 'start') { if ($attempt['state'] === 'completed') authorisationReply(200, ['state' => 'completed']); if (in_array($attempt['state'], ['failed', 'expired'], true)) authorisationReply(200, ['state' => $attempt['state']]); if (empty($attempt['session_id'])) { $stage = 'requesting authorisation from GoFile'; // Replace user-42 with the user ID from your authenticated backend session and // 999999999 with the customer's nine-digit VAT number: a new connection names the client it is for. // Stable idempotency key makes retrying this attempt safe. list($status, $reply) = gofile($path, array('software_user_id' => 'user-42', 'vat_number' => '999999999'), $key, $secret, $base, ['Idempotency-Key: web-' . $service . '-' . $id]); if (!in_array($status, [200, 201], true)) throw new RuntimeException('Could not start authorisation: HTTP ' . $status . ' ' . ($reply['data']['message'] ?? $reply['detail'] ?? 'API error')); $data = $reply['data'] ?? []; $state = $reply['state'] ?? ''; if (!in_array($state, ['pending', 'authorising', 'exchanging', 'completed', 'failed', 'expired'], true)) { throw new RuntimeException('GoFile returned an unexpected authorisation state.'); } if (!in_array($state, ['completed', 'failed', 'expired'], true)) { $session = $data['authorisation_session_id'] ?? ''; if (!is_string($session) || !preg_match('/^aus_[a-f0-9]{48}$/D', $session)) throw new RuntimeException('GoFile returned an invalid session reference.'); $url = $data['authorisation_url'] ?? ''; if ($state !== 'exchanging' && (!is_string($url) || parse_url($url, PHP_URL_SCHEME) !== 'https' || parse_url($url, PHP_URL_HOST) !== parse_url($base, PHP_URL_HOST) || parse_url($url, PHP_URL_PORT) !== parse_url($base, PHP_URL_PORT) || parse_url($url, PHP_URL_USER) !== null || parse_url($url, PHP_URL_PASS) !== null)) { throw new RuntimeException('Unexpected authorisation destination.'); } $attempt['session_id'] = $session; $attempt['authorisation_url'] = $url; $attempt['state'] = $state; $_SESSION['gofile_attempts'][$id] = $attempt; authorisationReply(200, ['state' => $state, 'authorisation_url' => $url]); } // A completed replay already contains the connection; save it below. } else authorisationReply(200, ['state' => $attempt['state'], 'authorisation_url' => $attempt['authorisation_url']]); } if (!in_array($action, ['start', 'status'], true)) authorisationReply(400, ['error' => 'Unknown action.']); if (in_array($attempt['state'], ['completed', 'failed', 'expired'], true)) { authorisationReply(200, ['state' => $attempt['state']]); } if ($reply === null) { if (empty($attempt['session_id'])) authorisationReply(409, ['error' => 'Start authorisation first.']); $stage = 'checking authorisation with GoFile'; // One bounded API request per check. Never sleep or loop inside PHP. list($status, $reply) = gofile($path, array('authorisation_session_id' => $attempt['session_id']), $key, $secret, $base); } if ($status === 404 || $status === 410) $state = 'expired'; elseif (!in_array($status, [200, 201], true)) throw new RuntimeException('Could not check authorisation: HTTP ' . $status . ' ' . ($reply['data']['message'] ?? $reply['detail'] ?? 'API error')); else $state = $reply['state'] ?? ''; if (!in_array($state, ['pending', 'authorising', 'exchanging', 'completed', 'failed', 'expired'], true)) { throw new RuntimeException('GoFile returned an unexpected authorisation state.'); } if ($state === 'completed') { $connection = $reply['data']['connection_id'] ?? ''; if (!is_string($connection) || !preg_match('/^hmc_[a-f0-9]{24}$/D', $connection)) { throw new RuntimeException('GoFile did not return a valid completed connection.'); } // Run the save step configured at the top of this file. // If saving throws, do not report success; the next check can retry. $stage = 'saving the connection in your software'; $gofileSaveConnection($connection); } $_SESSION['gofile_attempts'][$id]['state'] = $state; if (in_array($state, ['completed', 'failed', 'expired'], true)) { unset($_SESSION['gofile_attempts'][$id]['authorisation_url']); } authorisationReply(200, ['state' => $state]); } // Resume an unfinished journey after refresh without asking users for IDs. $id = ''; foreach (array_reverse($_SESSION['gofile_attempts'] ?? [], true) as $candidate => $attempt) { if ($attempt['service'] === $service && !in_array($attempt['state'], ['completed', 'failed', 'expired'], true)) { $id = $candidate; break; } } if ($id === '') { $id = bin2hex(random_bytes(16)); $_SESSION['gofile_attempts'][$id] = ['service' => $service, 'state' => 'new', 'retain_until' => time() + 86400]; } $resume = !empty($_SESSION['gofile_attempts'][$id]['session_id']); $config = ['csrf' => $_SESSION['gofile_csrf'], 'attempt' => $id, 'resume' => $resume]; session_write_close(); echo '' . '
Authorise through HMRC. This page will confirm when your connection is saved.
' . ' ' . ' ' . 'Open authorisation window' . 'Start a new authorisation' . ''; echo ''; } catch (Throwable $error) { $message = str_replace(array_filter([$key, $secret]), '[redacted]', $error->getMessage()); error_log('GoFile authorisation error: ' . json_encode([ 'service' => $service, 'stage' => $stage, 'message' => $message, ], JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE)); authorisationReply(500, ['error' => 'Unable to complete authorisation while ' . $stage . '. ' . $message]); }