Most APIs need to know who is calling. There are a few common ways to prove it. This lesson shows how to send credentials safely when you call an API, and how to check them when you build one.
1. API keys
A long random string that identifies the caller. Sent in a header (best) or, if the service requires it, in the query string.
<?php
$ch = curl_init("https://api.example.com/data");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["X-API-Key: " . getenv("EXAMPLE_API_KEY")],
]);
A key in a URL can end up in logs, browser history and the Referer header. Prefer headers.
2. Bearer tokens
The standard header for tokens (including OAuth access tokens and JWTs):
<?php
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("EXAMPLE_API_TOKEN")]
3. Basic authentication
A username and password, sent as Authorization: Basic <base64(user:pass)>. It is not encrypted, only encoded, so it is safe only over HTTPS.
<?php
echo "Authorization: Basic " . base64_encode("user:secret");
Authorization: Basic dXNlcjpzZWNyZXQ=
4. OAuth 2.0
When your app acts for a user on another service (“log in with Google”, “post to a user’s account”), the standard is OAuth 2.0. The user approves access on the other service, which gives your app a token. You never see the user’s password. Use a maintained library (such as league/oauth2-client) rather than writing the flow yourself.
Keep keys secret
- Store keys in environment variables or a config file outside the web root (Environment Variables).
- Never put a secret API key in JavaScript, in HTML, or in a mobile app. Anyone can read it. Call the API from your server and expose only your own endpoint.
- Do not commit keys to Git. If one leaks, rotate it (create a new key and delete the old).
- Use keys with the least permissions the service allows.
- Use separate keys for development and production.
Checking credentials on your own API
When you build an API, check the key or token on every request, in constant time.
<?php
function validApiKey(string $given, array $validKeyHashes): bool
{
$hash = hash("sha256", $given);
foreach ($validKeyHashes as $stored) {
if (hash_equals($stored, $hash)) {
return true;
}
}
return false;
}
$stored = [hash("sha256", "key_live_abc123")]; // in practice: hashes saved in the database
var_dump(validApiKey("key_live_abc123", $stored));
var_dump(validApiKey("guess", $stored));
bool(true)
bool(false)
Store only the hash of each key, so a database leak does not expose working keys. Show the key to the user once, when it is created.
Read the header on the server
<?php
function bearerToken(array $server): ?string
{
$header = $server["HTTP_AUTHORIZATION"] ?? "";
if (preg_match('/^Bearer\s+(\S+)$/i', $header, $m) === 1) {
return $m[1];
}
return null;
}
var_dump(bearerToken(["HTTP_AUTHORIZATION" => "Bearer abc.def"]));
var_dump(bearerToken([]));
string(7) "abc.def"
NULL
(On some Apache setups, the Authorization header must be passed to PHP with a rewrite rule.)
Respond correctly
- Missing or wrong credentials:
401 Unauthorized, with aWWW-Authenticateheader. - Valid credentials, but not allowed:
403 Forbidden. - Too many requests:
429.
Webhooks: verify the signature
When a service calls your URL (a payment confirmation, for example), anyone could send a fake call. Services sign their messages with a secret. You recompute the signature and compare.
<?php
function signatureIsValid(string $payload, string $signatureHeader, string $secret): bool
{
$expected = hash_hmac("sha256", $payload, $secret);
return hash_equals($expected, $signatureHeader);
}
$secret = "whsec_example_secret";
$payload = '{"event":"payment.paid","amount":500}';
$signature = hash_hmac("sha256", $payload, $secret); // the service computes this and sends it
var_dump(signatureIsValid($payload, $signature, $secret));
var_dump(signatureIsValid($payload . " ", $signature, $secret)); // the body was changed
bool(true)
bool(false)
Read the raw body (file_get_contents("php://input")) for the check, not a re-encoded version. Use hash_equals(), check any timestamp the service includes to avoid replays, and handle the same event more than once (webhooks are often delivered twice).
Rate limiting
Limit how many requests each key may make per minute, and answer 429 when exceeded. See Authentication Security.