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.
Endpoint
Dział zatytułowany „Endpoint”GET /userinfoHost: id.dealercrm.plAuthorization: Bearer <access_token>Accept: application/jsonWymagania:
- Bearer token musi mieć scope
openid(lub szerszy nadrzędny). Bezopenid,/userinfozwróci401. - 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.yamlfirewalluserinfo).
Response
Dział zatytułowany „Response”// 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}| Pole | Typ | Opis |
|---|---|---|
sub | string | UUID 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. |
email | string | Aktualny email usera, lowercase. |
name | string | Display name (z form rejestracji lub z Google/MS resource owner). |
is_internal_staff | bool | true ⇒ 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 profilei daje dostęp do/userinfo. Osobno DealerID mintuje krótkie JWT (server-to-server, 5 min TTL) z scopeapp.directory.read— tych tokenów nie używaj do callu/userinfo(nie mająopenid, dostaniesz 401). Patrz App Directory — kontrakt.
Co znaczy is_internal_staff
Dział zatytułowany „Co znaczy is_internal_staff”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.phpiInternalStaffMustUseGooglewOidcController. - W downstream apkach (CRM, BMS, …): claim
is_internal_staffw/userinfosł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).
- ustawienie
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.
Implementacja w DealerID
Dział zatytułowany „Implementacja w DealerID”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 }Przykład curl
Dział zatytułowany „Przykład curl”# 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}Obsługa po stronie aplikacji partnerskiej
Dział zatytułowany „Obsługa po stronie aplikacji partnerskiej”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; }}Zasady dobrego wykorzystania
Dział zatytułowany „Zasady dobrego wykorzystania”- Wołaj
/userinfotylko 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. - 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_stafftraktuj jako „nice to have, fallback to last known value”. - Skip dla tokenów bez
openid— np. M2M token mintowany przez DealerID dlaapp.directory.readnie maopenidwscopes, więc/userinfozwróci 401 i niepotrzebnie marnujesz timeout. Sprawdźin_array('openid', $scopes, true)przed callem.