diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..aeff0c1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +## [6.0.0](https://github.com/anzusystems/auth-bundle/compare/5.0.0...6.0.0) (2026-07-22) + +### Features +* New opt-in `personal_access_token` config section (disabled by default — upgrading without enabling it requires no schema or configuration changes): sha256-hashed bearer tokens bound to a user with expiration (default 2 months, max 1 year), revocation, a versioned auth cache with the entry lifetime capped at the token expiry, throttled `lastUsedAt` tracking and a read-only-mode guard. +* `AbstractPersonalAccessToken` mapped superclass — the host subclasses it, owns the table, indexes (`UNIQ_tokenHash`, `IDX_revokedAt_expiresAt`) and migration; the `user` relation targets the contracts `AnzuUser` via doctrine `resolve_target_entities`. Configured via `entity_class`, `user_entity_class` and `auth_cache_pool`. +* `PersonalAccessTokenAuthenticator` (`Security/Authentication`) — stateless bearer authenticator claiming only tokens with the `anzu_pat_` prefix, cache-first user lookup, uniform information-free 401 responses with a `WWW-Authenticate: Bearer` header. +* `PersonalAccessTokenVoter` + `PersonalAccessTokenPermission` — `auth_personalAccessToken_(create|read|revoke)` permissions with owner-only access and creation gated by `ROLE_MCP`. +* `PersonalAccessTokenController` (`Controller/Api`) — list own tokens, create (plaintext returned exactly once, request excluded from audit logs) and revoke; the host defines the routes. +* Console commands `anzu:personal-access-token:create` (prints the plaintext token once) and `anzu:personal-access-token:notify-expiring` (daily cron notifying owners 7 days and 1 day before expiry through `PersonalAccessTokenExpiryNotifierInterface`; no-op by default — alias your own implementation, which must be idempotent per (token, daysRemaining) pair because consecutive final-notice windows overlap). + +### Changes +* BC change: `anzusystems/common-bundle` requirement floor raised to `^9.4` (uses `AuditLogResourceHelper::excludeFromAuditLogs()`), with `^12.0` allowed. +* BC change: new explicit `symfony/console` requirement `^7.3|^8.0` (invokable command support). diff --git a/README.md b/README.md index 8962115..33516be 100644 --- a/README.md +++ b/README.md @@ -68,3 +68,79 @@ $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`. `user_entity_class` must name the same class +as the common-bundle `settings.user_entity_class` — the authenticator and `CurrentAnzuUserProvider` would otherwise +load different user classes. The entity relies on constructor-less proxies, so use doctrine/orm 3 (lazy ghosts); +the ORM 2 legacy proxy strategy conflicts with the final entity constructor. + +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` attribute routes — import them with a prefix: + +```php +$routes + ->import('@AnzuSystemsAuthBundle/Controller/Api/PersonalAccessTokenController.php', type: 'attribute') + ->prefix('/api/adm/v1'); +``` + +Authorization uses the `auth_personalAccessToken_(create|read|revoke)` permissions (see +`AnzuSystems\AuthBundle\Security\PersonalAccessTokenPermission`); creation additionally requires the role +configured via `create_role` (default `ROLE_MCP`). + +Console commands: + +* `anzu:personal-access-token:create --name=