funcional a parte de token

This commit is contained in:
2026-05-19 16:24:51 -05:00
parent cd38287503
commit edc6e6486b
6 changed files with 32 additions and 41 deletions

View File

@@ -54,11 +54,10 @@ VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
JWT_ISSUER= JWT_ISSUER=https://sistema-distribuido-trabalho-faculd.vercel.app
JWT_AUDIENCE= JWT_AUDIENCE=internal-apis
JWT_PUBLIC_KEY_PEM= JWT_PUBLIC_KEY_PEM="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAEAXf0r0pF9jsaUb+T5ue\nm/6lJby0lLqNstP4bpXg4izqzVl8ad9gM/mOS+1M8U204/CMBowC2XFVKQITI78y\n3o1KrlyUpmTfZrrDHidCII53v3E/N6Vou4hEV5xLQhuE61sXB4bwDpr+JgAq17IV\nTfUR+ePFY6xmPCimTuGTXNPOJprkYlV1jEYzMHvtk6FSV39eZDp2GM3wnGYk95ib\n5fFd+xRB8kUdrtub5Cif/ayyF2vsmgsjN41d2qOw6MFsNsXsOXVcrCE/0GvvW5C8\nRTPCifEPbHJ/Du7ye1yDjHDyjYXnnoZ3cOg6VIa12OnlBRfL6sJBT6VCvIbzQN1z\n7QIDAQAB\n-----END PUBLIC KEY-----"
JWT_TOKEN= JWT_TOKEN=
JWT_ALLOW_ANY_TOKEN=false
# Railway production example: # Railway production example:
# APP_ENV=production # APP_ENV=production

View File

@@ -142,10 +142,9 @@ Principais variáveis usadas pelo projeto:
| `APP_DEBUG` | Ativa ou desativa debug | | `APP_DEBUG` | Ativa ou desativa debug |
| `APP_URL` | URL base da aplicação | | `APP_URL` | URL base da aplicação |
| `DB_CONNECTION` | Driver do banco, atualmente `sqlite` | | `DB_CONNECTION` | Driver do banco, atualmente `sqlite` |
| `JWT_ISSUER` | Emissor esperado no token JWT | | `JWT_ISSUER` | Emissor esperado no token JWT: `https://sistema-distribuido-trabalho-faculd.vercel.app` |
| `JWT_AUDIENCE` | Audiência esperada no token JWT | | `JWT_AUDIENCE` | Audiência esperada no token JWT: `internal-apis` |
| `JWT_PUBLIC_KEY_PEM` | Chave pública usada para validar JWT RS256 | | `JWT_PUBLIC_KEY_PEM` | Chave pública usada para validar JWT RS256 |
| `JWT_ALLOW_ANY_TOKEN` | Quando `true`, aceita qualquer Bearer token. Use apenas para integração/demo |
| `SCRIBE_AUTH_KEY` | Token usado apenas para gerar exemplos 200 na documentação | | `SCRIBE_AUTH_KEY` | Token usado apenas para gerar exemplos 200 na documentação |
--- ---
@@ -172,14 +171,6 @@ O token precisa:
Sem o header `Authorization`, a API retorna `401`. Sem o header `Authorization`, a API retorna `401`.
Para ambientes de demonstração ou integração inicial com o frontend, é possível configurar:
```env
JWT_ALLOW_ANY_TOKEN=true
```
Nesse modo, a API aceita qualquer valor enviado como `Bearer token`. Para produção, mantenha `JWT_ALLOW_ANY_TOKEN=false` e use a validação JWT completa.
--- ---
## Executando o Projeto ## Executando o Projeto

View File

@@ -22,15 +22,6 @@ class JwtAuthMiddleware
$token = $matches[1]; $token = $matches[1];
if (config('jwt.allow_any_token')) {
$request->attributes->set('auth', [
'id' => $this->subjectFromUnverifiedToken($token),
'token' => $token
]);
return $next($request);
}
[$header, $payload, $signature] = $this->decodeToken($token); [$header, $payload, $signature] = $this->decodeToken($token);
if (($header['alg'] ?? null) !== 'RS256') { if (($header['alg'] ?? null) !== 'RS256') {
@@ -40,7 +31,7 @@ class JwtAuthMiddleware
if ( if (
!$this->signatureIsValid($token, $signature) || !$this->signatureIsValid($token, $signature) ||
($payload['iss'] ?? null) !== config('jwt.issuer') || ($payload['iss'] ?? null) !== config('jwt.issuer') ||
($payload['aud'] ?? null) !== config('jwt.audience') || !$this->audienceIsValid($payload['aud'] ?? null) ||
empty($payload['sub']) || empty($payload['sub']) ||
$this->tokenIsExpired($payload) $this->tokenIsExpired($payload)
) { ) {
@@ -123,19 +114,19 @@ class JwtAuthMiddleware
return time() >= (int) $payload['exp']; return time() >= (int) $payload['exp'];
} }
private function subjectFromUnverifiedToken(string $token): string private function audienceIsValid(mixed $audience): bool
{ {
$parts = explode('.', $token); $expectedAudience = config('jwt.audience');
if (count($parts) !== 3) { if (is_string($audience)) {
return 'external-consumer'; return $audience === $expectedAudience;
} }
try { if (is_array($audience)) {
$payload = $this->base64UrlDecodeJson($parts[1]); return in_array($expectedAudience, $audience, true);
return (string) ($payload['sub'] ?? 'external-consumer');
} catch (\Exception $e) {
return 'external-consumer';
} }
return false;
} }
} }

