From 4b2aef7e57751ab35d1eb7d4e6aa918efec10690 Mon Sep 17 00:00:00 2001 From: gabrielzigo Date: Thu, 20 Aug 2026 13:51:06 +0200 Subject: [PATCH 1/4] Add never-expiring personal access tokens with a per-token MCP rate limit --- CHANGELOG.md | 12 ++++ README.md | 11 +++- composer.json | 2 +- .../CreatePersonalAccessTokenCommand.php | 60 ++++++++++++++++++- .../Cache/PersonalAccessTokenAuthCache.php | 36 +++++++---- .../Facade/PersonalAccessTokenFacade.php | 11 ++++ .../Model/CachedPersonalAccessToken.php | 27 +++++++++ .../PersonalAccessTokenRepository.php | 2 +- src/Entity/AbstractPersonalAccessToken.php | 24 ++++++-- .../PersonalAccessTokenAuthenticator.php | 53 +++++++++------- .../PersonalAccessTokenAuthCacheTest.php | 45 ++++++++++---- .../Facade/PersonalAccessTokenFacadeTest.php | 39 +++++++++++- .../PersonalAccessTokenAuthenticatorTest.php | 20 +++++-- 13 files changed, 281 insertions(+), 61 deletions(-) create mode 100644 src/Domain/PersonalAccessToken/Model/CachedPersonalAccessToken.php diff --git a/CHANGELOG.md b/CHANGELOG.md index aeff0c1..5fe26d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## [Unreleased] + +### Features +* `AbstractPersonalAccessToken::expiresAt` is nullable — `NULL` means the token never expires (active in `findOneActiveByTokenHash()`, skipped by `anzu:personal-access-token:notify-expiring`); new `isNeverExpiring()`. 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`. +* `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. +* `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: `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..439b4e0 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,20 @@ $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`). Console commands: -* `anzu:personal-access-token:create --name=