PHPackages                             musikhood/auth-client-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. musikhood/auth-client-bundle

ActiveSymfony-bundle[Authentication &amp; Authorization](/categories/authentication)

musikhood/auth-client-bundle
============================

Symfony bundle for editor\_v3 cookie-based auth (BEARER + refresh\_token, JWT/JWKS validation, user mirror).

v0.3.0(1w ago)063↓100%MITPHPPHP &gt;=8.2

Since May 6Pushed 1w agoCompare

[ Source](https://github.com/musikhood/auth-client-bundle)[ Packagist](https://packagist.org/packages/musikhood/auth-client-bundle)[ RSS](/packages/musikhood-auth-client-bundle/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (21)Versions (12)Used By (0)

auth-client-bundle
==================

[](#auth-client-bundle)

Symfony bundle dla mikroserwisów, które delegują uwierzytelnianie do zewnętrznego auth servera (editor\_v3) i wystawiają front-endowi spójny kontrakt oparty na ciasteczkach HttpOnly.

Bundle obsługuje całą warstwę HTTP — endpointy `/api/login`, `/api/logout`, `/api/token/refresh`, `/api/v1/user/me` — walidację JWT/JWKS, kontrakt ciasteczek (`BEARER` + `refresh_token`, oba HttpOnly), lokalną kopię użytkownika z lazy upsert oraz listener z circuit breakerem, który co 30 s weryfikuje sesję w auth serverze.

Front nigdy nie widzi JWT. Używa `withCredentials: true` plus interceptora axiosa, który na `401` woła `/api/token/refresh`. Front napisany przeciw samemu auth serverowi działa bez zmian z dowolnym mikroserwisem korzystającym z tej paczki.

Wymagania
---------

[](#wymagania)

- PHP `>=8.2`
- Symfony `^6.4 || ^7.0` (security-bundle, framework-bundle, http-client)
- ORM po stronie konsumenta (zalecany Doctrine) — paczka dostarcza tylko interfejsy (`PanelUserInterface`, `PanelUserRepositoryInterface`); konkretną encję i repozytorium tworzy konsument.

Instalacja
----------

[](#instalacja)

### 1. Dodaj endpoint Symfony Flex

[](#1-dodaj-endpoint-symfony-flex)

Bundle dostarcza recipe Symfony Flex, które konfiguruje `bundles.php`, `config/packages/auth_client.yaml`, import tras i zmienne środowiskowe. Recipe siedzi w [musikhood/symfony-recipes](https://github.com/musikhood/symfony-recipes).

Dodaj endpoint do `composer.json` **w aplikacji konsumenta** (jednorazowo):

```
{
    "extra": {
        "symfony": {
            "endpoint": [
                "https://api.github.com/repos/musikhood/symfony-recipes/contents/index.json",
                "flex://defaults"
            ]
        }
    }
}
```

Zostaw `flex://defaults` po swoim endpoincie — bez tego nie będą działać oficjalne recipes Symfony (Doctrine, Mailer itd.).

### 2. Zainstaluj paczkę

[](#2-zainstaluj-paczkę)

```
composer require musikhood/auth-client-bundle:^0.1
```

Flex zrobi automatycznie:

- zarejestruje `Musikhood\AuthClient\AuthClientBundle` w `config/bundles.php`
- utworzy `config/packages/auth_client.yaml` z szablonem na zmienne środowiskowe
- utworzy `config/routes/auth_client.yaml` importujący trasy paczki
- doda klucze `AUTH_*` do `.env`
- wyświetli listę kroków, które musisz dokończyć ręcznie (sekcja 3 niżej)

Jeśli `composer require` nie pokaże komunikatu post-install, prawdopodobnie:

- Flex nie jest zainstalowany w konsumencie (`composer require symfony/flex`)
- brakuje konfiguracji endpointu z kroku 1
- paczka była już zainstalowana wcześniej — zrób `composer remove musikhood/auth-client-bundle && composer clear-cache` i powtórz `require`

### 3. Kroki, które musisz wykonać ręcznie

[](#3-kroki-które-musisz-wykonać-ręcznie)

Recipe robi tylko to, co bezpiecznie da się zautomatyzować. Pięć rzeczy wymaga jeszcze Twojej ręki — wszystkie dotykają miejsc specyficznych dla projektu, których recipe nie może zgadnąć.

#### 3.1. Ustaw zmienne środowiskowe

[](#31-ustaw-zmienne-środowiskowe)

W `.env.local` (albo Twoim secrets manager):

```
AUTH_BASE_URL=https://auth.twoja-domena.com
AUTH_PANEL_ID=01234567-89ab-cdef-0123-456789abcdef
AUTH_CLIENT_ID=
AUTH_CLIENT_SECRET=
AUTH_COOKIE_SECURE=1
```

`AUTH_COOKIE_SECURE` ustaw na `1` w produkcji (HTTPS), `0` tylko dla lokalnego deva po HTTP.

#### 3.2. Stwórz encję `User`

[](#32-stwórz-encję-user)

Paczka nigdy nie pisze do tabeli użytkowników — to robi konsument. Zaimplementuj `Musikhood\AuthClient\Contract\PanelUserInterface`(referencja: [`docs/example-entity.php`](docs/example-entity.php)):

```
// src/Entity/User.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Musikhood\AuthClient\Contract\PanelUserInterface;
use Ramsey\Uuid\UuidInterface;

#[ORM\Entity(repositoryClass: \App\Repository\UserRepository::class)]
#[ORM\Table(name: 'users')]
#[ORM\UniqueConstraint(name: 'uniq_users_email', columns: ['email'])]
class User implements PanelUserInterface
{
    #[ORM\Id]
    #[ORM\Column(type: 'uuid', unique: true)]
    private UuidInterface $id;

    #[ORM\Column(type: 'string', length: 180, unique: true)]
    private string $email;

    #[ORM\Column(type: 'string', length: 255, nullable: true)]
    private ?string $displayName = null;

    /** @var list */
    #[ORM\Column(type: 'json')]
    private array $rolesForPanel = [];

    #[ORM\Column(type: 'boolean', options: ['default' => false])]
    private bool $disabled = false;

    #[ORM\Column(type: 'datetime_immutable')]
    private \DateTimeImmutable $lastSyncedAt;

    private function __construct() {}

    /** @param list $rolesForPanel */
    public static function create(
        UuidInterface $id,
        string $email,
        ?string $displayName,
        array $rolesForPanel,
    ): self {
        $user = new self();
        $user->id = $id;
        $user->email = $email;
        $user->displayName = $displayName;
        $user->rolesForPanel = array_values($rolesForPanel);
        $user->disabled = false;
        $user->lastSyncedAt = new \DateTimeImmutable();
        return $user;
    }

    /** @param list $rolesForPanel */
    public function syncFromClaims(string $email, ?string $displayName, array $rolesForPanel): void
    {
        $this->email = $email;
        $this->displayName = $displayName;
        $this->rolesForPanel = array_values($rolesForPanel);
        $this->lastSyncedAt = new \DateTimeImmutable();
    }

    public function markDisabled(bool $disabled): void { $this->disabled = $disabled; }
    public function getId(): UuidInterface { return $this->id; }
    public function getEmail(): string { return $this->email; }
    public function getDisplayName(): ?string { return $this->displayName; }
    /** @return list */
    public function getRolesForPanel(): array { return $this->rolesForPanel; }
    public function isDisabled(): bool { return $this->disabled; }

    /** @return list */
    public function getRoles(): array
    {
        $roles = ['ROLE_USER'];
        foreach ($this->rolesForPanel as $r) { $roles[] = 'ROLE_' . $r; }
        return array_values(array_unique($roles));
    }

    public function getUserIdentifier(): string { return $this->email; }
    public function eraseCredentials(): void {}
}
```

Jeśli używasz innej lokalizacji niż `src/Entity/` (np. DDD ze strukturą `src/Domain/User/Entity/User.php`), umieść klasę gdziekolwiek — paczka patrzy tylko na kontrakt `PanelUserInterface`.

#### 3.3. Stwórz `UserRepository`

[](#33-stwórz-userrepository)

Zaimplementuj `Musikhood\AuthClient\Contract\PanelUserRepositoryInterface`i powiąż go z interfejsem przez atrybut `#[AsAlias]`. Dzięki temu **nie musisz nic dopisywać do `services.yaml`** — Symfony sam podepnie repo pod interfejs.

Referencja: [`docs/example-repository.php`](docs/example-repository.php).

```
// src/Repository/UserRepository.php
namespace App\Repository;

use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Musikhood\AuthClient\Contract\PanelUserInterface;
use Musikhood\AuthClient\Contract\PanelUserRepositoryInterface;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\DependencyInjection\Attribute\AsAlias;

/** @extends ServiceEntityRepository */
#[AsAlias(id: PanelUserRepositoryInterface::class)]
class UserRepository extends ServiceEntityRepository implements PanelUserRepositoryInterface
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, User::class);
    }

    public function findById(UuidInterface $id): ?PanelUserInterface
    {
        return $this->find($id);
    }

    public function findByEmail(string $email): ?PanelUserInterface
    {
        return $this->findOneBy(['email' => $email]);
    }

    public function save(PanelUserInterface $user): void
    {
        $this->getEntityManager()->persist($user);
    }

    public function flush(): void
    {
        $this->getEntityManager()->flush();
    }

    public function createFromClaims(
        UuidInterface $id,
        string $email,
        ?string $displayName,
        array $rolesForPanel,
    ): PanelUserInterface {
        return User::create($id, $email, $displayName, $rolesForPanel);
    }
}
```

Atrybut `#[AsAlias]` wymaga Symfony 6.1+. Jeśli z jakiegoś powodu wolisz mapowanie w YAML-u, zamiast atrybutu dodaj do `config/services.yaml`:

```
services:
    Musikhood\AuthClient\Contract\PanelUserRepositoryInterface:
        alias: App\Repository\UserRepository
```

Paczka rozwiązuje `PanelUserRepositoryInterface` z kontenera DI — bez jednego z tych dwóch wariantów authenticator i `MeController` wywalą się przy starcie z błędem "Cannot autowire".

#### 3.4. Skonfiguruj security

[](#34-skonfiguruj-security)

Recipe **nie modyfikuje** `config/packages/security.yaml`, bo większość projektów ma już własny firewall i auto-merge byłby ryzykowny. Skopiuj ten fragment ręcznie:

```
# config/packages/security.yaml
security:
    providers:
        # Authenticator zwraca User-a wprost — Symfony nie ma skąd go
        # przeładować, więc in_memory provider jest poprawny.
        in_memory:
            memory: ~

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        login:
            # Login, logout i refresh muszą być publiczne — authenticator
            # nie może działać na /api/token/refresh, bo access_token jest
            # wtedy już nieważny.
            pattern: ^/api/(login|logout|token/(refresh|invalidate))$
            stateless: true
            security: false

        api:
            pattern: ^/api
            stateless: true
            custom_authenticators:
                - Musikhood\AuthClient\Security\JwtCookieAuthenticator
            entry_point: Musikhood\AuthClient\Security\JwtCookieAuthenticator

    access_control:
        - { path: ^/api/(login|logout|token/(refresh|invalidate)), roles: PUBLIC_ACCESS }
        # Webhook inwalidacji usera (0s revocation) — własna autoryzacja przez
        # podpis JWT auth servera, NIE user auth. MUSI być przed catch-all ^/api.
        - { path: ^/api/auth-client/webhook/, roles: PUBLIC_ACCESS }
        - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }

    # Opcjonalnie: zdefiniuj hierarchię ról. Paczka nie narzuca żadnych
    # konkretnych ról — `panel_roles` z JWT są wystawiane wprost z
    # prefiksem ROLE_.
    role_hierarchy:
        ROLE_ADMIN: [ROLE_USER]
```

#### 3.5. Stwórz tabelę w bazie

[](#35-stwórz-tabelę-w-bazie)

Albo skopiuj [`docs/example-migration.sql`](docs/example-migration.sql)prosto do swojego narzędzia migracji, albo wygeneruj migrację z encji:

```
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
```

Tabela potrzebuje kolumn: `id` (UUID), `email` (unique), `display_name`, `roles_for_panel` (JSON), `disabled` (bool), `last_synced_at`. Pełny mapping w `docs/example-entity.php`.

Synchronizacja z auth serverem
------------------------------

[](#synchronizacja-z-auth-serverem)

Auth server jest jedynym źródłem prawdy dla ról, displayName i flagi `disabled`. Lokalna kopia użytkownika w mikroserwisie jest aktualizowana w dwóch momentach:

1. **Pierwszy kontakt z userem (bootstrap)** — gdy authenticator widzi zalogowanego usera, którego jeszcze nie ma w lokalnej tabeli, paczka tworzy lokalną kopię z claimów świeżego JWT (email, displayName, role per-panel). To jednorazowe — przy każdym kolejnym requeście tego usera authenticator NIE rusza już lokalnej kopii.
2. **Co ~30s na żądanie zalogowanego usera** — `AuthValidationListener`woła `/api/v1/user/me` na auth serverze i synchronizuje pełen payload (email, displayName, role per-panel, flaga `disabled`). To jest jedyna ścieżka aktualizacji istniejącej kopii. Krok pomijany jeśli wynik z poprzedniego wywołania jeszcze leży w cache (`validation_cache_ttl`, domyślnie 30s).

W szczególności **paczka nie używa lokalnej flagi `isDisabled()` do podejmowania decyzji o autoryzacji**. Gating disabled userów leci wyłącznie przez `/me` — co znaczy że:

- Po **zablokowaniu** konta w panelu admin auth servera użytkownik zostanie wylogowany w czasie max. `validation_cache_ttl` sekund.
- Po **odblokowaniu** konta użytkownik znowu działa bez ponownego logowania (jeśli JWT jest jeszcze ważny — w innym przypadku front interceptor zrobi `/api/token/refresh` i auth server wystawi nowy).
- Po zmianie ról / displayName w panelu admin nowe wartości pojawią się w lokalnej kopii w czasie max. `validation_cache_ttl` sekund. Auth server NIE podbija `tokenVersion` przy tych zmianach — istniejące JWT zostają ważne, propagacja idzie przez `/me`.

Lokalne pole `disabled` w encji konsumenta służy tylko do wyświetlenia (np. w panelu zarządzania userami w mikroserwisie). Aktualizowane automatycznie przez `syncFromMe()`.

Webhook inwalidacji (0s revocation)
-----------------------------------

[](#webhook-inwalidacji-0s-revocation)

Od wersji `0.3.0` paczka nasłuchuje webhooków od auth servera i skraca czas rewokacji sesji z ~30s (poll `/me` opisany wyżej) do setek milisekund.

**Jak to działa.** Po inwalidacji usera (zablokowanie konta, zmiana hasła, odebranie dostępu do panelu) auth server podbija `tokenVersion` i pushuje podpisany webhook na endpoint paczki `POST /api/auth-client/webhook/user-invalidated`. Paczka weryfikuje podpis (`WebhookJwtValidator`, ten sam JWKS co user JWT), zapisuje nową `tokenVersion`w cache (`UserTokenVersionStore`) i kasuje cache walidacji usera. Przy najbliższym requeście tego usera `JwtCookieAuthenticator` porównuje `ver`z jego JWT z zapisaną wartością i odrzuca stary token (401) bez czekania na poll `/me`.

**Co musisz zrobić.**

1. Dodaj `PUBLIC_ACCESS` dla ścieżki webhooka w `security.yaml` (patrz krok „Skonfiguruj security" powyżej). Webhook autoryzuje się sam podpisem JWT auth servera — to model jak weryfikacja podpisu webhooków Stripe/GitHub, nie dziura w security.
2. W panelu admin auth servera ustaw pole „Webhook URL" dla swojego panelu na bazowy URL backendu mikroserwisu (np. `https://pim.vitkac.com`). Auth server sam dokleja ścieżkę `/api/auth-client/webhook/user-invalidated`.
3. Upewnij się, że `cache.app` jest skonfigurowane (Redis zalecany — stan musi być współdzielony między procesami workerów i przeżyć restart).

**Nie musisz nic zmieniać w swojej encji.** `tokenVersion` żyje w cache PSR, nie w kolumnie DB — webhook może przyjść nawet dla usera, którego mikroserwis jeszcze nie zna lokalnie. Reset cache (flush Redisa) jest nieszkodliwy: brak wpisu = pass, a poll `/me` dogoni inwalidację w ~30s (fallback).

**Monolog tip.** Jeśli używasz `fingers_crossed` z `action_level: error`(typowy prod default Symfony), logi `webhook.received` (poziom `info`) będą buforowane i tracone, gdy webhook kończy się 200 OK — bufor jest zrzucany tylko gdy w tym samym requeście wystąpi `error`. Żeby widzieć je w `kubectl logs`, dodaj osobny handler/channel ze `stream` do `php://stderr`na poziomie `info`.

Kontrakt z front-endem
----------------------

[](#kontrakt-z-front-endem)

Front nigdy nie widzi JWT. Wymaga trzech rzeczy:

- `axios.defaults.withCredentials = true` (lub równowartość `fetch` z `credentials: 'include'`)
- na `401` z dowolnego `/api/*`: `POST /api/token/refresh`, potem retry
- na `401` z `/api/token/refresh`: czyść lokalny stan UI, redirect do logowania

To dokładnie ten sam kontrakt co przy gadaniu wprost z auth serverem, więc istniejący front przesiada się na inny backend bez zmian.

Pełna konfiguracja
------------------

[](#pełna-konfiguracja)

Wszystkie klucze z domyślnymi wartościami — ustawiasz je w `config/packages/auth_client.yaml`. Recipe wgrywa tylko klucze wymagane (cztery `AUTH_*` env-y plus `cookie.secure`); reszta poniżej ma sensowne defaulty.

```
auth_client:
    base_url:      '%env(AUTH_BASE_URL)%'      # wymagane
    panel_id:      '%env(AUTH_PANEL_ID)%'      # wymagane
    client_id:     '%env(AUTH_CLIENT_ID)%'     # wymagane
    client_secret: '%env(AUTH_CLIENT_SECRET)%' # wymagane

    jwks_cache_ttl: 3600         # TTL cache dokumentu JWKS (sekundy)
    validation_cache_ttl: 30     # TTL cache introspekcji /me per user (sekundy)

    cookie:
        access_name: BEARER
        refresh_name: refresh_token
        path: /
        secure: '%env(bool:default::AUTH_COOKIE_SECURE)%'
        http_only: true
        same_site: lax           # lax | strict | none
        # TTL ciasteczek nie jest konfigurowalne — paczka zna typowe wartości
        # auth servera (BEARER 15 min, refresh_token 30 dni). Jeśli auth server
        # ma inne TTL, podbumpuj wersję paczki.

    circuit_breaker:
        failure_threshold: 3     # ile kolejnych błędów /me otwiera breaker
        open_seconds: 60         # ile sekund breaker jest otwarty

    http:
        timeout: 5.0
        max_duration: 10.0
```

Endpointy wystawiane przez paczkę
---------------------------------

[](#endpointy-wystawiane-przez-paczkę)

MetodaŚcieżkaOpis`POST``/api/login`Wymienia `{username, password}` na ciasteczka `BEARER` + `refresh_token`.`POST``/api/logout` (alias `/api/token/invalidate`)Czyści ciasteczka, unieważnia refresh token w auth serverze.`POST``/api/token/refresh`Generuje nową parę ciasteczek z refresh tokena.`GET``/api/v1/user/me`Zwraca dane zalogowanego użytkownika (`id`, `email`, `displayName`, `roles`, `disabled`). Czyta z lokalnej kopii — nie wymaga round-tripa do auth servera.`POST``/api/auth-client/webhook/user-invalidated`Webhook inwalidacji usera (0s revocation). Autoryzacja podpisem JWT auth servera (nie user auth — wymaga `PUBLIC_ACCESS` w `access_control`). Patrz „Webhook inwalidacji".Licencja
--------

[](#licencja)

MIT — patrz [LICENSE](LICENSE).

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance98

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity42

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Total

11

Last Release

12d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/cc51851767102a8f466c1a028ae6cc9f4ae38b13732a7196d5bb4324f5cceb1b?d=identicon)[musikhood](/maintainers/musikhood)

---

Tags

jwtsymfonyauthSymfony BundleJWKSeditor-v3

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/musikhood-auth-client-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/musikhood-auth-client-bundle/health.svg)](https://phpackages.com/packages/musikhood-auth-client-bundle)
```

###  Alternatives

[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.4M195](/packages/sulu-sulu)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.4M506](/packages/shopware-core)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k5.8M710](/packages/sylius-sylius)[shopware/platform

The Shopware e-commerce core

3.4k1.5M3](/packages/shopware-platform)[contao/core-bundle

Contao Open Source CMS

1231.6M2.6k](/packages/contao-core-bundle)[web-auth/webauthn-framework

FIDO2/Webauthn library for PHP and Symfony Bundle.

51090.8k2](/packages/web-auth-webauthn-framework)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
