🔐 Autenticação de API

💡 Porquê Autenticação?

APIs precisam de saber quem estás e o que podes fazer. A autenticação garante que apenas utilizadores autorizados acedam aos recursos.

Tipos de Autenticação

🔑 API Key

Chave simples enviada em cada pedido. Comum em APIs públicas.

Segurança: Baixa-Média

👤 Basic Auth

Username e password codificados em Base64. Simples mas inseguro.

Segurança: Baixa (só com HTTPS)

🎫 Bearer Token

Token de acesso enviado no header. Permite scopes e permissões.

Segurança: Média-Alta

🔄 OAuth 2.0

Protocolo completo para autorização. Usado por Google, Facebook, etc.

Segurança: Alta

🔑 API Key

A forma mais simples de autenticação. Uma chave única que identifica a tua aplicação.

Como Funciona

App PHP
→ API Key →
API Server

A chave é enviada em cada pedido

Header

API Key no Header
<?php
use GuzzleHttp\Client;

$client = new Client([
    'base_uri' => 'https://api.exemplo.com',
    'headers' => [
        'X-API-Key' => getenv('API_KEY'), // Melhor que colocar diretamente!
        'Accept' => 'application/json'
    ]
]);

$response = $client->get('/users');
?>

Query Parameter

API Key na URL
<?php
// Menos seguro - visível na URL
$response = $client->get('/users', [
    'query' => [
        'api_key' => getenv('API_KEY')
    ]
]);
?>
⚠️ Segurança
  • Nunca coloques API keys no código fonte (usa .env)
  • Não commits API keys para repositórios públicos
  • Usa HTTPS para proteger a chave em trânsito
  • Regista as chaves para detetar uso indevido

👤 Basic Authentication

Como Funciona

Envia username e password codificados em Base64 no header Authorization.

Basic Auth com Guzzle
<?php
use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.exemplo.com']);

// Método 1: Usando opção 'auth'
$response = $client->get('/protected', [
    'auth' => [
        'username',
        'password'
    ]
]);

// Método 2: Header manual
$credentials = base64_encode('username:password');
$response = $client->get('/protected', [
    'headers' => [
        'Authorization' => 'Basic ' . $credentials
    ]
]);
?>

cURL

Basic Auth com cURL
<?php
$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL => 'https://api.exemplo.com/protected',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_USERPWD => 'username:password'
]);

$response = curl_exec($ch);
curl_close($ch);
?>
⚠️ Nunca sem HTTPS!

Basic Auth envia as credenciais em Base64 (que é facilmente reversível). Sempre usa HTTPS para proteger as credenciais!

🎫 Bearer Token

O que é?

Um token de acesso (geralmente JWT ou OAuth token) que autoriza o acesso a recursos.

Bearer Token com Guzzle
<?php
use GuzzleHttp\Client;

// Token obtido previamente (login, OAuth, etc.)
$token = getenv('ACCESS_TOKEN');

$client = new Client([
    'base_uri' => 'https://api.exemplo.com',
    'headers' => [
        'Authorization' => 'Bearer ' . $token,
        'Accept' => 'application/json'
    ]
]);

// Todos os pedidos usarão o token automaticamente
$response = $client->get('/protected-resource');
?>

Token por Pedido

Token por Pedido
<?php
$client = new Client(['base_uri' => 'https://api.exemplo.com']);

$response = $client->get('/resource', [
    'headers' => [
        'Authorization' => 'Bearer ' . $token
    ]
]);
?>

Renovar Token Expirado

Middleware para Renovar Token
<?php
class ApiClient {
    private $client;
    private $token;
    private $tokenExpiry;

    public function __construct() {
        $this->client = new Client(['base_uri' => 'https://api.exemplo.com']);
    }

    private function ensureValidToken() {
        if (!$this->token || time() >= $this->tokenExpiry) {
            $this->refreshToken();
        }
    }

