funcional a parte de token
This commit is contained in:
@@ -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
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user