From 652486b7411527cb254a578aaee46cfca1c9890f Mon Sep 17 00:00:00 2001 From: gabrielzigo Date: Wed, 22 Jul 2026 11:39:33 +0200 Subject: [PATCH 1/9] Add opt-in personal access token authentication Opt-in "personal_access_token" config section (disabled by default): sha256 hashed bearer tokens bound to a user with expiration, revocation, versioned auth cache, throttled last-used tracking, ownership voter, management controller and console commands for creation and expiry notifications (pluggable notifier interface with a no-op default). The entity ships as a mapped superclass targeting the contracts AnzuUser via resolve_target_entities; the host subclasses it and owns the table, indexes and migration. --- README.md | 66 ++++++++ composer.json | 12 +- .../CreatePersonalAccessTokenCommand.php | 129 +++++++++++++++ ...ifyExpiringPersonalAccessTokensCommand.php | 70 ++++++++ .../Api/PersonalAccessTokenController.php | 95 +++++++++++ .../AnzuSystemsAuthExtension.php | 88 ++++++++++ src/DependencyInjection/Configuration.php | 28 ++++ .../Cache/PersonalAccessTokenAuthCache.php | 88 ++++++++++ .../Facade/PersonalAccessTokenFacade.php | 68 ++++++++ .../Manager/PersonalAccessTokenManager.php | 38 +++++ .../Model/PersonalAccessTokenCreateDto.php | 41 +++++ .../Model/PersonalAccessTokenCreateResult.php | 19 +++ .../NoopPersonalAccessTokenExpiryNotifier.php | 15 ++ ...onalAccessTokenExpiryNotifierInterface.php | 16 ++ .../PersonalAccessTokenRepository.php | 84 ++++++++++ src/Entity/AbstractPersonalAccessToken.php | 150 +++++++++++++++++ .../PersonalAccessTokenAuthenticator.php | 154 ++++++++++++++++++ .../PersonalAccessTokenPermission.php | 14 ++ .../Voter/PersonalAccessTokenVoter.php | 54 ++++++ tests/Data/Entity/PersonalAccessToken.php | 11 ++ .../AnzuSystemsAuthExtensionTest.php | 50 ++++++ .../PersonalAccessTokenAuthCacheTest.php | 75 +++++++++ .../Facade/PersonalAccessTokenFacadeTest.php | 120 ++++++++++++++ .../AbstractPersonalAccessTokenTest.php | 40 +++++ .../PersonalAccessTokenAuthenticatorTest.php | 137 ++++++++++++++++ .../Voter/PersonalAccessTokenVoterTest.php | 134 +++++++++++++++ 26 files changed, 1793 insertions(+), 3 deletions(-) create mode 100644 src/Command/CreatePersonalAccessTokenCommand.php create mode 100644 src/Command/NotifyExpiringPersonalAccessTokensCommand.php create mode 100644 src/Controller/Api/PersonalAccessTokenController.php create mode 100644 src/Domain/PersonalAccessToken/Cache/PersonalAccessTokenAuthCache.php create mode 100644 src/Domain/PersonalAccessToken/Facade/PersonalAccessTokenFacade.php create mode 100644 src/Domain/PersonalAccessToken/Manager/PersonalAccessTokenManager.php create mode 100644 src/Domain/PersonalAccessToken/Model/PersonalAccessTokenCreateDto.php create mode 100644 src/Domain/PersonalAccessToken/Model/PersonalAccessTokenCreateResult.php create mode 100644 src/Domain/PersonalAccessToken/Notification/NoopPersonalAccessTokenExpiryNotifier.php create mode 100644 src/Domain/PersonalAccessToken/Notification/PersonalAccessTokenExpiryNotifierInterface.php create mode 100644 src/Domain/PersonalAccessToken/Repository/PersonalAccessTokenRepository.php create mode 100644 src/Entity/AbstractPersonalAccessToken.php create mode 100644 src/Security/Authentication/PersonalAccessTokenAuthenticator.php create mode 100644 src/Security/PersonalAccessTokenPermission.php create mode 100644 src/Security/Voter/PersonalAccessTokenVoter.php create mode 100644 tests/Data/Entity/PersonalAccessToken.php create mode 100644 tests/Domain/PersonalAccessToken/Cache/PersonalAccessTokenAuthCacheTest.php create mode 100644 tests/Domain/PersonalAccessToken/Facade/PersonalAccessTokenFacadeTest.php create mode 100644 tests/Entity/AbstractPersonalAccessTokenTest.php create mode 100644 tests/Security/Authentication/PersonalAccessTokenAuthenticatorTest.php create mode 100644 tests/Security/Voter/PersonalAccessTokenVoterTest.php diff --git a/README.md b/README.md index 8962115..c82423a 100644 --- a/README.md +++ b/README.md @@ -68,3 +68,69 @@ $routes ->import('@AnzuSystemsAuthBundle/Controller/Api/JsonCredentialsAuthController.php', type: 'attribute') ->prefix('/api/auth/'); ``` + +## Personal access tokens + +Opt-in personal access token (PAT) authentication: an sha256-hashed bearer token bound to a user, with expiration, +revocation, cached authentication, expiry notifications and management API. Disabled by default — a project that does +not enable it needs no schema or configuration changes after a bundle upgrade. + +Enable it by subclassing the mapped superclass and pointing the config to it: + +```php +use AnzuSystems\AuthBundle\Domain\PersonalAccessToken\Repository\PersonalAccessTokenRepository; +use AnzuSystems\AuthBundle\Entity\AbstractPersonalAccessToken; +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity(repositoryClass: PersonalAccessTokenRepository::class)] +#[ORM\Table(name: 'personal_access_token')] +#[ORM\Index(name: 'IDX_revokedAt_expiresAt', fields: ['revokedAt', 'expiresAt'])] +#[ORM\UniqueConstraint(name: 'UNIQ_tokenHash', fields: ['tokenHash'])] +class PersonalAccessToken extends AbstractPersonalAccessToken +{ +} +``` + +```yaml +anzu_systems_auth: + personal_access_token: + enabled: true + entity_class: App\Domain\PersonalAccessToken\Entity\PersonalAccessToken + user_entity_class: App\Domain\User\Entity\User + auth_cache_pool: 'some_redis.cache' +``` + +The `user` relation targets `AnzuSystems\Contracts\Entity\AnzuUser` — make sure doctrine +`resolve_target_entities` maps it to the project user class. Add the doctrine mapping for the bundle's `Entity` +namespace and generate the migration with `doctrine:migrations:diff`. + +Wire the authenticator into a firewall protecting the API that accepts the tokens: + +```yaml +security: + firewalls: + mcp: + pattern: ^/api/mcp + stateless: true + provider: app_user_provider_id + entry_point: AnzuSystems\AuthBundle\Security\Authentication\PersonalAccessTokenAuthenticator + custom_authenticators: + - AnzuSystems\AuthBundle\Security\Authentication\PersonalAccessTokenAuthenticator +``` + +Management API routes (list/create/revoke) are provided by +`AnzuSystems\AuthBundle\Controller\Api\PersonalAccessTokenController` — add routes pointing at its +`getList`/`create`/`revoke` methods. Authorization uses the `auth_personalAccessToken_(create|read|revoke)` +permissions (see `AnzuSystems\AuthBundle\Security\PersonalAccessTokenPermission`); creation additionally requires +the `ROLE_MCP` role. + +Console commands: + +* `anzu:personal-access-token:create --name=