175 lines
6.5 KiB
PHP
175 lines
6.5 KiB
PHP
<?php
|
|
|
|
use Firebase\JWT\JWT;
|
|
use App\Models\Player;
|
|
use App\Lottery\ErrorCode;
|
|
use App\Support\PlayerAuthSource;
|
|
use App\Support\PlayerFundingMode;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Database\Seeders\CurrencySeeder;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Database\Seeders\LotterySettingsSeeder;
|
|
use App\Services\Player\PlayerNativeAuthService;
|
|
use App\Exceptions\PlayerAuthenticationException;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function (): void {
|
|
config([
|
|
'lottery.player_auth.native.secret' => 'independent-native-secret-32bytes!!',
|
|
'lottery.player_auth.native.ttl_seconds' => 3600,
|
|
'lottery.main_site.sso_jwt_secret' => null,
|
|
'lottery.main_site.wallet_api_url' => null,
|
|
]);
|
|
|
|
$this->seed(CurrencySeeder::class);
|
|
$this->seed(LotterySettingsSeeder::class);
|
|
});
|
|
|
|
function nativeSecretGuardPlayer(string $suffix): Player
|
|
{
|
|
$site = DB::table('admin_sites')->where('is_default', true)->first();
|
|
$rootId = (int) DB::table('agent_nodes')->where('depth', 0)->value('id');
|
|
|
|
return Player::query()->create([
|
|
'site_code' => (string) $site->code,
|
|
'agent_node_id' => $rootId,
|
|
'site_player_id' => 'native:secret-guard-'.$suffix,
|
|
'auth_source' => PlayerAuthSource::LOTTERY_NATIVE,
|
|
'funding_mode' => PlayerFundingMode::CREDIT,
|
|
'username' => 'secret_guard_'.$suffix,
|
|
'password_hash' => Hash::make('secret-pass'),
|
|
'default_currency' => 'NPR',
|
|
'status' => 0,
|
|
]);
|
|
}
|
|
|
|
function nativeSecretGuardToken(Player $player, string $secret): string
|
|
{
|
|
$now = time();
|
|
|
|
return JWT::encode([
|
|
'player_id' => (int) $player->id,
|
|
'auth_source' => PlayerAuthSource::LOTTERY_NATIVE,
|
|
'token_version' => (int) ($player->native_token_version ?? 0),
|
|
'site_code' => (string) $player->site_code,
|
|
'iat' => $now,
|
|
'exp' => $now + 3600,
|
|
], $secret, 'HS256');
|
|
}
|
|
|
|
function expectNativeSecretConfigurationRejected(Closure $callback): void
|
|
{
|
|
try {
|
|
$callback();
|
|
test()->fail('Expected native JWT secret configuration to be rejected.');
|
|
} catch (PlayerAuthenticationException $exception) {
|
|
expect($exception->lotteryCode)->toBe(ErrorCode::PlayerSsoSecretNotConfigured->value)
|
|
->and($exception->httpStatus)->toBe(503);
|
|
}
|
|
}
|
|
|
|
test('missing native secret rejects token issuance and verification with 503', function (): void {
|
|
$player = nativeSecretGuardPlayer('missing');
|
|
config(['lottery.player_auth.native.secret' => null]);
|
|
|
|
expectNativeSecretConfigurationRejected(
|
|
fn () => app(PlayerNativeAuthService::class)->issueToken($player),
|
|
);
|
|
|
|
$token = nativeSecretGuardToken($player, 'previous-native-secret-32bytes!!');
|
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
|
->getJson('/api/v1/player/me')
|
|
->assertStatus(503)
|
|
->assertJsonPath('code', ErrorCode::PlayerSsoSecretNotConfigured->value);
|
|
});
|
|
|
|
test('native secret matching legacy sso secret rejects token issuance and verification', function (): void {
|
|
$player = nativeSecretGuardPlayer('legacy-match');
|
|
$sharedSecret = 'shared-legacy-native-secret-32bytes!!';
|
|
config([
|
|
'lottery.player_auth.native.secret' => $sharedSecret,
|
|
'lottery.main_site.sso_jwt_secret' => $sharedSecret,
|
|
]);
|
|
|
|
expectNativeSecretConfigurationRejected(
|
|
fn () => app(PlayerNativeAuthService::class)->issueToken($player),
|
|
);
|
|
|
|
$token = nativeSecretGuardToken($player, $sharedSecret);
|
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
|
->getJson('/api/v1/player/me')
|
|
->assertStatus(503)
|
|
->assertJsonPath('code', ErrorCode::PlayerSsoSecretNotConfigured->value);
|
|
});
|
|
|
|
test('native secret matching enabled database site sso secret rejects issuance and verification', function (): void {
|
|
$player = nativeSecretGuardPlayer('database-match');
|
|
$sharedSecret = 'shared-database-native-secret-32bytes!!';
|
|
config([
|
|
'lottery.player_auth.native.secret' => $sharedSecret,
|
|
'lottery.main_site.sso_jwt_secret' => 'different-legacy-sso-secret-32bytes!!',
|
|
]);
|
|
DB::table('admin_sites')->where('is_default', true)->update([
|
|
'status' => 1,
|
|
'sso_jwt_secret_encrypted' => encrypt($sharedSecret),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
expectNativeSecretConfigurationRejected(
|
|
fn () => app(PlayerNativeAuthService::class)->issueToken($player),
|
|
);
|
|
|
|
$token = nativeSecretGuardToken($player, $sharedSecret);
|
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
|
->getJson('/api/v1/player/me')
|
|
->assertStatus(503)
|
|
->assertJsonPath('code', ErrorCode::PlayerSsoSecretNotConfigured->value);
|
|
});
|
|
|
|
test('native secret matching disabled database site sso secret rejects issuance and verification', function (): void {
|
|
$player = nativeSecretGuardPlayer('disabled-database-match');
|
|
$sharedSecret = 'shared-disabled-site-secret-32bytes!!';
|
|
config([
|
|
'lottery.player_auth.native.secret' => $sharedSecret,
|
|
'lottery.main_site.sso_jwt_secret' => 'different-legacy-sso-secret-32bytes!!',
|
|
]);
|
|
DB::table('admin_sites')->where('is_default', true)->update([
|
|
'status' => 0,
|
|
'sso_jwt_secret_encrypted' => encrypt($sharedSecret),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
expectNativeSecretConfigurationRejected(
|
|
fn () => app(PlayerNativeAuthService::class)->issueToken($player),
|
|
);
|
|
|
|
$token = nativeSecretGuardToken($player, $sharedSecret);
|
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
|
->getJson('/api/v1/player/me')
|
|
->assertStatus(503)
|
|
->assertJsonPath('code', ErrorCode::PlayerSsoSecretNotConfigured->value);
|
|
});
|
|
|
|
test('independent native secret still issues and verifies tokens', function (): void {
|
|
$player = nativeSecretGuardPlayer('independent');
|
|
config([
|
|
'lottery.player_auth.native.secret' => 'independent-native-secret-32bytes!!',
|
|
'lottery.main_site.sso_jwt_secret' => 'different-legacy-sso-secret-32bytes!!',
|
|
]);
|
|
DB::table('admin_sites')->where('is_default', true)->update([
|
|
'status' => 1,
|
|
'sso_jwt_secret_encrypted' => encrypt('different-database-sso-secret-32bytes!!'),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$token = app(PlayerNativeAuthService::class)->issueToken($player);
|
|
|
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
|
->getJson('/api/v1/player/me')
|
|
->assertOk()
|
|
->assertJsonPath('data.id', $player->id)
|
|
->assertJsonPath('data.auth_source', PlayerAuthSource::LOTTERY_NATIVE);
|
|
});
|