    private function refreshToken() {
        $response = $this->client->post('/auth/refresh', [
            'json' => [
                'refresh_token' => getenv('REFRESH_TOKEN')
            ]
        ]);

        $data = json_decode($response->getBody(), true);
        $this->token = $data['access_token'];
        $this->tokenExpiry = time() + $data['expires_in'] - 60; // 1 min margem
    }

    public function request($method, $uri, $options = []) {
        $this->ensureValidToken();

        $options['headers']['Authorization'] = 'Bearer ' . $this->token;

        return $this->client->request($method, $uri, $options);
    }
}
?>

🔄 OAuth 2.0

O que é OAuth 2.0?

Protocolo de autorização que permite a aplicações aceder a recursos em nome de um utilizador, sem saber a password.

Fluxo Authorization Code

1. Utilizador
→ Autoriza →
App
2. App
→ Redirect →
Auth Server
3. Auth Server
→ Code →
App
4. App
→ Code + Secret →
Auth Server
5. Auth Server
→ Access Token →
App

Obter Token OAuth

OAuth 2.0 Token Request
<?php
use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://oauth.exemplo.com']);

// Trocar authorization code por token
$response = $client->post('/oauth/token', [
    'form_params' => [
        'grant_type' => 'authorization_code',
        'code' => $_GET['code'],
        'client_id' => getenv('OAUTH_CLIENT_ID'),
        'client_secret' => getenv('OAUTH_CLIENT_SECRET'),
        'redirect_uri' => 'https://meusite.com/callback'
    ]
]);

$tokenData = json_decode($response->getBody(), true);

// Guardar tokens
$accessToken = $tokenData['access_token'];
$refreshToken = $tokenData['refresh_token'];
$expiresIn = $tokenData['expires_in'];

// Usar o access token
$apiClient = new Client([
    'base_uri' => 'https://api.exemplo.com',
    'headers' => [
        'Authorization' => 'Bearer ' . $accessToken
    ]
]);
?>

Client Credentials Flow

Para autenticação máquina-a-máquina (sem utilizador):

Client Credentials
<?php
$response = $client->post('/oauth/token', [
    'form_params' => [
        'grant_type' => 'client_credentials',
        'client_id' => getenv('CLIENT_ID'),
        'client_secret' => getenv('CLIENT_SECRET'),
        'scope' => 'read write'
    ]
]);

$tokenData = json_decode($response->getBody(), true);
$accessToken = $tokenData['access_token'];
?>

Refresh Token

Refresh Token
<?php
// Quando o access token expira, usar refresh token
$response = $client->post('/oauth/token', [
    'form_params' => [
        'grant_type' => 'refresh_token',
        'refresh_token' => $storedRefreshToken,
        'client_id' => getenv('CLIENT_ID'),
        'client_secret' => getenv('CLIENT_SECRET')
    ]
]);

$newTokenData = json_decode($response->getBody(), true);
?>

📜 JWT (JSON Web Token)

O que é JWT?

Um token compacto e autossuficiente que contém informações (claims) codificadas em JSON.

Estrutura do JWT

Header
.
Payload
.
Signature

Cada parte é codificada em Base64

Decodificar JWT

Decodificar JWT (sem verificação)
<?php
// Decodificar JWT sem biblioteca
function decodeJwt($token) {
    $parts = explode('.', $token);

    if (count($parts) !== 3) {
        throw new Exception('Token inválido');
    }

    $header = json_decode(base64_decode($parts[0]), true);
    $payload = json_decode(base64_decode($parts[1]), true);

    return [
        'header' => $header,
        'payload' => $payload,
        'signature' => $parts[2]
    ];
}

// Uso
$jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
$decoded = decodeJwt($jwt);

echo "ID: " . $decoded['payload']['sub'];
echo "Expira: " . date('Y-m-d H:i:s', $decoded['payload']['exp']);
?>

Verificar JWT com Biblioteca

Usando firebase/php-jwt
# Instalar
composer require firebase/php-jwt
<?php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

$jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
$key = 'sua_chave_secreta';

try {
    $decoded = JWT::decode($jwt, new Key($key, 'HS256'));
    print_r($decoded);
} catch (\Exception $e) {
    echo "Token inválido: " . $e->getMessage();
}
?>

