Using API keys, JWT and OAuth2 correctly
Choosing the right authentication mechanism decides the security and integration effort of a PHP API. This article shows when API keys are enough, how JWT signatures really work, and when the extra effort of OAuth2 truly pays off, with secure storage and full code examples.
Table of Contents
- 1. Why authentication is an early design decision
- 2. API keys: the simplest form, use cases and limits
- 3. Generating, hashing and storing API keys securely
- 4. JWT: structure, signature and stateless verification
- 5. Validating JWT in PHP without a library black box
- 6. OAuth2: core concepts and when it is really needed
- 7. Getting token refresh and expiration right
- 8. Cleanly separating authentication and authorization
- 9. API keys vs. JWT vs. OAuth2 compared
- 10. Summary
- 11. FAQ
1. Why authentication is an early design decision
Choosing the right mechanism for API authentication is one of the decisions that is hard to change later in a PHP API, because every client is already integrated against the chosen scheme. Whoever commits too early to a complex OAuth2 flow, when a simple API key would have sufficed for the actual use case, produces unnecessary integration effort for every consumer. Conversely, whoever sticks too long with plain API keys, when several user roles and delegated permissions are actually needed, builds security gaps through improvised role logic.
The three dominant approaches for API authentication in practice are static API keys, signed JWT tokens, and the full OAuth2 authorization framework. Each of these mechanisms solves a different problem, and none is fundamentally better, the right choice depends on the number of client types, the need for delegated access, and the desired control over expiration and revocation.
This article walks through all three mechanisms in detail, with particular focus on how they are implemented correctly in PHP, including the most common security mistakes that arise in a hand rolled API authentication when details like hashing, signature verification, or expiration are overlooked.
2. API keys: the simplest form, use cases and limits
An API key is the simplest form of API authentication: a long, random string that the client sends with every request in a header, usually as Authorization: Bearer or in a custom X-API-Key header. The server checks the key against a stored list and identifies the caller through it. This model works excellently for server to server communication, where a single client operates with a fixed identity, for example a partner system that syncs data regularly.
The limits of API keys show up as soon as several users act through the same client, or access rights need to be time limited and delegated. An API key usually has no built in expiration and no granular permissions, a compromised key stays valid until manually revoked. For end user applications with a login flow, an API key is therefore usually the wrong choice, while for internal service to service communication and simple partner integrations it remains the most pragmatic solution.
3. Generating, hashing and storing API keys securely
A common mistake in hand rolled API authentication with API keys: the key is stored in plain text in the database. Just like passwords, API keys should never be persisted in plain text, because a database leak would immediately compromise all keys. The solution is to generate the key as a cryptographically secure random value, show it to the client once in plain text, and store only the hash in the database.
Unlike passwords, a fast cryptographic hash such as SHA-256 is sufficient for API keys, because the key itself already has high entropy and does not need protection against brute force dictionary attacks the way a human chosen password does. A timing safe comparison when checking the hash still matters to avoid timing attacks.
<?php
declare(strict_types=1);
/**
* Generates and verifies API keys, storing only their hash.
*/
final class ApiKeyManager
{
public function __construct(private readonly \PDO $pdo)
{
}
/**
* Generates a new key, returns the plaintext once for the client.
*/
public function generate(int $clientId): string
{
$plaintext = 'msk_' . bin2hex(random_bytes(24));
$hash = hash('sha256', $plaintext);
$stmt = $this->pdo->prepare(
'INSERT INTO api_keys (client_id, key_hash, created_at) VALUES (:client_id, :hash, NOW())'
);
$stmt->execute(['client_id' => $clientId, 'hash' => $hash]);
return $plaintext; // shown to the client exactly once
}
public function verify(string $providedKey): ?int
{
$hash = hash('sha256', $providedKey);
$stmt = $this->pdo->prepare(
'SELECT client_id FROM api_keys WHERE key_hash = :hash AND revoked_at IS NULL'
);
$stmt->execute(['hash' => $hash]);
$clientId = $stmt->fetchColumn();
return $clientId !== false ? (int) $clientId : null;
}
}
4. JWT: structure, signature and stateless verification
JWT, JSON Web Token, solves a problem that API keys do not cover: stateless API authentication without a database lookup on every request. A JWT consists of three Base64 URL encoded parts, separated by dots: a header stating the algorithm, a payload with the actual claims such as user ID and expiration, and a signature. The signature is computed over header and payload with a secret key, so any tampering with the content makes signature verification fail.
The decisive advantage over API keys: the server does not need to look up the token in a database, signature verification alone is enough to confirm authenticity and integrity. This makes JWT particularly attractive for systems with several stateless backend instances that do not want to share a common session database. The downside: once issued, a token cannot be revoked before expiration without additional infrastructure such as a blocklist.
{
"header": { "alg": "HS256", "typ": "JWT" },
"payload": {
"sub": "user-4821",
"role": "customer",
"iat": 1785484800,
"exp": 1785488400
}
}
5. Validating JWT in PHP without a library black box
Even though production PHP APIs should usually rely on a vetted library like firebase/php-jwt for JWT processing, looking at a manual implementation helps to understand what these libraries actually do. Verification consists of three steps: decoding header and payload, recomputing the signature with the secret key and comparing it to the provided signature in constant time, and finally checking the expiration in the exp claim.
A common security mistake in hand rolled JWT verification: the algorithm from the header is trusted blindly, instead of being fixed in server code as the expected algorithm. Otherwise an attacker could set the algorithm in the header to none and smuggle in an unsigned payload, a real, documented attack against poorly implemented JWT libraries.
<?php
declare(strict_types=1);
/**
* Minimal, educational JWT verification for HS256 tokens.
* Production code should use a vetted library instead.
*/
final class JwtVerifier
{
public function __construct(private readonly string $secret)
{
}
/**
* @return array<string, mixed> Decoded payload
* @throws \RuntimeException On invalid signature or expired token
*/
public function verify(string $token): array
{
[$headerB64, $payloadB64, $signatureB64] = explode('.', $token) + [null, null, null];
if ($headerB64 === null || $payloadB64 === null || $signatureB64 === null) {
throw new \RuntimeException('Malformed token.');
}
// Algorithm is fixed here, never trusted from the token header
$expectedSignature = hash_hmac('sha256', "{$headerB64}.{$payloadB64}", $this->secret, true);
$expectedB64 = rtrim(strtr(base64_encode($expectedSignature), '+/', '-_'), '=');
if (!hash_equals($expectedB64, $signatureB64)) {
throw new \RuntimeException('Invalid signature.');
}
$payload = json_decode(base64_decode(strtr($payloadB64, '-_', '+/')), true);
if (($payload['exp'] ?? 0) < time()) {
throw new \RuntimeException('Token expired.');
}
return $payload;
}
}
6. OAuth2: core concepts and when it is really needed
OAuth2 is not an authentication protocol in the strict sense, but an authorization framework for delegated access: an application gains access to resources on behalf of a user, without ever seeing that user's password. Central concepts are the resource owner, the client, the authorization server, and the resource server. After successful authorization, the client receives an access token, often implemented as a JWT, and optionally a refresh token for long term renewal.
OAuth2 pays off almost exclusively when third party applications actually need to access an API on behalf of users, for example when external partner apps are allowed to retrieve orders on behalf of a shop customer. For an internal API where client and server belong to the same company, the implementation effort of OAuth2 is disproportionately high in most cases, simpler mechanisms like API keys or directly issued JWTs are entirely sufficient here.
7. Getting token refresh and expiration right
Short expiration times for access tokens significantly reduce the damage from a stolen token, but generate more frequent renewal requests. The established solution: short lived access tokens, typically 15 minutes, combined with long lived refresh tokens that are valid exclusively for the refresh endpoint and remain revocable server side, because unlike access tokens they are stored in a database.
A refresh token should be rotated on every use, meaning a new refresh token is issued and the old one is invalidated immediately. If an already used, old refresh token is presented again, that indicates a stolen token, and the API should, as a precaution, revoke the entire token family rather than just rejecting the single request.
8. Cleanly separating authentication and authorization
A common design mistake conflates authentication, the question "who is the caller", with authorization, the question "what is this caller allowed to do". API keys, JWT, and OAuth2 solve exclusively the first problem, identifying the caller. Roles, permissions, and resource specific access rules belong in a separate authorization layer that kicks in after successful authentication and remains swappable independently of the chosen authentication mechanism.
This separation pays off especially when the authentication mechanism changes later, for example a migration from API keys to OAuth2. If authorization logic remains its own layer, the migration only affects how the caller's identity is determined, not which permissions are derived from it.
9. API keys vs. JWT vs. OAuth2 compared
The following table compares the three mechanisms along the most important decision criteria.
| Criterion | API key | JWT | OAuth2 |
|---|---|---|---|
| Implementation effort | Very low | Moderate | High |
| Stateless verification | No, DB lookup required | Yes, signature check only | Yes, access token like JWT |
| Delegated user access | Not designed for it | Only with extra logic | Core feature |
| Revocation before expiry | Simple via DB flag | Only with a blocklist | Via refresh token revocation |
| Typical use | Server to server, partners | Own mobile/web clients | Third party integrations |
In practice, many PHP APIs combine several mechanisms: API keys for server to server integrations, JWT for their own mobile client, and OAuth2 exclusively for the handful of third party partners that actually need delegated access.
Mironsoft
PHP API security and authentication architecture
The right authentication for your PHP API?
We analyze your use case, choose between API keys, JWT and OAuth2 the fitting solution, and implement secure storage, signature verification, and token refresh production ready.
Security audit
Reviewing existing authentication logic for weaknesses
JWT/OAuth2 rollout
Implementing signature verification, expiration and refresh flows cleanly
Authorization layer
Cleanly separating roles and permissions from authentication
10. Summary
API authentication in PHP is not a one size fits all decision: API keys remain the pragmatic choice for server to server communication with a fixed identity, JWT brings stateless verification for your own mobile and web clients, and OAuth2 pays off almost exclusively for delegated access by real third parties. Choosing between these three mechanisms based on their actual use case rather than trend avoids both unnecessary implementation effort and mismatched security gaps.
Regardless of the chosen mechanism, the same basic rules apply: never store keys and secrets in plain text, always compare signatures in constant time, fix the expected algorithm in server code rather than trusting the token, and strictly separate authentication from authorization. In the end, these fundamentals decide more about the security of a PHP API than the choice between API key, JWT, and OAuth2 itself.
API Authentication in PHP: The Key Points at a Glance
API keys
Store only hashed, ideal for server to server, no built in granular permissions or expiration.
JWT
Stateless signature check instead of a DB lookup, always fix the algorithm server side, never trust the header.
OAuth2
Use only for real delegated third party access, otherwise disproportionately high effort.
Refresh strategy
Short lived access tokens plus rotating, revocable refresh tokens for the right balance of security and usability.