Skip to content

Authentication Examples

This page provides examples of different authentication methods when working with APIs using the Fetch HTTP package.

API Key Authentication

Many APIs use API keys for authentication, typically sent as a header or query parameter:

API Key in Header

php
use function Fetch\Http\fetch;

// API key in a custom header
$response = fetch('https://api.example.com/data', [
    'headers' => [
        'X-API-Key' => 'your-api-key'
    ]
]);

// Using the fluent interface
$response = fetch()
    ->withHeader('X-API-Key', 'your-api-key')
    ->get('https://api.example.com/data');

API Key in Query Parameter

php
use function Fetch\Http\get;

// API key as a query parameter
$response = get('https://api.example.com/data', [
    'api_key' => 'your-api-key'
]);

// Using the fetch function
$response = fetch('https://api.example.com/data', [
    'query' => ['api_key' => 'your-api-key']
]);

Bearer Token Authentication

Bearer tokens are commonly used with OAuth 2.0 and JWT:

php
use function Fetch\Http\fetch;

// Using the token option
$response = fetch('https://api.example.com/me', [
    'token' => 'your-access-token'
]);

// Using the Authorization header directly
$response = fetch('https://api.example.com/me', [
    'headers' => [
        'Authorization' => 'Bearer your-access-token'
    ]
]);

// Using the fluent interface
$response = fetch()
    ->withToken('your-access-token')
    ->get('https://api.example.com/me');

Basic Authentication

Basic authentication sends credentials in the Authorization header:

php
use function Fetch\Http\fetch;

// Using the auth option
$response = fetch('https://api.example.com/protected', [
    'auth' => ['username', 'password']
]);

// Using the fluent interface
$response = fetch()
    ->withAuth('username', 'password')
    ->get('https://api.example.com/protected');

// Manually setting the Authorization header with Base64 encoding
$credentials = base64_encode('username:password');
$response = fetch('https://api.example.com/protected', [
    'headers' => [
        'Authorization' => "Basic {$credentials}"
    ]
]);

Digest Authentication

Some APIs use digest authentication, which requires a more complex challenge-response flow:

php
use function Fetch\Http\fetch;
use GuzzleHttp\Client;

// The easiest way is to use Guzzle's built-in support
$client = new Client();
fetch_client(['client' => $client]);

$response = fetch('https://api.example.com/protected', [
    'auth' => ['username', 'password', 'digest']
]);

OAuth 1.0a Authentication

OAuth 1.0a requires signing requests with a complex algorithm:

php
use function Fetch\Http\fetch;
use GuzzleHttp\Subscriber\Oauth\Oauth1;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Client;

// Create a handler stack
$stack = HandlerStack::create();

// Add the OAuth 1.0 middleware
$middleware = new Oauth1([
    'consumer_key'    => 'your-consumer-key',
    'consumer_secret' => 'your-consumer-secret',
    'token'           => 'your-token',
    'token_secret'    => 'your-token-secret'
]);
$stack->push($middleware);

// Create a client with the handler
$client = new Client(['handler' => $stack]);
fetch_client(['client' => $client]);

// Make a request - OAuth 1.0 signature is added automatically
$response = fetch('https://api.example.com/resources');

// Reset when done
fetch_client(reset: true);

OAuth 2.0 Client Credentials Flow

For server-to-server API authentication:

php
use function Fetch\Http\fetch;
use function Fetch\Http\post;

function getAccessToken(string $clientId, string $clientSecret, string $tokenUrl): string
{
    // Request an access token
    $response = post($tokenUrl, [
        'grant_type' => 'client_credentials',
        'client_id' => $clientId,
        'client_secret' => $clientSecret
    ], [
        'headers' => [
            'Content-Type' => 'application/x-www-form-urlencoded',
            'Accept' => 'application/json'
        ]
    ]);

    if (!$response->successful()) {
        throw new \RuntimeException(
            "Failed to get access token: " . $response->status() . " " . $response->body()
        );
    }

    $tokenData = $response->json();

    if (!isset($tokenData['access_token'])) {
        throw new \RuntimeException("No access token in response");
    }

    return $tokenData['access_token'];
}

