OAuth2 SSO od zera z Laravel Passport — jak to działa pod maską

Zbudowałem centralny serwer SSO oparty na Laravel Passport. Tłumaczę flow Authorization Code, tokeny, odświeżanie sesji i integrację z wieloma klientami.

Po co własne SSO?

W systemie mam kilku klientów, którzy potrzebują uwierzytelniania: Frontend, Admin. Zamiast duplikować logikę logowania w każdym serwisie, wyodrębniłem ją do osobnego mikroserwisu SSO opartego na OAuth2.

Benefity:

  • Jeden punkt zarządzania sesjami i tokenami
  • Zmiana hasła w jednym miejscu → wszystkie klienty dotknięte
  • Możliwość dodania 2FA, social login itp. bez zmian w klientach

Laravel Passport jako serwer OAuth2

Laravel Passport to pełna implementacja OAuth2 na bazie league/oauth2-server. Wspiera grant types: Authorization Code, Client Credentials, Password (deprecated), Refresh Token.

W moim projekcie używam Authorization Code z PKCE — najbezpieczniejszy flow dla aplikacji webowych.

Jak wygląda flow?

1. Użytkownik klika "Zaloguj się" na Frontend
2. Frontend przekierowuje na SSO: /oauth/authorize?client_id=...&redirect_uri=...&code_challenge=...
3. SSO pokazuje stronę logowania
4. Użytkownik podaje dane → SSO weryfikuje przez Users API
5. SSO przekierowuje z powrotem: /oauth/callback?code=AUTH_CODE
6. Frontend wymienia AUTH_CODE na access_token + refresh_token (POST /oauth/token)
7. access_token trafia do sesji, używany do wywołań API

Weryfikacja hasła przez Users API

SSO nie ma własnej bazy użytkowników — deleguje weryfikację do Users API przez internal endpoint:

// SSO AuthController
$response = Http::withHeaders([
    'X-Internal-Api-Key' => config('services.users.internal_key'),
])->post(config('services.users.url') . '/api/internal/verify-password', [
    'email' => $request->email,
    'password' => $request->password,
]);

Dzięki temu Users jest jedynym serwisem, który "zna" hasła. SSO tylko pyta.

Refresh token i wygasanie sesji

access_token wygasa po 1 godzinie. Przy każdym żądaniu do API frontend sprawdza czy token nie wygasł i w razie potrzeby odpytuje SSO o nowy token przy pomocy refresh_token.

if ($this->isTokenExpired($session->get('expires_at'))) {
    $newTokens = $this->refreshAccessToken($session->get('refresh_token'));
    $session->put('access_token', $newTokens['access_token']);
}

Klient Admin (FilamentPHP)

Admin panel używa tego samego SSO — zaimplementowałem osobny OAuth2 client z redirect_uri wskazującym na domenę admina. Filament dostaje token i robi API calls z Bearer auth.

Comments

No comments yet