create(); $app = Application::factory()->create(); // Store authorization code $code = 'test-auth-code'; Cache::put("auth_code:$code", [ 'user_id' => $user->id, 'client_id' => $app->id, 'scope' => 'openid profile email', ], now()->addMinutes(5)); $this->actingAs($user); $response = $this->post(route('auth.token'), [ 'grant_type' => 'authorization_code', 'code' => $code, 'redirect_uri' => $app->redirect_uri, 'client_id' => $app->client_id, 'client_secret' => $app->client_secret, ]); $response->assertStatus(200); $data = $response->json(); // Parse the JWT ID token $parser = new Parser(new JoseEncoder()); $idToken = $parser->parse($data['id_token']); // Verify that the 'sub' claim contains the user's UUID, not the ID expect($idToken->claims()->get('sub'))->toBe((string) $user->uuid); expect($idToken->claims()->get('sub'))->not->toBe((string) $user->id); expect(Str::isUuid($idToken->claims()->get('sub')))->toBeTrue(); }); test('userinfo endpoint sub claim uses user UUID', function () { $user = User::factory()->create([ 'name' => 'Test User', 'email' => 'test@example.com', 'preferred_username' => 'testuser', 'avatar' => 'avatar.jpg' ]); $app = Application::factory()->create(); // Create a valid access token $accessToken = 'valid-access-token'; AuthenticationToken::create([ 'user_id' => $user->id, 'application_id' => $app->id, 'token' => $accessToken, 'issued_at' => now(), 'expires_at' => now()->addHour(), 'ip' => '127.0.0.1', 'user_agent' => 'Test Agent' ]); $response = $this->get(route('auth.userinfo'), [ 'Authorization' => 'Bearer ' . $accessToken ]); $response->assertStatus(200); $data = $response->json(); // Verify that the 'sub' field contains the user's UUID, not the ID expect($data['sub'])->toBe((string) $user->uuid); expect($data['sub'])->not->toBe((string) $user->id); expect(Str::isUuid($data['sub']))->toBeTrue(); // Verify other fields are still correct expect($data['email'])->toBe($user->email); expect($data['name'])->toBe($user->name); expect($data['preferred_username'])->toBe($user->preferred_username); }); test('JWT sub claim is consistent across multiple tokens for same user', function () { $user = User::factory()->create(); $app = Application::factory()->create(); // Generate first token $code1 = 'test-auth-code-1'; Cache::put("auth_code:$code1", [ 'user_id' => $user->id, 'client_id' => $app->id, 'scope' => 'openid profile email', ], now()->addMinutes(5)); $response1 = $this->post(route('auth.token'), [ 'grant_type' => 'authorization_code', 'code' => $code1, 'redirect_uri' => $app->redirect_uri, 'client_id' => $app->client_id, 'client_secret' => $app->client_secret, ]); // Generate second token $code2 = 'test-auth-code-2'; Cache::put("auth_code:$code2", [ 'user_id' => $user->id, 'client_id' => $app->id, 'scope' => 'openid profile email', ], now()->addMinutes(5)); $response2 = $this->post(route('auth.token'), [ 'grant_type' => 'authorization_code', 'code' => $code2, 'redirect_uri' => $app->redirect_uri, 'client_id' => $app->client_id, 'client_secret' => $app->client_secret, ]); $parser = new Parser(new JoseEncoder()); $idToken1 = $parser->parse($response1->json()['id_token']); $idToken2 = $parser->parse($response2->json()['id_token']); // Both tokens should have the same 'sub' claim (user's UUID) expect($idToken1->claims()->get('sub'))->toBe($idToken2->claims()->get('sub')); expect($idToken1->claims()->get('sub'))->toBe((string) $user->uuid); }); test('different users have different UUID sub claims', function () { $user1 = User::factory()->create(); $user2 = User::factory()->create(); $app = Application::factory()->create(); // Generate token for user 1 $code1 = 'test-auth-code-1'; Cache::put("auth_code:$code1", [ 'user_id' => $user1->id, 'client_id' => $app->id, 'scope' => 'openid profile email', ], now()->addMinutes(5)); $response1 = $this->post(route('auth.token'), [ 'grant_type' => 'authorization_code', 'code' => $code1, 'redirect_uri' => $app->redirect_uri, 'client_id' => $app->client_id, 'client_secret' => $app->client_secret, ]); // Generate token for user 2 $code2 = 'test-auth-code-2'; Cache::put("auth_code:$code2", [ 'user_id' => $user2->id, 'client_id' => $app->id, 'scope' => 'openid profile email', ], now()->addMinutes(5)); $response2 = $this->post(route('auth.token'), [ 'grant_type' => 'authorization_code', 'code' => $code2, 'redirect_uri' => $app->redirect_uri, 'client_id' => $app->client_id, 'client_secret' => $app->client_secret, ]); $parser = new Parser(new JoseEncoder()); $idToken1 = $parser->parse($response1->json()['id_token']); $idToken2 = $parser->parse($response2->json()['id_token']); // Each user should have different 'sub' claims expect($idToken1->claims()->get('sub'))->not->toBe($idToken2->claims()->get('sub')); expect($idToken1->claims()->get('sub'))->toBe((string) $user1->uuid); expect($idToken2->claims()->get('sub'))->toBe((string) $user2->uuid); });