View File

@@ -4,5 +4,4 @@ return [
'issuer' => env('JWT_ISSUER'), 'issuer' => env('JWT_ISSUER'),
'audience' => env('JWT_AUDIENCE'), 'audience' => env('JWT_AUDIENCE'),
'public_key' => env('JWT_PUBLIC_KEY_PEM'), 'public_key' => env('JWT_PUBLIC_KEY_PEM'),
'allow_any_token' => env('JWT_ALLOW_ANY_TOKEN', false),
]; ];

View File

@@ -17,6 +17,8 @@ Route::get('/', function () {
return view('welcome'); return view('welcome');
}); });
Route::redirect('/games/most-played', '/api/v1/games/most-played');
Route::get('/health', function () { Route::get('/health', function () {
return response('ok', 200); return response('ok', 200);
}); });

View File

@@ -11,6 +11,7 @@ class GameRankingApiTest extends TestCase
use RefreshDatabase; use RefreshDatabase;
private string $jwt; private string $jwt;
private string $privateKey;
protected function setUp(): void protected function setUp(): void
{ {
@@ -101,16 +102,23 @@ class GameRankingApiTest extends TestCase
->assertJson(['userId' => 'consumer-project']); ->assertJson(['userId' => 'consumer-project']);
} }
public function test_can_accept_any_bearer_token_when_enabled_for_demo_integration(): void public function test_accepts_token_with_audience_array_containing_expected_audience(): void
{ {
config(['jwt.allow_any_token' => true]); $this->jwt = $this->makeJwt($this->privateKey, ['other-api', 'ranking-api']);
$this->withHeader('Authorization', 'Bearer token-do-front') $this->getJsonWithJwt('/api/v1/games/most-played')
->getJson('/api/v1/rankings/weekly')
->assertOk() ->assertOk()
->assertJsonCount(10); ->assertJsonCount(10);
} }
public function test_rejects_generic_bearer_token(): void
{
$this->withHeader('Authorization', 'Bearer token-do-front')
->getJson('/api/v1/rankings/weekly')
->assertUnauthorized()
->assertJson(['message' => 'Invalid or expired token']);
}
private function getJsonWithJwt(string $uri) private function getJsonWithJwt(string $uri)
{ {
return $this->withHeader('Authorization', 'Bearer '.$this->jwt) return $this->withHeader('Authorization', 'Bearer '.$this->jwt)
@@ -125,6 +133,7 @@ class GameRankingApiTest extends TestCase
]); ]);
openssl_pkey_export($key, $privateKey); openssl_pkey_export($key, $privateKey);
$this->privateKey = $privateKey;
$publicKey = openssl_pkey_get_details($key)['key']; $publicKey = openssl_pkey_get_details($key)['key'];
config([ config([
@@ -136,14 +145,14 @@ class GameRankingApiTest extends TestCase
$this->jwt = $this->makeJwt($privateKey); $this->jwt = $this->makeJwt($privateKey);
} }
private function makeJwt(string $privateKey): string private function makeJwt(string $privateKey, string|array $audience = 'ranking-api'): string
{ {
$encode = fn (string $value): string => rtrim(strtr(base64_encode($value), '+/', '-_'), '='); $encode = fn (string $value): string => rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
$header = $encode(json_encode(['alg' => 'RS256', 'typ' => 'JWT'])); $header = $encode(json_encode(['alg' => 'RS256', 'typ' => 'JWT']));
$payload = $encode(json_encode([ $payload = $encode(json_encode([
'iss' => 'gameverse-auth', 'iss' => 'gameverse-auth',
'aud' => 'ranking-api', 'aud' => $audience,
'sub' => 'consumer-project', 'sub' => 'consumer-project',
'iat' => time(), 'iat' => time(),
'exp' => time() + 3600, 'exp' => time() + 3600,