diff --git a/CHANGELOG.md b/CHANGELOG.md index aeff0c1..0fa6229 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## [Unreleased] + +### Features +* `AbstractPersonalAccessToken::expiresAt` is nullable — `NULL` means the token never expires (active in `findOneActiveByTokenHash()`, skipped by `anzu:personal-access-token:notify-expiring`). New `rateLimit` column (`?int`, not serialized) — a per-token MCP rate limit overriding the configured default. Hosts must add a migration: `expires_at DATETIME DEFAULT NULL`, `rate_limit INT UNSIGNED DEFAULT NULL`, `user_id` foreign key `ON DELETE CASCADE`. +* `anzu:personal-access-token:create` gained `--never-expires` (mutually exclusive with `--expires-at`) and `--rate-limit=N`; `PersonalAccessTokenFacade::create()` gained `?int $rateLimit` and `bool $neverExpires`. The management API keeps creating expiring tokens without a rate limit. +* `PersonalAccessTokenFacade::deleteByUser()` + `PersonalAccessTokenManager::delete()` remove all tokens of a user and invalidate their auth cache entries — call it before deleting the user (the only reliable path — `created_by`/`modified_by` of the user's own tokens still reference the user); the `user` join column additionally declares `onDelete: CASCADE` as a best-effort database-level cleanup (hosts must update the foreign key in their migration). +* `PersonalAccessTokenAuthenticator` sets `McpRateLimiter::TOKEN_ATTRIBUTE_KEY` (`pat_`) and `McpRateLimiter::TOKEN_ATTRIBUTE_LIMIT` on the security token; `PersonalAccessTokenAuthCache` caches a `CachedPersonalAccessToken` (token id, user id, rate limit) under a new key prefix instead of the bare user id. + +### Changes +* BC change: `anzusystems/common-bundle` requirement raised to `^11.5` (the authenticator uses `McpRateLimiter::TOKEN_ATTRIBUTE_*`). +* BC change: `PersonalAccessTokenFacade` constructor gained `PersonalAccessTokenRepository $repository` (autowired). +* BC change: `AbstractPersonalAccessToken::getExpiresAt()` returns `?DateTimeImmutable`, `setExpiresAt()` accepts `null`; `PersonalAccessTokenAuthCache::getUserId()/storeUserId()` replaced by `getToken()/storeToken()`. +* A cached token whose user entity no longer exists now fails authentication instead of falling back to the database lookup. + ## [6.0.0](https://github.com/anzusystems/auth-bundle/compare/5.0.0...6.0.0) (2026-07-22) ### Features diff --git a/README.md b/README.md index 33516be..1cebb28 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ $routes ## Personal access tokens -Opt-in personal access token (PAT) authentication: an sha256-hashed bearer token bound to a user, with expiration, +Opt-in personal access token (PAT) authentication: an sha256-hashed bearer token bound to a user, with optional 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. @@ -130,13 +130,25 @@ $routes ->prefix('/api/adm/v1'); ``` +The authenticator sets the `McpRateLimiter::TOKEN_ATTRIBUTE_KEY` (`pat_`) and `McpRateLimiter::TOKEN_ATTRIBUTE_LIMIT` +(the token's `rateLimit`) attributes on the security token, so the common-bundle MCP rate limiter (`anzusystems/common-bundle` +`>=11.5`) buckets requests per personal access token and honours the per-token limit. + 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`). +Deleting a user: call `PersonalAccessTokenFacade::deleteByUser($user)` before removing the user entity — it deletes the +user's tokens in one flush and invalidates their auth cache entries. The `user` join column also declares `onDelete: CASCADE`, +but that is best-effort only: the `created_by`/`modified_by` columns of the user's own tokens still reference the user, so +the explicit `deleteByUser()` call is the contract. + Console commands: -* `anzu:personal-access-token:create --name=