// Get an access token
$token = getAccessToken(
    'your-client-id',
    'your-client-secret',
    'https://auth.example.com/oauth/token'
);

// Use the token for API requests
$response = fetch('https://api.example.com/data', [
    'token' => $token
]);

// Process the response
if ($response->successful()) {
    $data = $response->json();
    // Process data...
}

OAuth 2.0 Authorization Code Flow

For web apps that need user authentication:

php
use function Fetch\Http\fetch;
use function Fetch\Http\post;

class OAuth2Client
{
    private string $clientId;
    private string $clientSecret;
    private string $redirectUri;
    private string $tokenUrl;
    private string $authorizationUrl;

    public function __construct(
        string $clientId,
        string $clientSecret,
        string $redirectUri,
        string $tokenUrl,
        string $authorizationUrl
    ) {
        $this->clientId = $clientId;
        $this->clientSecret = $clientSecret;
        $this->redirectUri = $redirectUri;
        $this->tokenUrl = $tokenUrl;
        $this->authorizationUrl = $authorizationUrl;
    }

    public function getAuthorizationUrl(array $scopes = [], string $state = null): string
    {
        $params = [
            'client_id' => $this->clientId,
            'redirect_uri' => $this->redirectUri,
            'response_type' => 'code',
            'scope' => implode(' ', $scopes)
        ];

        if ($state) {
            $params['state'] = $state;
        }

        return $this->authorizationUrl . '?' . http_build_query($params);
    }

    public function getAccessToken(string $code): array
    {
        $response = post($this->tokenUrl, [
            'grant_type' => 'authorization_code',
            'code' => $code,
            'client_id' => $this->clientId,
            'client_secret' => $this->clientSecret,
            'redirect_uri' => $this->redirectUri
        ], [
            'headers' => [
                'Content-Type' => 'application/x-www-form-urlencoded',
                'Accept' => 'application/json'
            ]
        ]);

        if (!$response->successful()) {
            throw new \RuntimeException(
                "Failed to get access token: " . $response->status() . " " . $response->body()
            );
        }

        return $response->json();
    }

    public function refreshToken(string $refreshToken): array
    {
        $response = post($this->tokenUrl, [
            'grant_type' => 'refresh_token',
            'refresh_token' => $refreshToken,
            'client_id' => $this->clientId,
            'client_secret' => $this->clientSecret
        ], [
            'headers' => [
                'Content-Type' => 'application/x-www-form-urlencoded',
                'Accept' => 'application/json'
            ]
        ]);

        if (!$response->successful()) {
            throw new \RuntimeException(
                "Failed to refresh token: " . $response->status() . " " . $response->body()
            );
        }

        return $response->json();
    }

    public function request(string $url, string $accessToken, string $method = 'GET', array $options = []): array
    {
        // Set the Authorization header
        $options['headers'] = $options['headers'] ?? [];
        $options['headers']['Authorization'] = "Bearer {$accessToken}";
        $options['method'] = $method;

        $response = fetch($url, $options);

        if (!$response->successful()) {
            throw new \RuntimeException(
                "API request failed: " . $response->status() . " " . $response->body()
            );
        }

        return $response->json();
    }
}

// Usage in a controller
function oauthCallback()
{
    // Create an OAuth2 client
    $oauth = new OAuth2Client(
        'your-client-id',
        'your-client-secret',
        'https://your-app.com/oauth/callback',
        'https://auth.example.com/oauth/token',
        'https://auth.example.com/oauth/authorize'
    );

    // Check for authorization code in the request
    $code = $_GET['code'] ?? null;
    $state = $_GET['state'] ?? null;

    // Verify state parameter (CSRF protection)
    if ($state !== $_SESSION['oauth_state']) {
        die('Invalid state parameter');
    }

    try {
        // Exchange the code for tokens
        $tokens = $oauth->getAccessToken($code);

        // Store tokens securely
        $_SESSION['access_token'] = $tokens['access_token'];
        $_SESSION['refresh_token'] = $tokens['refresh_token'] ?? null;
        $_SESSION['token_expires_at'] = time() + ($tokens['expires_in'] ?? 3600);

        // Fetch user profile
        $user = $oauth->request(
            'https://api.example.com/me',
            $tokens['access_token']
        );

        // Store user data
        $_SESSION['user'] = $user;

        // Redirect to dashboard
        header('Location: /dashboard');
        exit;
    } catch (\Exception $e) {
        die('Authentication error: ' . $e->getMessage());
    }
}

