API Authentication in PHP: Using API Keys, JWT and OAuth2 Correctly
AI generated
<?php
8.4
PHP · API Design · Authentication · Security
API Authentication in PHP
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.

19 min read API Keys · JWT · OAuth2 · Bearer Token PHP 8.4

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.

11. FAQ: API Authentication in PHP

1When is an API key enough?
For server to server communication with a fixed identity, no delegated access or roles needed.
2Store API keys in plain text?
No, hash them like passwords. SHA-256 is sufficient due to the keys' high entropy.
3Advantage of JWT over API keys?
Stateless verification via signature check, no database lookup required per request.
4Why not trust the header algorithm?
Attackers could set it to none. The expected algorithm must be fixed server side.
5What is OAuth2 at its core?
An authorization framework for delegated access on behalf of a user, not a pure auth protocol.
6When does OAuth2 pay off?
For real third party access on behalf of users, not for internal APIs within the same company.
7How long should access tokens last?
Short, around 15 minutes, combined with long lived, revocable refresh tokens.
8What is refresh token rotation?
Every use issues a new refresh token and invalidates the old one immediately.
9Why separate authentication and authorization?
Because they answer different questions, and separation simplifies later mechanism migrations.
10Can multiple mechanisms be combined?
Yes, often API keys for server to server, JWT for own clients, OAuth2 only for real third parties.