From da8a8eee98278c1f3f19636dc2fcbef802eba0de Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Fri, 24 Jul 2026 17:59:10 +0200 Subject: [PATCH 1/4] Create API auth exception class --- src/Exceptions/ApiAuthenticationException.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/Exceptions/ApiAuthenticationException.php diff --git a/src/Exceptions/ApiAuthenticationException.php b/src/Exceptions/ApiAuthenticationException.php new file mode 100644 index 00000000000..7b537fd338a --- /dev/null +++ b/src/Exceptions/ApiAuthenticationException.php @@ -0,0 +1,17 @@ +json( + ['message' => $this->getMessage()], + 401, + ); + } +} From 349cdcc0dc0a65dcf2754008131a23f8afadc7d0 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Fri, 24 Jul 2026 17:59:28 +0200 Subject: [PATCH 2/4] Send http header with authentication method --- src/Exceptions/ApiAuthenticationException.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Exceptions/ApiAuthenticationException.php b/src/Exceptions/ApiAuthenticationException.php index 7b537fd338a..2987ad8d97f 100644 --- a/src/Exceptions/ApiAuthenticationException.php +++ b/src/Exceptions/ApiAuthenticationException.php @@ -12,6 +12,7 @@ public function toResponse($request) return response()->json( ['message' => $this->getMessage()], 401, + ['WWW-Authenticate' => 'Bearer'], ); } } From 9b17e9dba27e3610c820cc0400b134a33dbacd48 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Fri, 24 Jul 2026 17:59:42 +0200 Subject: [PATCH 3/4] Throw api auth exception from api middleware --- src/API/Middleware/HandleAuthentication.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/API/Middleware/HandleAuthentication.php b/src/API/Middleware/HandleAuthentication.php index c40b890a4ce..65c0a1faa63 100644 --- a/src/API/Middleware/HandleAuthentication.php +++ b/src/API/Middleware/HandleAuthentication.php @@ -3,6 +3,7 @@ namespace Statamic\API\Middleware; use Closure; +use Statamic\Exceptions\ApiAuthenticationException; class HandleAuthentication { @@ -18,7 +19,7 @@ public function handle($request, Closure $next) ($token = config('statamic.api.auth_token')) && ($request->bearerToken() !== $token) ) { - abort(401); + throw new ApiAuthenticationException; } return $next($request); From add65f692ac5b1bdfc44b6e87e50459943356137 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Fri, 24 Jul 2026 17:59:57 +0200 Subject: [PATCH 4/4] Add tests for usage of custom api auth exception --- tests/API/AuthenticationTest.php | 44 ++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/tests/API/AuthenticationTest.php b/tests/API/AuthenticationTest.php index 26914ab1998..72f1ea5332d 100644 --- a/tests/API/AuthenticationTest.php +++ b/tests/API/AuthenticationTest.php @@ -40,28 +40,50 @@ public function it_cant_authenticate_with_invalid_auth_token() Facades\Config::set('statamic.api.auth_token', 'foobar'); $this - ->withToken($token = 'invalid') - ->getJson($url = '/api/collections/articles/entries') - ->assertUnauthorized(); + ->withToken('invalid') + ->getJson('/api/collections/articles/entries') + ->assertUnauthorized() + ->assertHeader('WWW-Authenticate', 'Bearer') + ->assertExactJson(['message' => 'Unauthenticated.']); + } + + #[Test] + public function it_cant_authenticate_without_auth_token() + { + Facades\Config::set('statamic.api.auth_token', 'foobar'); $this - ->withToken($token) - ->get($url) - ->assertUnauthorized(); + ->getJson('/api/collections/articles/entries') + ->assertUnauthorized() + ->assertHeader('WWW-Authenticate', 'Bearer') + ->assertExactJson(['message' => 'Unauthenticated.']); } #[Test] - public function it_cant_authenticate_without_auth_token() + public function it_returns_the_same_json_response_when_debug_mode_is_enabled() { + Facades\Config::set('app.debug', true); Facades\Config::set('statamic.api.auth_token', 'foobar'); $this - ->getJson($url = '/api/collections/articles/entries') - ->assertUnauthorized(); + ->getJson('/api/collections/articles/entries') + ->assertUnauthorized() + ->assertHeader('WWW-Authenticate', 'Bearer') + ->assertExactJson(['message' => 'Unauthenticated.']); + } + + #[Test] + public function it_returns_json_for_unauthenticated_requests() + { + Facades\Config::set('statamic.api.auth_token', 'foobar'); $this - ->get($url) - ->assertUnauthorized(); + ->withHeader('Accept', 'text/html') + ->get('/api/collections/articles/entries') + ->assertUnauthorized() + ->assertHeader('Content-Type', 'application/json') + ->assertHeader('WWW-Authenticate', 'Bearer') + ->assertExactJson(['message' => 'Unauthenticated.']); } #[Test]