// Starting the OAuth flow
function startOAuth()
{
    $oauth = new OAuth2Client(
        'your-client-id',
        'your-client-secret',
        'https://your-app.com/oauth/callback',
        'https://auth.example.com/oauth/token',
        'https://auth.example.com/oauth/authorize'
    );

    // Generate a random state parameter for CSRF protection
    $state = bin2hex(random_bytes(16));
    $_SESSION['oauth_state'] = $state;

    // Redirect user to authorization URL
    $authUrl = $oauth->getAuthorizationUrl(
        ['profile', 'email', 'read', 'write'],
        $state
    );

    header('Location: ' . $authUrl);
    exit;
}

Token Refresh Logic

Handling token expiration and refresh:

php
use function Fetch\Http\fetch;
use function Fetch\Http\post;

class TokenManager
{
    private string $clientId;
    private string $clientSecret;
    private string $tokenUrl;
    private ?string $accessToken = null;
    private ?string $refreshToken = null;
    private ?int $expiresAt = null;

    public function __construct(
        string $clientId,
        string $clientSecret,
        string $tokenUrl
    ) {
        $this->clientId = $clientId;
        $this->clientSecret = $clientSecret;
        $this->tokenUrl = $tokenUrl;
    }

    public function setTokens(string $accessToken, ?string $refreshToken = null, ?int $expiresIn = null)
    {
        $this->accessToken = $accessToken;
        $this->refreshToken = $refreshToken;

        if ($expiresIn !== null) {
            $this->expiresAt = time() + $expiresIn - 60; // 60-second buffer
        } else {
            $this->expiresAt = null;
        }
    }

    public function getAccessToken(): string
    {
        if ($this->isTokenExpired() && $this->refreshToken) {
            $this->refreshAccessToken();
        }

        if (!$this->accessToken) {
            throw new \RuntimeException("No access token available");
        }

        return $this->accessToken;
    }

    private function isTokenExpired(): bool
    {
        if (!$this->expiresAt) {
            return false; // No expiration time, assume still valid
        }

        return time() >= $this->expiresAt;
    }

    private function refreshAccessToken()
    {
        if (!$this->refreshToken) {
            throw new \RuntimeException("No refresh token available");
        }

        try {
            $response = post($this->tokenUrl, [
                'grant_type' => 'refresh_token',
                'refresh_token' => $this->refreshToken,
                'client_id' => $this->clientId,
                'client_secret' => $this->clientSecret
            ], [
                'headers' => [
                    'Content-Type' => 'application/x-www-form-urlencoded',
                    'Accept' => 'application/json'
                ]
            ]);

            if (!$response->successful()) {
                throw new \RuntimeException(
                    "Failed to refresh token: " . $response->status() . " " . $response->body()
                );
            }

            $tokenData = $response->json();

            $this->accessToken = $tokenData['access_token'];
            $this->refreshToken = $tokenData['refresh_token'] ?? $this->refreshToken;

            if (isset($tokenData['expires_in'])) {
                $this->expiresAt = time() + $tokenData['expires_in'] - 60;
            }
        } catch (\Exception $e) {
            // If refresh fails, invalidate tokens to force re-authentication
            $this->accessToken = null;
            $this->refreshToken = null;
            $this->expiresAt = null;

            throw new \RuntimeException("Token refresh failed: " . $e->getMessage(), 0, $e);
        }
    }
}

// Usage with a protected API client
class ApiClient
{
    private string $baseUrl;
    private TokenManager $tokenManager;

    public function __construct(string $baseUrl, TokenManager $tokenManager)
    {
        $this->baseUrl = rtrim($baseUrl, '/');
        $this->tokenManager = $tokenManager;
    }

    public function get(string $endpoint, array $query = null)
    {
        return $this->request('GET', $endpoint, $query);
    }

    public function post(string $endpoint, array $data)
    {
        return $this->request('POST', $endpoint, $data);
    }

