feat: identify the authenticated account in the userinfo claims

This commit is contained in:
2026-09-19 10:54:06 +02:00
parent 2cb4871268
commit ec83ad598f
5 changed files with 241 additions and 5 deletions
+15 -1
View File
@@ -61,6 +61,13 @@ class PersonalData
/** @var list<string> */
public array $hotelCodes = [];
// The authenticated account, not BusPro's idea of this person. One BusPro person may sign in
// under any of the addresses on its record, and each of those is a separate local account with
// its own roles, so neither the BusPro ids nor the communication email identifies one. Patched
// on by the caller in the same way as the roles and hotel codes above.
public ?string $subject = null;
public ?string $loginEmail = null;
public function __construct()
{
$this->address = new Address();
@@ -132,14 +139,21 @@ class PersonalData
* Creates a structured array containing user profile information
* suitable for JWT claims or user session data.
*
* `sub` and `email` describe the account that authenticated and are read from the patched-on
* fields, never from BusPro: `person_id`/`address_id` are shared by every account of the same
* person, and `profile.communication.email` is the first contact address on the BusPro record,
* which is not necessarily the one signed in with. Deliberately without a fallback to that
* address — a caller that forgets to patch them gets null rather than a wrong identity.
*
* @return array<string, mixed> The claims array with user profile data
*/
public function getClaims(): array
{
return [
'sub' => $this->subject,
'person_id' => $this->personId,
'address_id' => $this->addressId,
'email' => $this->communication->email,
'email' => $this->loginEmail,
'roles' => $this->roles,
'profile' => [
'first_name' => $this->firstName,
+10 -2
View File
@@ -26,8 +26,9 @@ class UserinfoController extends AbstractController
#[Route('/userinfo', name: 'api_userinfo', methods: ['GET'])]
public function index(): JsonResponse
{
// basic scopes applicable to all authenticated users
$scopes = ['email'];
// basic scopes applicable to all authenticated users. `sub` is not gated on a scope of its
// own: it identifies the account every other claim describes, so it is always exported.
$scopes = ['sub', 'email'];
// extend scopes depending on granted permissions
if ($this->isGranted('ROLE_OAUTH2_ID')) {
@@ -53,6 +54,13 @@ class UserinfoController extends AbstractController
return new JsonResponse(['message' => $data->message, 'code' => $data->code], Response::HTTP_BAD_REQUEST);
}
// Patch the identity of the account that authenticated. BusPro accepts any of the
// addresses on a person's record as a login and answers all of them with the same
// ids and the same first contact address, so only the local account tells the staff
// account and the private one apart — and they hold different roles.
$data->subject = (string) $user->getId();
$data->loginEmail = $user->getEmail();
// Patch current user's roles. The implicit ROLE_USER says nothing about the
// account — every authenticated user holds it — and is not exported.
$data->roles = Role::effectiveOnly($user->getRoles());