Criar JWT

Criar JWT
<?php
use Firebase\JWT\JWT;

$payload = [
    'iss' => 'https://meusite.com',      // Emissor
    'sub' => '123',                       // ID do utilizador
    'iat' => time(),                      // Criado em
    'exp' => time() + 3600,               // Expira em 1 hora
    'name' => 'João Silva',
    'email' => 'joao@exemplo.com'
];

$jwt = JWT::encode($payload, $key, 'HS256');
echo $jwt;
?>

🛡️ Boas Práticas de Segurança

✅ Fazer

  • ✓ Usar variáveis de ambiente
  • ✓ Sempre usar HTTPS
  • ✓ Guardar tokens de forma segura
  • ✓ Implementar refresh tokens
  • ✓ Validar tokens no servidor
  • ✓ Usar expiração curta em tokens

❌ Evitar

  • ✗ Hardcoded credentials no código
  • ✗ Commits de .env para Git
  • ✗ HTTP sem SSL
  • ✗ Guardar tokens em localStorage
  • ✗ Tokens sem expiração
  • ✗ Ignorar validação de tokens

Variáveis de Ambiente

.env
# .env
API_KEY=sk_live_abc123xyz
API_SECRET=secret_456
DATABASE_URL=mysql://user:pass@localhost/db
Ler variáveis de ambiente
# Instalar vlucas/phpdotenv
composer require vlucas/phpdotenv
<?php
require 'vendor/autoload.php';

$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();

// Usar as variáveis
$apiKey = $_ENV['API_KEY'];
$apiSecret = $_ENV['API_SECRET'];

// Ou com getenv()
$apiKey = getenv('API_KEY');
?>

.gitignore

.gitignore
# Nunca commitar estes ficheiros!
.env
.env.local
.env.production
*.pem
*.key
credentials.json
💡 Dica de Segurança

Usa diferentes API keys para desenvolvimento e produção. Nunca uses uma chave de produção em ambiente de desenvolvimento!

📝 Exercícios

▼

Exercício 1: API Key

Desafio: Cria um cliente Guzzle que usa API Key no header para autenticação.

<?php
use GuzzleHttp\Client;

$client = new Client([
    'base_uri' => 'https://api.exemplo.com',
    'headers' => [
        'X-API-Key' => getenv('API_KEY'),
        'Accept' => 'application/json'
    ]
]);

$response = $client->get('/users');
?>
▼

Exercício 2: Bearer Token

Desafio: Faz um pedido autenticado com Bearer token obtido de uma variável de ambiente.

<?php
use GuzzleHttp\Client;

$token = getenv('ACCESS_TOKEN');

$client = new Client(['base_uri' => 'https://api.exemplo.com']);

$response = $client->get('/protected', [
    'headers' => [
        'Authorization' => 'Bearer ' . $token
    ]
]);

$data = json_decode($response->getBody(), true);
?>
▼

Exercício 3: Tratamento de Token Expirado

Desafio: Implementa tratamento quando o token expira (código 401) e tenta renovar.

<?php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;

$client = new Client(['base_uri' => 'https://api.exemplo.com']);
$token = getenv('ACCESS_TOKEN');

try {
    $response = $client->get('/resource', [
        'headers' => ['Authorization' => 'Bearer ' . $token]
    ]);
} catch (ClientException $e) {
    if ($e->getCode() === 401) {
        // Token expirado - tentar renovar
        $newToken = refreshToken();

        // Tentar novamente com novo token
        $response = $client->get('/resource', [
            'headers' => ['Authorization' => 'Bearer ' . $newToken]
        ]);
    } else {
        throw $e;
    }
}

function refreshToken() {
    $client = new Client(['base_uri' => 'https://auth.exemplo.com']);
    $response = $client->post('/oauth/token', [
        'form_params' => [
            'grant_type' => 'refresh_token',
            'refresh_token' => getenv('REFRESH_TOKEN')
        ]
    ]);
    $data = json_decode($response->getBody(), true);
    return $data['access_token'];
}
?>