Przejdź do głównej zawartości

Wyszukiwanie jest dostępne tylko w buildach produkcyjnych. Spróbuj zbudować i uruchomić aplikację, aby przetestować lokalnie.

Endpoint /userinfo

GET /userinfo to standardowy OIDC userinfo endpoint, eksponowany przez DealerID. Jego rola: po tym, jak twoja aplikacja dostała access_token z /token, woła /userinfo żeby pobrać podstawowe claimy usera (sub, email, name) i — istotne dla naszej platformy — flagę is_internal_staff.

GET /userinfo
Host: id.dealercrm.pl
Authorization: Bearer <access_token>
Accept: application/json

Wymagania:

  • Bearer token musi mieć scope openid (lub szerszy nadrzędny). Bez openid, /userinfo zwróci 401.
  • Token musi być ważny (sig RS256, nbf/exp w zakresie).
  • Token musi pochodzić z DealerID (sprawdzane na poziomie firewalla — patrz apps/dealerid/config/packages/security.yaml firewall userinfo).
// HTTP/1.1 200 OK
// Content-Type: application/json
{
"sub": "0192ab34-5678-7abc-9def-0123456789ab",
"email": "lukasz.kopcza@grupadealer.pl",
"name": "Łukasz Kopcza",
"is_internal_staff": true
}
PoleTypOpis
substringUUID v7 usera w bazie DealerID (app_user.id). Uwaga: w samym access JWT sub to email — to jest historyczna niespójność; tu sub to UUID.
emailstringAktualny email usera, lowercase.
namestringDisplay name (z form rejestracji lub z Google/MS resource owner).
is_internal_staffbooltrue ⇒ user jest pracownikiem Grupy Dealer (auto-provisioning po SSO Google z @grupadealer.pl). Patrz SSO Google + auto-provisioning.

Uwaga o scope: standardowy access_token wystawiany w flow OAuth2 ma scope openid email profile i daje dostęp do /userinfo. Osobno DealerID mintuje krótkie JWT (server-to-server, 5 min TTL) z scope app.directory.read — tych tokenów nie używaj do callu /userinfo (nie mają openid, dostaniesz 401). Patrz App Directory — kontrakt.

Flaga ustawiana w app_user.is_internal_staff (migracja Version20260518150000). Wartość TRUE oznacza, że user jest pracownikiem Grupy Dealer. Konsekwencje:

  • W DealerID: nie może logować się hasłem ani Microsoft Entra ID. Wyłącznie Google Workspace. Patrz apps/dealerid/src/Module/User/Infrastructure/Security/InternalStaffPasswordLoginBlocker.php i InternalStaffMustUseGoogle w OidcController.
  • W downstream apkach (CRM, BMS, …): claim is_internal_staff w /userinfo służy do auto-grantowania flag staff/admin w lokalnej tabeli membership. Patrz CRM: apps/crm-api/src/Module/Iam/Application/Command/ProvisionUserIfMissing.php (lazy provision
    • ustawienie iam_user.is_staff = TRUE).

Trust boundary: apki ufają DealerID w temacie is_internal_staff. Bez DealerID nie ma sposobu, by pracownik Grupy Dealer „udowodnił” swój status — to jest fundament zaufania platformy.

Kontroler: apps/dealerid/src/Module/User/Ui/Http/UserInfoController.php.

#[Route(path: '/userinfo', name: 'userinfo', methods: ['GET'])]
public function __invoke(): JsonResponse
{
$securityUser = $this->security->getUser();
// Security layer już wymusiła ROLE_OAUTH2_OPENID — tu mamy zaufanego usera.
\assert(null !== $securityUser);
$email = $securityUser->getUserIdentifier();
$view = $this->userFinder->findByEmail($email);
\assert(null !== $view);
return new JsonResponse([
'sub' => $view->id,
'email' => $view->email,
'name' => $view->name,
'is_internal_staff' => $view->isInternalStaff,
]);
}

Firewall (z security.yaml):

userinfo:
pattern: ^/userinfo$
stateless: true
provider: dealerid_users
oauth2: true
access_control:
- { path: ^/userinfo$, roles: ROLE_OAUTH2_OPENID }
Okno terminala
# 1. Dostań access_token (zakładamy, że masz code z /authorize):
ACCESS_TOKEN=$(curl -sS -X POST http://localhost:8082/token \
-d grant_type=authorization_code \
-d client_id=crm-frontend \
-d redirect_uri=http://localhost:3001/auth/callback \
-d code="$CODE" \
-d code_verifier="$VERIFIER" \
| jq -r .access_token)
# 2. Zawołaj /userinfo:
curl -sS http://localhost:8082/userinfo \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Accept: application/json" | jq
// Spodziewany output:
{
"sub": "0192ab34-...",
"email": "lukasz.kopcza@grupadealer.pl",
"name": "Łukasz Kopcza",
"is_internal_staff": true
}

Patrz przykład CRM JwtAuthenticator::fetchIsInternalStaff():

private function fetchIsInternalStaff(string $bearerToken): ?bool
{
try {
$response = $this->httpClient->request('GET', $this->userinfoUrl, [
'headers' => [
'Authorization' => 'Bearer ' . $bearerToken,
'Accept' => 'application/json',
],
'timeout' => 2.0,
]);
if (200 !== $response->getStatusCode()) {
return null;
}
$data = $response->toArray(throw: false);
$value = $data['is_internal_staff'] ?? null;
return \is_bool($value) ? $value : null;
} catch (HttpClientException) {
return null;
}
}
  1. Wołaj /userinfo tylko przy provisioning (pierwszy login danego usera) lub przy świadomej resync (np. cron raz na dzień). Nie wołaj na każdy request — to dodatkowy hop network.
  2. Toleruj timeout (2 s wystarczy). Gdy DealerID jest down, twoja apka musi pozwolić userowi się zalogować — bo Bearer JWT już jest zwalidowany lokalnie. is_internal_staff traktuj jako „nice to have, fallback to last known value”.
  3. Skip dla tokenów bez openid — np. M2M token mintowany przez DealerID dla app.directory.read nie ma openid w scopes, więc /userinfo zwróci 401 i niepotrzebnie marnujesz timeout. Sprawdź in_array('openid', $scopes, true) przed callem.