Przejdź do głównej zawartości

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

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.

EndpointURL (dev)AuthCel
/authorizehttp://localhost:8082/authorizeSesja Symfony (cookie)Front-channel. Tu user się loguje i (ew.) klika „Zezwól”.
/tokenhttp://localhost:8082/tokenclient_id + (secret/PKCE)Back-channel. Wymiana code na access_token (+ opcjonalnie refresh_token).
/userinfohttp://localhost:8082/userinfoBearer (scope=openid)OIDC claims usera (sub, email, name, is_internal_staff). Patrz /userinfo.

W docker-compose port DealerID nginx to 8082. W prod produkcji https://id.dealercrm.pl.

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;

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).

ScopeWymagany dlaCo odblokowuje
openidKażdy login OIDCDostęp do /userinfo. Bez tego scope, JwtAuthenticator w CRM nie woła /userinfo (oszczędność I/O).
emailWiększość use-case’ówClaim email w /userinfo.
profileWiększość use-case’ówClaim name w /userinfo.
app.directory.readTylko M2M token mintowany przez DealerIDPozwala apce przyjąć call do /api/dealerid/user-apps. Nie używaj w SPA. Patrz App Directory.
// 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: sub to 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 — tabela iam_user w CRM ma własny id. Patrz apps/crm-api/src/Module/Iam/Infrastructure/Security/JwtAuthenticator.php.

Patrz przykład w CRM: apps/crm-api/src/Module/Iam/Infrastructure/Security/JwtAuthenticator.php. Minimum:

  1. Parsuj JWT (lcobucci/jwt lub równoważne).
  2. Weryfikuj SignedWith(RS256, publicKey) — publiczny klucz DealerID dostarczany jako env OAUTH_PUBLIC_KEY (PEM).
  3. Weryfikuj StrictValidAt (nbf/iat/exp z tolerancją 0s).
  4. Wyciągnij sub (email) i scopes.
  5. Lazy provision lokalnego usera (jeśli pierwszy login).
  6. Mapuj role / permission na konwencje twojej apki.

refresh_token opaque, ma długi TTL (config DealerID, domyślnie tygodnie). Wymiana standardowa:

POST /token
grant_type=refresh_token
&refresh_token=def50200...
&client_id=bms-frontend

Aplikacja 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.

  • Implicit flow — wyłączony (response_type=token zwró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”.