    private function request(string $method, string $endpoint, ?array $data = null)
    {
        try {
            // Get a valid access token
            $accessToken = $this->tokenManager->getAccessToken();

            $options = [
                'method' => $method,
                'headers' => [
                    'Authorization' => "Bearer {$accessToken}",
                    'Accept' => 'application/json'
                ]
            ];

            if ($data !== null) {
                if ($method === 'GET') {
                    $options['query'] = $data;
                } else {
                    $options['json'] = $data;
                }
            }

            $response = fetch($this->baseUrl . '/' . ltrim($endpoint, '/'), $options);

            if ($response->status() === 401 && $response->body() === 'Token has expired') {
                // Force token refresh and retry
                $this->tokenManager->setTokens(null);
                return $this->request($method, $endpoint, $data);
            }

            if (!$response->successful()) {
                throw new \RuntimeException(
                    "API request failed: " . $response->status() . " " . $response->body()
                );
            }

            return $response->json();
        } catch (\Exception $e) {
            // Handle exceptions
            throw new \RuntimeException("API request error: " . $e->getMessage(), 0, $e);
        }
    }
}

// Example usage
$tokenManager = new TokenManager(
    'your-client-id',
    'your-client-secret',
    'https://auth.example.com/oauth/token'
);

// Initial token setup (from a previous auth flow)
$tokenManager->setTokens(
    'initial-access-token',
    'refresh-token',
    3600 // Expires in 1 hour
);

$api = new ApiClient('https://api.example.com', $tokenManager);

try {
    // This will automatically refresh the token if needed
    $user = $api->get('me');
    echo "User profile: " . $user['name'];

    $posts = $api->get('posts', ['limit' => 10]);
    echo "Found " . count($posts) . " posts";
} catch (\Exception $e) {
    echo "Error: " . $e->getMessage();
}

JWT Authentication

Using JSON Web Tokens for authentication:

php
use function Fetch\Http\fetch;

// Function to create a JWT
function createJwt(array $payload, string $secret): string
{
    // Create header
    $header = [
        'alg' => 'HS256',
        'typ' => 'JWT'
    ];

    // Encode Header
    $header = base64UrlEncode(json_encode($header));

    // Encode Payload
    $payload = base64UrlEncode(json_encode($payload));

    // Create Signature
    $signature = hash_hmac('sha256', "$header.$payload", $secret, true);
    $signature = base64UrlEncode($signature);

    // Create JWT
    return "$header.$payload.$signature";
}

