From 3b79e98404316c391b09d73030bd4405793fe3f4 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 7 Sep 2026 18:46:26 +0100 Subject: [PATCH 1/4] run custom api middleware before the response cache the `statamic.api.middleware` group and graphql schema middleware now run before `HandleAuthentication` and the cache middleware, so cached responses can no longer bypass custom authentication. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015MAdoF7RTBQ4mUTaki7k2e --- routes/routes.php | 12 +-- src/GraphQL/DefaultSchema.php | 4 +- tests/API/APITest.php | 2 +- tests/API/CustomMiddlewareTest.php | 76 +++++++++++++++++++ .../Feature/GraphQL/CustomMiddlewareTest.php | 10 +++ tests/Feature/GraphQL/RequestCacheTest.php | 19 ++--- 6 files changed, 100 insertions(+), 23 deletions(-) create mode 100644 tests/API/CustomMiddlewareTest.php diff --git a/routes/routes.php b/routes/routes.php index 31a12883ff2..e6bee7db6a2 100644 --- a/routes/routes.php +++ b/routes/routes.php @@ -1,5 +1,6 @@ group(function () { - Route::middleware(config('statamic.api.middleware')) - ->name('statamic.api.') - ->prefix(config('statamic.api.route')) - ->group(__DIR__.'/api.php'); - }); + ]) + ->name('statamic.api.') + ->prefix(config('statamic.api.route')) + ->group(__DIR__.'/api.php'); } if (config('statamic.cp.enabled')) { diff --git a/src/GraphQL/DefaultSchema.php b/src/GraphQL/DefaultSchema.php index ed2e70278a4..cdf69f56f08 100644 --- a/src/GraphQL/DefaultSchema.php +++ b/src/GraphQL/DefaultSchema.php @@ -78,9 +78,9 @@ private function getQueries() private function getMiddleware() { return array_merge( - [HandleAuthentication::class, CacheResponse::class], config('statamic.graphql.middleware', []), - GraphQL::getExtraMiddleware() + GraphQL::getExtraMiddleware(), + [HandleAuthentication::class, CacheResponse::class], ); } diff --git a/tests/API/APITest.php b/tests/API/APITest.php index e48647caf0d..e90ce1ab634 100644 --- a/tests/API/APITest.php +++ b/tests/API/APITest.php @@ -1143,7 +1143,7 @@ class FakeTokenHandler { public function handle(\Statamic\Contracts\Tokens\Token $token, \Illuminate\Http\Request $request, \Closure $next) { - return $next($token); + return $next($request); } } diff --git a/tests/API/CustomMiddlewareTest.php b/tests/API/CustomMiddlewareTest.php new file mode 100644 index 00000000000..95f63cf7804 --- /dev/null +++ b/tests/API/CustomMiddlewareTest.php @@ -0,0 +1,76 @@ +setMiddlewareGroups(array_merge( + $app[Kernel::class]->getMiddlewareGroups(), + ['secure-api' => [RequireApiKeyHeader::class, SubstituteBindings::class]], + )); + + $app['config']->set('statamic.api.middleware', 'secure-api'); + } + + public function setUp(): void + { + parent::setUp(); + + Facades\Config::set('statamic.api.resources.collections', true); + + $collection = Facades\Collection::make('articles')->save(); + + EntryFactory::id('apple')->slug('apple')->collection($collection)->create(); + } + + #[Test] + public function custom_middleware_is_applied() + { + $this + ->getJson('/api/collections/articles/entries') + ->assertUnauthorized(); + + $this + ->getJson('/api/collections/articles/entries', ['X-Api-Key' => 'secret']) + ->assertOk() + ->assertJsonPath('data.0.id', 'apple'); + } + + #[Test] + public function custom_middleware_runs_before_the_cached_response_is_returned() + { + $this + ->getJson('/api/collections/articles/entries', ['X-Api-Key' => 'secret']) + ->assertOk(); + + $this + ->getJson('/api/collections/articles/entries') + ->assertUnauthorized(); + } +} + +class RequireApiKeyHeader +{ + public function handle($request, $next) + { + if ($request->header('X-Api-Key') !== 'secret') { + abort(401); + } + + return $next($request); + } +} diff --git a/tests/Feature/GraphQL/CustomMiddlewareTest.php b/tests/Feature/GraphQL/CustomMiddlewareTest.php index b39238c90c6..0535488d5b7 100644 --- a/tests/Feature/GraphQL/CustomMiddlewareTest.php +++ b/tests/Feature/GraphQL/CustomMiddlewareTest.php @@ -53,6 +53,16 @@ protected function addCustomMiddlewareThroughConfig($app) { $app['config']->set('statamic.graphql.middleware', [CountRequests::class]); } + + #[Test] + #[DefineEnvironment('addCustomMiddlewareWithMethod')] + public function custom_middleware_runs_before_the_cached_response_is_returned() + { + $this->post('/graphql', ['query' => '{ping}']); + $this->post('/graphql', ['query' => '{ping}']); + + $this->assertEquals(2, app('request-count')); + } } class CountRequests diff --git a/tests/Feature/GraphQL/RequestCacheTest.php b/tests/Feature/GraphQL/RequestCacheTest.php index 97f6c2927ff..2a143296174 100644 --- a/tests/Feature/GraphQL/RequestCacheTest.php +++ b/tests/Feature/GraphQL/RequestCacheTest.php @@ -27,7 +27,6 @@ public function getEnvironmentSetup($app) GraphQL::addQuery(QueryOne::class); GraphQL::addQuery(QueryTwo::class); - GraphQL::addMiddleware(TrackRequests::class); } #[Test] @@ -254,6 +253,11 @@ class QueryOne extends PingQuery public function resolve() { + app('request-tracking')[] = [ + 'query' => request()->input('query'), + 'variables' => request()->input('variables'), + ]; + return 'one'; } } @@ -268,19 +272,6 @@ public function resolve() } } -class TrackRequests -{ - public function handle($request, $next) - { - app('request-tracking')[] = [ - 'query' => $request->input('query'), - 'variables' => $request->input('variables'), - ]; - - return $next($request); - } -} - class TestTokenHandler { public function handle($token, $request, $next) From 01c73a1715176be0a36ba9e38b7b9a744f0488bd Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 7 Sep 2026 20:29:21 +0100 Subject: [PATCH 2/4] keep `HandleAuthentication` outermost in the rest api middleware stack only the custom middleware needs to run before `Cache`. moving `HandleAuthentication` inside the configured group meant `HandleToken` ran before the `auth_token` check. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01624MAUMx1KeFxpnqwivkiA --- routes/routes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/routes.php b/routes/routes.php index e6bee7db6a2..aac9aa2d14f 100644 --- a/routes/routes.php +++ b/routes/routes.php @@ -11,8 +11,8 @@ if (config('statamic.api.enabled')) { Route::middleware([ RequireStatamicPro::class, - ...Arr::wrap(config('statamic.api.middleware')), HandleAuthentication::class, + ...Arr::wrap(config('statamic.api.middleware')), Cache::class, ]) ->name('statamic.api.') From 6bb7911d6c51ca1fd3796a76df4b6122f795d5ee Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 7 Sep 2026 20:29:22 +0100 Subject: [PATCH 3/4] keep `HandleAuthentication` first in the graphql schema middleware only `CacheResponse` needs to move behind the custom middleware. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01624MAUMx1KeFxpnqwivkiA --- src/GraphQL/DefaultSchema.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/GraphQL/DefaultSchema.php b/src/GraphQL/DefaultSchema.php index cdf69f56f08..b963f68eb8b 100644 --- a/src/GraphQL/DefaultSchema.php +++ b/src/GraphQL/DefaultSchema.php @@ -78,9 +78,10 @@ private function getQueries() private function getMiddleware() { return array_merge( + [HandleAuthentication::class], config('statamic.graphql.middleware', []), GraphQL::getExtraMiddleware(), - [HandleAuthentication::class, CacheResponse::class], + [CacheResponse::class], ); } From 5136fc777a91b29494fa2dfe8b877f1379513c5e Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 7 Sep 2026 20:29:37 +0100 Subject: [PATCH 4/4] cover the `statamic.graphql.middleware` config path in `CustomMiddlewareTest` the config test was pointed at the `GraphQL::addMiddleware()` environment, leaving `addCustomMiddlewareThroughConfig` unused. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01624MAUMx1KeFxpnqwivkiA --- tests/Feature/GraphQL/CustomMiddlewareTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/GraphQL/CustomMiddlewareTest.php b/tests/Feature/GraphQL/CustomMiddlewareTest.php index 0535488d5b7..e582485bc41 100644 --- a/tests/Feature/GraphQL/CustomMiddlewareTest.php +++ b/tests/Feature/GraphQL/CustomMiddlewareTest.php @@ -41,7 +41,7 @@ protected function addCustomMiddlewareWithMethod($app) } #[Test] - #[DefineEnvironment('addCustomMiddlewareWithMethod')] + #[DefineEnvironment('addCustomMiddlewareThroughConfig')] public function a_custom_middleware_can_be_added_to_the_default_schema_through_config() { $this->post('/graphql', ['query' => '{ping}']); @@ -55,7 +55,7 @@ protected function addCustomMiddlewareThroughConfig($app) } #[Test] - #[DefineEnvironment('addCustomMiddlewareWithMethod')] + #[DefineEnvironment('addCustomMiddlewareThroughConfig')] public function custom_middleware_runs_before_the_cached_response_is_returned() { $this->post('/graphql', ['query' => '{ping}']);