Flow logowania (OAuth2 authorization_code + PKCE)
DealerID jest standardowym OAuth2 / OIDC serwerem (league/oauth2-server-bundle). Wszystkie
aplikacje platformy logują userów flowem authorization_code z PKCE (S256). Implicit i
password grant są wyłączone — nie używamy ich nigdzie.
Endpointy DealerID
Dział zatytułowany „Endpointy DealerID”| Endpoint | URL (dev) | Auth | Cel |
|---|---|---|---|
/authorize | http://localhost:8082/authorize | Sesja Symfony (cookie) | Front-channel. Tu user się loguje i (ew.) klika „Zezwól”. |
/token | http://localhost:8082/token | client_id + (secret/PKCE) | Back-channel. Wymiana code na access_token (+ opcjonalnie refresh_token). |
/userinfo | http://localhost:8082/userinfo | Bearer (scope=openid) | OIDC claims usera (sub, email, name, is_internal_staff). Patrz /userinfo. |
W docker-compose port DealerID nginx to
8082. W prod produkcjihttps://id.dealercrm.pl.
Sekwencja (authorization_code + PKCE)
Dział zatytułowany „Sekwencja (authorization_code + PKCE)”sequenceDiagram
autonumber
actor U as User<br/>(przeglądarka)
participant App as Aplikacja partnerska<br/>(np. BMS frontend / BFF)
participant DID as DealerID<br/>(/authorize, /token, /userinfo)
participant Provider as External IdP<br/>(opcjonalnie: Google / MS)
U->>App: GET / (brak sesji)
App->>App: generuj code_verifier + code_challenge<br/>+ state, ustaw w cookie (HttpOnly)
App-->>U: 302 → /authorize?<br/>response_type=code<br/>&client_id=bms-frontend<br/>&redirect_uri=...<br/>&scope=openid email profile<br/>&state=...<br/>&code_challenge=...<br/>&code_challenge_method=S256
U->>DID: GET /authorize?...
alt User nie ma sesji w DealerID
DID-->>U: 302 → /login
U->>DID: POST /login (form) lub /oauth/google/start
opt SSO przez Google/Microsoft
DID->>Provider: OIDC redirect
Provider-->>U: login UI
U->>Provider: credentials
Provider->>DID: /oauth/{provider}/callback?code=...
DID->>DID: SsoSessionAuthenticator<br/>(provisioning internal staff jeśli google +<br/>domena ∈ internal_domains)
end
DID->>DID: SetSessionCookie<br/>(symfony main firewall)
end
alt is_first_party (CRM, BMS, ...)
DID->>DID: AuthorizationRequestSubscriber<br/>auto-approve → wystaw code
else third-party client
DID-->>U: ekran zgody (/oauth/consent)
U->>DID: POST /oauth/consent (allow / deny)
end
DID-->>U: 302 → redirect_uri?code=...&state=...
U->>App: GET /auth/callback?code=...&state=...
App->>App: weryfikuj state == cookie.state<br/>(CSRF)
App->>DID: POST /token<br/>grant_type=authorization_code<br/>client_id, redirect_uri,<br/>code, code_verifier
DID->>DID: weryfikuj client + redirect_uri,<br/>SHA256(code_verifier) == code_challenge,<br/>code nieexpired & nie revoked
DID-->>App: 200 { access_token (JWT, RS256),<br/>refresh_token, expires_in, token_type }
App->>App: pakuj tokeny w encrypted session cookie<br/>(iron-session w BFF) lub w secure store
App-->>U: 302 → / (zalogowany)
Note over App,DID: Dalsze requesty: App → API → /userinfo,<br/>App → swój backend z Bearer JWT
Konfiguracja klienta (przykład z apps/crm-frontend/lib/auth/config.ts)
Dział zatytułowany „Konfiguracja klienta (przykład z apps/crm-frontend/lib/auth/config.ts)”export const authConfig = { sessionPassword: required("SESSION_PASSWORD"), sessionCookieName: "crm_session", pkceCookieName: "crm_pkce", frontendBaseUrl: required("FRONTEND_BASE_URL"), oauth: { authorizeUrl: required("OAUTH_AUTHORIZE_URL"), // np. http://localhost:8082/authorize tokenUrl: required("OAUTH_TOKEN_URL"), // np. http://dealerid-nginx/token (server-side, sieć dockera) clientId: required("OAUTH_CLIENT_ID"), // np. crm-frontend redirectUri: required("OAUTH_REDIRECT_URI"), // np. http://localhost:3001/auth/callback scopes: required("OAUTH_SCOPES").split(/\s+/), // np. "openid email profile" }, crmApiUrl: required("CRM_API_URL"),} as const;PKCE — co generujemy
Dział zatytułowany „PKCE — co generujemy”Z apps/crm-frontend/lib/auth/pkce.ts:
code_verifier: random 64 bajty, base64url-encoded, 86 znaków. Nigdy nie wysyłany w/authorize.code_challenge:base64url(SHA-256(code_verifier)).state: random 16 bajtów, base64url. CSRF token — verifier i state razem w jednym HttpOnly cookie do callbacka.
W /authorize jadą: code_challenge + code_challenge_method=S256 + state.
W /token jadą: code_verifier (raw) — DealerID weryfikuje SHA256 i porównuje.
Plain text PKCE jest wyłączony globalnie (allow_plain_text_pkce = FALSE w oauth2_client).
| Scope | Wymagany dla | Co odblokowuje |
|---|---|---|
openid | Każdy login OIDC | Dostęp do /userinfo. Bez tego scope, JwtAuthenticator w CRM nie woła /userinfo (oszczędność I/O). |
email | Większość use-case’ów | Claim email w /userinfo. |
profile | Większość use-case’ów | Claim name w /userinfo. |
app.directory.read | Tylko M2M token mintowany przez DealerID | Pozwala apce przyjąć call do /api/dealerid/user-apps. Nie używaj w SPA. Patrz App Directory. |
Co dostaje aplikacja po wymianie code → token
Dział zatytułowany „Co dostaje aplikacja po wymianie code → token”// POST /token response (200):{ "token_type": "Bearer", "expires_in": 3600, "access_token": "eyJ0eXAiOiJKV1Q...", // RS256 JWT "refresh_token": "def50200...", // opaque, OAuth2 server stores hash "scope": "openid email profile"}access_token to JWT podpisany RS256 publicznym kluczem DealerID. Claimy:
{ "iss": "https://id.dealercrm.pl", "sub": "lukasz.kopcza@grupadealer.pl", // EMAIL, nie UUID "aud": "bms-frontend", "iat": 1715900000, "nbf": 1715900000, "exp": 1715903600, "jti": "...", "scopes": ["openid", "email", "profile"]}Uwaga:
subto email, nie UUID usera w DealerID. Decyzja wynika z tego, że apki downstream provisionują userów po emailu (lazy create) i sam UUID DealerID nie jest im potrzebny — tabelaiam_userw CRM ma własnyid. Patrzapps/crm-api/src/Module/Iam/Infrastructure/Security/JwtAuthenticator.php.
Po stronie aplikacji — walidacja tokena
Dział zatytułowany „Po stronie aplikacji — walidacja tokena”Patrz przykład w CRM: apps/crm-api/src/Module/Iam/Infrastructure/Security/JwtAuthenticator.php.
Minimum:
- Parsuj JWT (lcobucci/jwt lub równoważne).
- Weryfikuj
SignedWith(RS256, publicKey)— publiczny klucz DealerID dostarczany jako envOAUTH_PUBLIC_KEY(PEM). - Weryfikuj
StrictValidAt(nbf/iat/exp z tolerancją 0s). - Wyciągnij
sub(email) iscopes. - Lazy provision lokalnego usera (jeśli pierwszy login).
- Mapuj role / permission na konwencje twojej apki.
Refresh token flow
Dział zatytułowany „Refresh token flow”refresh_token opaque, ma długi TTL (config DealerID, domyślnie tygodnie). Wymiana standardowa:
POST /tokengrant_type=refresh_token&refresh_token=def50200...&client_id=bms-frontendAplikacja powinna rotować refresh token przy każdym użyciu (league/oauth2-server-bundle to
domyślnie robi). Stary refresh token jest revoked po wystawieniu nowego.
Co nie jest obsługiwane
Dział zatytułowany „Co nie jest obsługiwane”- Implicit flow — wyłączony (
response_type=tokenzwróci błąd). - Resource owner password grant — niedostępny. Nie ma sposobu, by apka miała plaintext hasło usera.
- Device code flow — niezaimplementowany. Jeśli będzie potrzebny (TV, CLI), zgłoś do DealerID.
- Hybrid OIDC flow (
response_type=code id_token) — niezaimplementowany. Używamy „pure OAuth2 + /userinfo”.