friendship ended with social-app. php is my new best friend

title: Authentication Examples description: Examples of different authentication methods with the Fetch HTTP package#

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#

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#

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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#