// Base64Url encode helper function
function base64UrlEncode(string $data): string
{
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

// Create JWT payload
$payload = [
    'sub' => '123456',
    'name' => 'John Doe',
    'iat' => time(),
    'exp' => time() + 3600 // Expires in 1 hour
];

// Generate JWT
$jwt = createJwt($payload, 'your-jwt-secret');

// Use JWT for API requests
$response = fetch('https://api.example.com/protected', [
    'headers' => [
        'Authorization' => "Bearer {$jwt}"
    ]
]);

if ($response->successful()) {
    $data = $response->json();
    echo "Successfully authenticated with JWT";
} else {
    echo "JWT authentication failed: " . $response->status();
}

API Key Rotation

Handling API key rotation for reliability:

php
use function Fetch\Http\fetch;

class ApiKeyManager
{
    private array $apiKeys;
    private int $currentKeyIndex = 0;
    private array $failedKeys = [];

    public function __construct(array $apiKeys)
    {
        if (empty($apiKeys)) {
            throw new \InvalidArgumentException("At least one API key must be provided");
        }

        $this->apiKeys = $apiKeys;
    }

    public function getCurrentKey(): string
    {
        return $this->apiKeys[$this->currentKeyIndex];
    }

    public function markCurrentKeyAsFailed(): bool
    {
        $failedKey = $this->getCurrentKey();
        $this->failedKeys[$failedKey] = time();

        // Try to rotate to the next valid key
        return $this->rotateToNextValidKey();
    }

    public function rotateToNextValidKey(): bool
    {
        $initialIndex = $this->currentKeyIndex;
        $keysChecked = 0;

        do {
            // Move to next key (with wraparound)
            $this->currentKeyIndex = ($this->currentKeyIndex + 1) % count($this->apiKeys);
            $keysChecked++;

            // Check if we've tried all keys
            if ($keysChecked >= count($this->apiKeys)) {
                // Reset to initial key
                $this->currentKeyIndex = $initialIndex;
                return false;
            }

            // Check if current key is valid
            $currentKey = $this->getCurrentKey();

            // If key was marked as failed more than 5 minutes ago, try it again
            if (isset($this->failedKeys[$currentKey]) &&
                time() - $this->failedKeys[$currentKey] > 300) {
                unset($this->failedKeys[$currentKey]);
            }
        } while (isset($this->failedKeys[$this->getCurrentKey()]));

        return true;
    }
}

class ApiClient
{
    private string $baseUrl;
    private ApiKeyManager $keyManager;

    public function __construct(string $baseUrl, ApiKeyManager $keyManager)
    {
        $this->baseUrl = rtrim($baseUrl, '/');
        $this->keyManager = $keyManager;
    }

    public function request(string $endpoint, string $method = 'GET', array $data = null)
    {
        $maxRetries = 3;
        $attempts = 0;

        while ($attempts < $maxRetries) {
            try {
                $apiKey = $this->keyManager->getCurrentKey();

                $options = [
                    'method' => $method,
                    'headers' => [
                        'X-API-Key' => $apiKey,
                        'Accept' => 'application/json'
                    ]
                ];

                if ($data !== null) {
                    if ($method === 'GET') {
                        $options['query'] = $data;
                    } else {
                        $options['json'] = $data;
                    }
                }

                $response = fetch($this->baseUrl . '/' . ltrim($endpoint, '/'), $options);

                // Handle API key errors
                if ($response->status() === 401 || $response->status() === 403) {
                    // This key might be invalid or rate limited
                    $hasValidKey = $this->keyManager->markCurrentKeyAsFailed();

                    if (!$hasValidKey) {
                        throw new \RuntimeException("All API keys are invalid or rate limited");
                    }

                    $attempts++;
                    continue;
                }

                // Handle other errors
                if (!$response->successful()) {
                    throw new \RuntimeException(
                        "API request failed: " . $response->status() . " " . $response->body()
                    );
                }

                return $response->json();
            } catch (\Exception $e) {
                $attempts++;

                if ($attempts >= $maxRetries) {
                    throw $e;
                }
            }
        }
    }
}

// Usage
$keyManager = new ApiKeyManager([
    'primary-api-key-123',
    'backup-api-key-456',
    'emergency-api-key-789'
]);

$api = new ApiClient('https://api.example.com', $keyManager);

try {
    $data = $api->request('data');
    echo "Successfully fetched data with API key";
} catch (\Exception $e) {
    echo "Error: " . $e->getMessage();
}

Multi-tenant API Client

An API client for applications that need to support multiple users or organizations:

php
use function Fetch\Http\fetch;
use function Matrix\async;
use function Matrix\await;

class MultiTenantApiClient
{
    private string $baseUrl;
    private array $tenantTokens = [];

    public function __construct(string $baseUrl)
    {
        $this->baseUrl = rtrim($baseUrl, '/');
    }

    public function setTenantToken(string $tenantId, string $token): void
    {
        $this->tenantTokens[$tenantId] = $token;
    }

    public function getTenantToken(string $tenantId): ?string
    {
        return $this->tenantTokens[$tenantId] ?? null;
    }

    public function request(string $tenantId, string $endpoint, string $method = 'GET', array $data = null)
    {
        $token = $this->getTenantToken($tenantId);

        if (!$token) {
            throw new \RuntimeException("No token available for tenant: {$tenantId}");
        }

        $options = [
            'method' => $method,
            'headers' => [
                'Authorization' => "Bearer {$token}",
                'Accept' => 'application/json',
                'X-Tenant-ID' => $tenantId
            ]
        ];

        if ($data !== null) {
            if ($method === 'GET') {
                $options['query'] = $data;
            } else {
                $options['json'] = $data;
            }
        }

        $response = fetch($this->baseUrl . '/' . ltrim($endpoint, '/'), $options);

        if (!$response->successful()) {
            throw new \RuntimeException(
                "API request failed for tenant {$tenantId}: " .
                $response->status() . " " . $response->body()
            );
        }

        return $response->json();
    }

    public async function requestAsync(string $tenantId, string $endpoint, string $method = 'GET', array $data = null)
    {
        return async(function() use ($tenantId, $endpoint, $method, $data) {
            return $this->request($tenantId, $endpoint, $method, $data);
        });
    }

    public async function requestForAllTenants(string $endpoint, string $method = 'GET', array $data = null)
    {
        $promises = [];

        foreach (array_keys($this->tenantTokens) as $tenantId) {
            $promises[$tenantId] = $this->requestAsync($tenantId, $endpoint, $method, $data);
        }

        return await(all($promises));
    }
}

// Usage
$multiTenantApi = new MultiTenantApiClient('https://api.example.com');

// Set up tokens for different tenants
$multiTenantApi->setTenantToken('tenant1', 'token-for-tenant1');
$multiTenantApi->setTenantToken('tenant2', 'token-for-tenant2');
$multiTenantApi->setTenantToken('tenant3', 'token-for-tenant3');

// Make a request for a specific tenant
try {
    $user = $multiTenantApi->request('tenant1', 'users/me');
    echo "Tenant1 user: " . $user['name'];
} catch (\Exception $e) {
    echo "Tenant1 error: " . $e->getMessage();
}

// Make requests for all tenants in parallel
await(async(function() use ($multiTenantApi) {
    try {
        $allTenantsData = await($multiTenantApi->requestForAllTenants('stats/summary'));

        foreach ($allTenantsData as $tenantId => $stats) {
            echo "{$tenantId} stats: " . json_encode($stats) . "\n";
        }
    } catch (\Exception $e) {
        echo "Error fetching data for all tenants: " . $e->getMessage();
    }
}));

Handling Authentication Challenges

Dealing with complex authentication flows:

php
use function Fetch\Http\fetch;

function fetchWithAuthChallenge(string $url, array $options = [])
{
    // First request
    $response = fetch($url, $options);

    // Check if we got an auth challenge
    if ($response->status() === 401) {
        $challengeHeader = $response->header('WWW-Authenticate');

        if ($challengeHeader && strpos($challengeHeader, 'Digest') === 0) {
            // Parse the digest challenge
            preg_match_all('/(\w+)=(?:"([^"]+)"|([^,]+))/', $challengeHeader, $matches, PREG_SET_ORDER);

            $challenge = [];
            foreach ($matches as $match) {
                $challenge[$match[1]] = $match[2] ?: $match[3];
            }

            if (isset($challenge['nonce'], $challenge['realm'])) {
                // Compute the digest response
                $username = 'your-username';
                $password = 'your-password';
                $method = $options['method'] ?? 'GET';

                $ha1 = md5("{$username}:{$challenge['realm']}:{$password}");
                $ha2 = md5("{$method}:{$url}");
                $nc = '00000001';
                $cnonce = md5(uniqid());

                $response = md5("{$ha1}:{$challenge['nonce']}:{$nc}:{$cnonce}:{$challenge['qop']}:{$ha2}");

                // Build the Authorization header
                $authHeader = 'Digest ' .
                    "username=\"{$username}\", " .
                    "realm=\"{$challenge['realm']}\", " .
                    "nonce=\"{$challenge['nonce']}\", " .
                    "uri=\"{$url}\", " .
                    "cnonce=\"{$cnonce}\", " .
                    "nc={$nc}, " .
                    "qop={$challenge['qop']}, " .
                    "response=\"{$response}\"";

                // Set the Authorization header for the second request
                $options['headers'] = $options['headers'] ?? [];
                $options['headers']['Authorization'] = $authHeader;

                // Make the second request with the auth response
                return fetch($url, $options);
            }
        }
    }

    // Either no challenge or we couldn't handle it
    return $response;
}

// Usage
$response = fetchWithAuthChallenge('https://api.example.com/protected');

if ($response->successful()) {
    $data = $response->json();
    echo "Successfully authenticated";
} else {
    echo "Authentication failed: " . $response->status();
}

Next Steps

Released under the MIT License. A modern HTTP client for PHP developers.