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#
- Check out API Integration Examples for more API integration patterns
- Explore Error Handling for handling authentication errors
- See Async Patterns for asynchronous authentication