Rejestracja aplikacji partnerskiej (klient OAuth2) w DealerID
Żeby nowa aplikacja platformy (np. BMS, Service, Analytics) mogła logować userów przez DealerID,
musi być wpisana jako klient OAuth2 do tabeli oauth2_client w bazie DealerID. Na chwilę
obecną jest to ręczna operacja SQL (migracja Doctrine lub UPDATE po stronie DBA). W przyszłości
planowany jest admin UI w DealerID dla staff, na razie zostajemy przy DB-as-source-of-truth.
Uwaga terminologiczna: w tym dokumencie „aplikacja partnerska” / „klient OAuth2” oznacza zewnętrzną apkę (z perspektywy DealerID) chcącą integrować się przez OAuth2 — np. nowy moduł BMS, Service, integracja third-party. To NIE to samo co „Application” w sensie modelu wewnętrznego CRM (instancja CRM pod konkretnym hostem, rekord w tabeli
application). Jedna aplikacja partnerska (oauth2_client.identifier = 'crm-frontend') wystawia wiele wewnętrznych Applications (host-based). Patrz Multi-tenancy: Aplikacje.
Tabela oauth2_client
Dział zatytułowany „Tabela oauth2_client”Schema (z apps/dealerid/migrations/Version20260512142135.php
Version20260518100000.php+Version20260520120000.php):
| Kolumna | Typ | Opis |
|---|---|---|
identifier | VARCHAR(32) | PK. Krótka stabilna nazwa klienta, np. crm-frontend, bms-frontend. Używana w logach, w mapie APP_PRESENTATION (logo/kolor karty na DealerID) i jako client_id w OAuth. |
name | VARCHAR(128) | Human-readable nazwa pokazywana na ekranie zgody, np. „DealerCRM Frontend”. |
secret | VARCHAR(128) | Bcrypt/argon hash client_secret. Klienci public (SPA / Next.js BFF z PKCE) mogą mieć NULL jeśli confidential = false — patrz konfiguracja league/oauth2-server-bundle. Confidential klienci (machine-to-machine) muszą mieć secret. |
redirect_uris | TEXT | Separowane białymi znakami. Whitelist callbacków po loginie. W produkcji wyłącznie HTTPS. W dev: http://<app>.localhost:<port>/auth/callback. |
grants | TEXT | Lista grantów separowana białymi znakami. Standard dla naszych SPA / BFF: authorization_code refresh_token. Dla M2M można dodać client_credentials. |
scopes | TEXT | Lista scope’ów separowana białymi znakami. Patrz Flow OAuth2 → scopes. |
active | BOOLEAN | Hard kill switch dla klienta — FALSE zwalnia wszystkie tokeny przy następnej walidacji. |
allow_plain_text_pkce | BOOLEAN | Zawsze FALSE w naszej platformie. Tylko S256 (code_challenge_method=S256). |
is_first_party | BOOLEAN | Gdy TRUE, ekran zgody jest pomijany (auto-approve). Ustawiamy TRUE dla apek Grupy Dealer (CRM, BMS, Service, Analytics). FALSE dla integracji third-party (np. zewnętrznych systemów dealerskich). |
app_directory_url | VARCHAR(255) | Opcjonalny endpoint server-to-server (HTTP wewnątrz sieci Docker / VPN), z którego DealerID pobiera listę „kart Applications” dla zalogowanego usera. Patrz App Directory — kontrakt. Może być NULL. |
Uwaga: kolumny
is_first_partyiapp_directory_urlzostały dodane w późniejszych migracjach (Version20260518100000 i Version20260520120000). Jeśli czytasz to po świeżym wgraniu bazy bez tych migracji — najpierw odpalphp bin/console doctrine:migrations:migratewapps/dealerid.
Jak zarejestrować nową apkę (krok po kroku)
Dział zatytułowany „Jak zarejestrować nową apkę (krok po kroku)”1. Wybierz identifier i name
Dział zatytułowany „1. Wybierz identifier i name”Konwencja: <app>-<surface>. Surface to frontend (SPA / BFF konsumujący OAuth2), service
(M2M klient bez usera), cli, itd.
identifier = 'bms-frontend'name = 'DealerBMS'2. Wygeneruj client_secret
Dział zatytułowany „2. Wygeneruj client_secret”Public client (SPA + PKCE, bez secretu)? Można NULL. Confidential? Wygeneruj 64-bajtowy random:
openssl rand -base64 64Hashuj przez bundle (Symfony command — czytaj
vendor/league/oauth2-server-bundle/src/Command/CreateClientCommand.php — lub przez nasz własny
admin script gdy powstanie). W bazie trzymamy hash, nigdy plaintextu.
3. Ustal redirect_uris
Dział zatytułowany „3. Ustal redirect_uris”Dla SPA/BFF typowo jedno:
http://bms.localhost:3004/auth/callback # devhttps://bms.dealercrm.pl/auth/callback # prodWpisuj dokładnie taki URL, z jakim przyjdzie request — redirect_uri w /authorize musi byte-for-byte
pasować, inaczej OAuth2 server odrzuci.
4. Wybierz grants i scopes
Dział zatytułowany „4. Wybierz grants i scopes”Domyślne dla apek platformy:
grants = 'authorization_code refresh_token'scopes = 'openid email profile'Dodatkowe scope’y (np. app.directory.read dla M2M discovery) — patrz Flow OAuth2 → scopes.
5. Zdecyduj is_first_party
Dział zatytułowany „5. Zdecyduj is_first_party”| Apka | is_first_party | Powód |
|---|---|---|
| CRM, BMS, Service, Analytics | TRUE | Apki Grupy Dealer. User wie, że loguje się do platformy — ekran zgody to friction bez wartości. |
| Portal Klienta | TRUE | First-party z perspektywy platformy (user nie wybiera czy mu się zgadza). |
| External integracja (BIK, GUS, …) | FALSE | User powinien świadomie zgodzić się, że apka odczyta jego dane z DealerID. |
6. (Opcjonalnie) wpisz app_directory_url
Dział zatytułowany „6. (Opcjonalnie) wpisz app_directory_url”Jeśli twój klient OAuth2 chce się pojawić na dashboardzie /account/apps DealerID — wystaw endpoint
(patrz Implementacja) i wpisz jego URL z perspektywy sieci DealerID:
UPDATE oauth2_clientSET app_directory_url = 'http://bms-nginx/api/dealerid/user-apps'WHERE identifier = 'bms-frontend';W produkcji to będzie wewnętrzny URL VPN / mesh service, nie publiczny https://.
Co zwraca ten endpoint: listę wszystkich Applications (po stronie apki partnerskiej — instancji per host) widocznych dla danego usera. Apka mająca model multi-tenant (jak CRM) może wystawić N kart per user. Apka single-tenant — typowo 1 kartę. Patrz App Directory — kontrakt.
7. Wpis SQL (przykład)
Dział zatytułowany „7. Wpis SQL (przykład)”INSERT INTO oauth2_client ( identifier, name, secret, redirect_uris, grants, scopes, active, allow_plain_text_pkce, is_first_party, app_directory_url) VALUES ( 'bms-frontend', 'DealerBMS', NULL, -- public client (PKCE) 'http://bms.localhost:3004/auth/callback https://bms.dealercrm.pl/auth/callback', 'authorization_code refresh_token', 'openid email profile', TRUE, FALSE, TRUE, 'http://bms-nginx/api/dealerid/user-apps');8. Dodaj wpis w APP_PRESENTATION po stronie DealerID
Dział zatytułowany „8. Dodaj wpis w APP_PRESENTATION po stronie DealerID”Logo + kolor karty na /account/apps zdefiniowane są lokalnie w
apps/dealerid/src/Module/User/Infrastructure/Directory/HttpAppDirectoryFetcher.php:
private const array APP_PRESENTATION = [ 'crm-frontend' => ['name' => 'DealerCRM', 'monogram' => 'DC', 'color' => 'oklch(0.52 0.19 258)'], 'bms-frontend' => ['name' => 'DealerBMS', 'monogram' => 'BM', 'color' => 'oklch(0.45 0.15 60)'], 'service-frontend' => ['name' => 'DealerService', 'monogram' => 'DS', 'color' => 'oklch(0.55 0.10 25)'], 'analytics-frontend' => ['name' => 'DealerAnalytics', 'monogram' => 'DA', 'color' => 'oklch(0.5 0.2 295)'], 'portal-frontend' => ['name' => 'Portal Klienta', 'monogram' => 'PK', 'color' => 'oklch(0.5 0.15 175)'],];Dodanie nowej apki = PR do DealerID z dorzuceniem wpisu.
Dlaczego tak? Trzymamy spójność wizualną po stronie DealerID — klient OAuth2 mówi „co user może” (lista Applications dla usera), DealerID mówi „jak to pokazać”. Apka partnerska nie podpina własnego logo / koloru dla karty na DealerID. (Branding na poziomie konkretnej Application — typu
gezet-renault.localhost→ skin Renault — to inny wymiar i mieszka po stronie apki, worganization.branding_skin_slug. Patrz Multi-tenancy: Aplikacje.)
Roadmap
Dział zatytułowany „Roadmap”- ✅ DB-as-source-of-truth dla klientów OAuth2
- 🟡 Admin UI w DealerID (
/staff/oauth-clients) — CRUD klientów, generator secretów, validator URI - 🟡 Rotacja secretów (dual-secret window)
- 🟡 Self-service registration dla third-party (poza scope na razie)