Example file
get-token.php
PHP: gets a token and keeps it in a small private file until shortly before it runs out.
<?php
// Gets an access token from Subcontext and keeps it until shortly before it
// runs out. Copy get_subcontext_token() into your own server code.
//
// An access token lets your code use the Subcontext API for two hours.
// You get one by sending your API application's client id
// and client secret to Subcontext (an OAuth "client credentials" request).
// Run this on your server, never in a web page, because it holds the secret.
//
// SUBCONTEXT_CLIENT_ID=... SUBCONTEXT_CLIENT_SECRET=... php get-token.php
//
// SUBCONTEXT_CLIENT_ID and SUBCONTEXT_CLIENT_SECRET: from the Subcontext
// dashboard, Integrations, API applications. The client secret is shown only
// once, when you create the application (or rotate its secret).
// SUBCONTEXT_SCOPE (optional): what the token may do, separated by spaces.
// Default "read". It can only include scopes ticked on the API application.
// SUBCONTEXT_BASE_URL (optional): the Subcontext address. Default
// https://subcontext.com.
//
// PHP usually starts afresh for every web request, so the token is kept in a
// small file in the server's temporary folder, readable only by the user PHP
// runs as. Use your framework's cache instead if it has one.
//
// Needs PHP 8.1 or later and no extensions beyond the standard ones.
const RENEW_EARLY_SECONDS = 300; // ask for a new token this long before the old one runs out
function subcontext_base_url(): string
{
return rtrim(getenv('SUBCONTEXT_BASE_URL') ?: 'https://subcontext.com', '/');
}
// Returns a token that is good for at least a few more minutes.
function get_subcontext_token(string $scope = 'read'): string
{
$clientId = getenv('SUBCONTEXT_CLIENT_ID');
$clientSecret = getenv('SUBCONTEXT_CLIENT_SECRET');
if (!$clientId || !$clientSecret) {
throw new RuntimeException('Set SUBCONTEXT_CLIENT_ID and SUBCONTEXT_CLIENT_SECRET (see the comment at the top of this file).');
}
// One cache file per application and scope.
$cacheFile = sys_get_temp_dir() . '/subcontext-token-' . hash('sha256', $clientId . ' ' . $scope) . '.json';
$cached = is_readable($cacheFile) ? json_decode((string) file_get_contents($cacheFile), true) : null;
if (is_array($cached) && time() < $cached['expires_at'] - RENEW_EARLY_SECONDS) {
return $cached['access_token'];
}
$baseUrl = subcontext_base_url();
[$status, $body] = subcontext_http('POST', $baseUrl . '/oauth/token', ['Content-Type: application/x-www-form-urlencoded'], http_build_query([
'grant_type' => 'client_credentials',
'client_id' => $clientId,
'client_secret' => $clientSecret,
'scope' => $scope,
// What the token is for (the Subcontext API). Required.
'resource' => $baseUrl . '/api/v1',
]));
if ($status !== 200) {
throw new RuntimeException('Token request failed (' . $status . '): ' . ($body['error_description'] ?? $body['error'] ?? 'no details'));
}
$token = ['access_token' => $body['access_token'], 'expires_at' => time() + (int) $body['expires_in']];
// Write the file so only this user can read it, then move it into place.
$temporary = tempnam(sys_get_temp_dir(), 'sxt');
chmod($temporary, 0600);
file_put_contents($temporary, json_encode($token));
rename($temporary, $cacheFile);
return $token['access_token'];
}
// Forget the cached token, for example after the API answers 401.
function forget_subcontext_token(string $scope = 'read'): void
{
@unlink(sys_get_temp_dir() . '/subcontext-token-' . hash('sha256', getenv('SUBCONTEXT_CLIENT_ID') . ' ' . $scope) . '.json');
}
// Sends a request and returns [HTTP status, decoded JSON body, response headers].
function subcontext_http(string $method, string $url, array $headers, ?string $content = null): array
{
$context = stream_context_create(['http' => [
'method' => $method,
'header' => implode("\r\n", array_merge($headers, ['Accept: application/json'])) . "\r\n",
'content' => $content ?? '',
'timeout' => 20,
'ignore_errors' => true, // read the body of error answers too
]]);
$raw = @file_get_contents($url, false, $context);
if ($raw === false) {
throw new RuntimeException('Could not reach ' . $url);
}
$responseHeaders = function_exists('http_get_last_response_headers') ? http_get_last_response_headers() : $http_response_header;
$status = preg_match('#^HTTP/\S+ (\d{3})#', $responseHeaders[0] ?? '', $match) ? (int) $match[1] : 0;
return [$status, json_decode($raw, true) ?: [], $responseHeaders];
}
// Run directly, it gets a token twice and shows the second one came from the cache.
if (PHP_SAPI === 'cli' && realpath($_SERVER['SCRIPT_FILENAME']) === __FILE__) {
$scope = getenv('SUBCONTEXT_SCOPE') ?: 'read';
try {
forget_subcontext_token($scope);
$first = get_subcontext_token($scope);
echo 'Got a token: ', substr($first, 0, 8), "...\n";
$second = get_subcontext_token($scope);
echo $second === $first ? "Asked again: the same token came from the cache, no new request.\n" : "Asked again: got a new token.\n";
} catch (RuntimeException $error) {
fwrite(STDERR, $error->getMessage() . "\n");
exit(1);
}
}