Introduction
In Week 1, we announced the reboot of the PHP framework and laid out the 12-week roadmap. In Week 2, we explored how GraphQL works in Rubrik Security Cloud (RSC) - from queries and mutations to the Connection -> Node pattern that powers the API.
Now it's time to make things real.
Before we can send a single GraphQL query from PHP, our code needs to authenticate to RSC and obtain a Bearer token using client credentials (OAuth2). Authentication isn't glamorous, but it is foundational, and doing it right from the start means every function we build on top of it will be portable, secure, and production-ready.
This week covers three essential pieces: how RSC authentication works, how to store your secrets safely, and how to encapsulate all of it into a reusable PHP helper - rkRscGetToken().
How Authentication Works in RSC
Rubrik Security Cloud uses the OAuth2 Client Credentials flow when authenticating programmatically via a Service Account.
To create one, navigate to:
Settings -> Users and Access -> Service Accounts
When you create a Service Account, RSC provides three pieces of information:
- Client ID (format: client|xxxxxxxx-xxxx-...)
- Client Secret (a random string - treat it like a password)
- Access Token URI (https://<your-tenant>/api/client_token)
Important: The client secret is shown only once. If you lose it, you must generate a new one.
Practitioner Tip "Always ensure you leverage least-privilege RBAC when scoping your service accounts. Give them access only to what they need within the script." |
Your script must send the following to the token endpoint:
- client_id
- client_secret
- grant_type=client_credentials
To: https://<your-tenant>/api/client_token
The endpoint returns an access_token (used in Authorization: Bearer <token>) and expires_in (lifetime in seconds). Best practice: cache the token until it expires - there's no reason to re-authenticate on every API call.
Practitioner Tip "If authentication feels abstract, don't overthink it - it's just a simple HTTP POST. Test it with curl first, understand the response, then automate it in code." |
The Raw cURL Call
Before writing a single line of PHP, it helps to see the authentication request in its simplest form. Here is the equivalent curl request:
export RSC_FQDN="example.my.rubrik.com"
export RSC_CLIENT_ID="client|c9bba9a9-1234-1234-b7c6-123440b4cf64"
export RSC_CLIENT_SECRET="ExampleServiceAccountSecret"
curl --silent --location "https://$RSC_FQDN/api/client_token" \
--header "Content-Type: application/x-www-form-urlencoded" \
--data "client_id=$RSC_CLIENT_ID&client_secret=$RSC_CLIENT_SECRET&grant_type=client_credentials"
A successful response looks like this:
{
"client_id": "client|c9bba9a9-1234-1234-b7c6-123560b4cf64",
"access_token": "eyJ...",
"expires_in": 43200
}Our PHP function will perform exactly this call under the hood.
Storing the Client ID and Secret Safely
Your client_id and client_secret are the keys to your RSC environment. They carry the same weight as a password and should be treated accordingly.
Three rules to build around:
- Secrets in source code are a liability that outlives your tenure. Version control is forever - a credential committed today can be extracted from git history long after it has been deleted.
- Runtime-injected secrets are invisible to the artifact; hardcoded ones travel with it. Into every registry push, every backup, every build log.
- Credentials that live in one place outside of code can be rotated in seconds, not hours. That gap matters when you are mid-incident.
The cleanest way to do this is environment variables - available at runtime, invisible to source control, and straightforward to rotate without touching application logic.
Option A - Interactive Usage (Good for Testing)
export RSC_FQDN="example.my.rubrik.com"
export RSC_CLIENT_ID="client|xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export RSC_CLIENT_SECRET="yourServiceAccountSecret"
php ./scripts/get_token.php
These variables live in the execution environment for the duration of your session and are available to any script running in that context - interactive shells, scheduled jobs, containers, automation platforms.
Option B - Secrets File
Create a file called rsc_secrets.env and store it in a hidden folder. Since the credentials in this file sit in plain text on your machine, consider using a vault or password manager.
export RSC_FQDN="example.my.rubrik.com"
export RSC_CLIENT_ID="client|xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export RSC_CLIENT_SECRET="yourServiceAccountSecret"
# Optional settings
export RSC_VERIFY_SSL="true"
export RSC_HTTP_TIMEOUT="30"
export RSC_TOKEN_CACHE="./.rsc_token_cache.json"Load and Run:
source ./rsc_secrets.env
php ./scripts/get_token.phpThis keeps secrets out of your code, centralised, and easy to rotate.
Practitioner Tip "If your script needs credentials hardcoded to work, you're doing it wrong. Your code should run unchanged whether it's on your laptop, a server, or a pipeline." |
Option C - Automation / Production (Recommended)
In production environments, use a secret manager and inject environment variables at runtime. Keep PHP unchanged - your code always reads the same way:
getenv('RSC_CLIENT_SECRET');What "Secure" Means in Practice
Getting the code right is only half the job. The other half is operational discipline - and this is where most real-world credential incidents actually originate.
- Treat your logs as an attack surface. Debug output, HTTP traces, and error logs can silently leak tokens - audit what gets captured before you ship to production.
- Apply least-privilege permissions to every secrets file. chmod 600 on rsc_secrets.env and a .gitignore entry are non-negotiable - one accidental commit can expose credentials in perpetuity.
- Build credential rotation into your operational runbook, not your compliance checklist. If you have never practiced revoke-and-redeploy under pressure, your recovery plan is theoretical.
- Keep secrets out of your artifacts entirely. Credentials baked into images travel through registries, backups, and pipelines - runtime injection means a leaked image does not mean a leaked credential.
Designing rkRscGetToken()
With the authentication flow understood and our secrets strategy in place, we can now build the helper function. The design goal is deliberately simple:
$token = rkRscGetToken();One call. No parameters. The function handles everything else. Specifically, it should:
- Read configuration from environment variables (no parameters required)
- Use local token caching to avoid redundant API calls
- Throw clear, actionable exceptions on failure
- Work identically across local development, CI pipelines, and production environments
Implementation: rkRscGetToken()
Create the file rkRscAuth.php:
<?php
function rkRscGetToken(): string
{
// ------------------------------------------------------------
// 1) Required environment variables
// ------------------------------------------------------------
$fqdn = getenv('RSC_FQDN') ?: '';
$clientId = getenv('RSC_CLIENT_ID') ?: '';
$clientSecret = getenv('RSC_CLIENT_SECRET') ?: '';
if ($fqdn === '')
{
throw new Exception("Missing RSC_FQDN");
}
if ($clientId === '')
{
throw new Exception("Missing RSC_CLIENT_ID");
}
if ($clientSecret === '')
{
throw new Exception("Missing RSC_CLIENT_SECRET");
}
// ------------------------------------------------------------
// 2) Optional settings
// ------------------------------------------------------------
$cacheFile = getenv('RSC_TOKEN_CACHE');
if ($cacheFile === false || $cacheFile === '')
{
$cacheFile = __DIR__ . '/.rsc_token_cache.json';
}
$verifySsl = true;
$verifySslEnv = getenv('RSC_VERIFY_SSL');
if ($verifySslEnv !== false && $verifySslEnv !== '')
{
$parsed = filter_var($verifySslEnv, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
$verifySsl = ($parsed === null) ? true : $parsed;
}
$timeout = 30;
$timeoutEnv = getenv('RSC_HTTP_TIMEOUT');
if ($timeoutEnv !== false && is_numeric($timeoutEnv))
{
$timeout = (int)$timeoutEnv;
}
// ------------------------------------------------------------
// 3) Token cache
// ------------------------------------------------------------
if (file_exists($cacheFile))
{
$raw = file_get_contents($cacheFile);
if ($raw !== false)
{
$cached = json_decode($raw, true);
if (isset($cached['access_token'], $cached['expires_at']))
{
if ((int)$cached['expires_at'] > (time() + 60))
{
return (string)$cached['access_token'];
}
}
}
}
// ------------------------------------------------------------
// 4) Request token
// ------------------------------------------------------------
$url = "https://{$fqdn}/api/client_token";
$post = http_build_query(
[
'client_id' => $clientId,
'client_secret' => $clientSecret,
'grant_type' => 'client_credentials',
]
);
$ch = curl_init();
if ($ch === false)
{
throw new Exception("Failed to initialize cURL");
}
curl_setopt_array(
$ch,
[
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $post,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
],
CURLOPT_TIMEOUT => $timeout,
]
);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifySsl);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifySsl ? 2 : 0);
$responseBody = curl_exec($ch);
if ($responseBody === false)
{
throw new Exception("cURL error: " . curl_error($ch));
}
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode < 200 || $httpCode >= 300)
{
throw new Exception("HTTP {$httpCode}: {$responseBody}");
}
// curl_close() not used (deprecated in PHP 8.5+)
// ------------------------------------------------------------
// 5) Parse response
// ------------------------------------------------------------
$data = json_decode($responseBody, true);
if (!is_array($data) || empty($data['access_token']))
{
throw new Exception("Invalid token response");
}
$accessToken = (string)$data['access_token'];
$expiresAt = time() + ((int)$data['expires_in'] ?? 3600);
// ------------------------------------------------------------
// 6) Cache token
// ------------------------------------------------------------
file_put_contents(
$cacheFile,
json_encode(
[
'access_token' => $accessToken,
'expires_at' => $expiresAt,
],
JSON_PRETTY_PRINT
)
);
return $accessToken;
}
Minimal Validation Script
With rkRscAuth.php in place, verifying that authentication works takes just a handful of lines. Create scripts/get_token.php:
<?php
require_once __DIR__ . '/../rkRscAuth.php';
try
{
$token = rkRscGetToken();
echo "Token retrieved successfully (" . strlen($token) . " characters)\n";
}
catch (Exception $e)
{
echo "[ERROR] " . $e->getMessage() . "\n";
exit(1);
}Run it:
source ./rsc_secrets.env
php ./scripts/get_token.phpPractitioner Tip "f your authentication code is longer than your business logic, you should probably refactor it into a helper - like we just did." |
What We Achieved
With rkRscGetToken() complete, the authentication layer is done and out of the way. What we have now:
- No credentials in code means no credential exposure in version control, container images, or build artifacts - the risk surface shrinks immediately
- Token caching means your framework can call rkRscGetToken() freely without worrying about rate limits or unnecessary latency on every operation.
- Exceptions that name the missing variable exactly tell you what is wrong and where, without digging through stack traces at 2am.
- One function, zero environment-specific branches - the same code runs on a developer laptop, a CI runner, and a production scheduler without modification.
What's Next: Sending Your First Real Query
Authentication is done. Next week, we put it to work.
In Week 4, we will:
- Build rkGraphQLFramework.php
- Wrap GraphQL queries into reusable functions
- Run our first real RSC queries using this token
Next step: real data.
Contributed by

Frederic Lhoest
Senior Technology Architect, PCCW Global

Mike Preston
Staff Technical Marketing Manager, Rubrik







