CaiWuXiTong/app/Http/Middleware/AddWWWAuthenticateHeader.php
WuYongJie 9595267ee7
Some checks failed
Tests / PHP 8.1 (push) Has been cancelled
Tests / PHP 8.2 (push) Has been cancelled
Tests / PHP 8.3 (push) Has been cancelled
chore: 导入 Akaunting 初始源码与团队协作规则
2026-07-20 18:20:25 +08:00

119 lines
4.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class AddWWWAuthenticateHeader
{
/**
* Handle an incoming request.
*
* MCP REQUIRED: Add WWW-Authenticate header to 401 responses.
* This tells MCP clients where to find the OAuth discovery metadata.
*
* Reference: https://datatracker.ietf.org/doc/html/rfc9728#section-5.1
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
// Skip if OAuth module is disabled to avoid unnecessary processing
if (! config('oauth.enabled', false)) {
return $next($request);
}
$response = $next($request);
// Only add header to 401 Unauthorized responses
if ($response->getStatusCode() === 401) {
$this->addWWWAuthenticateHeader($response, $request);
}
return $response;
}
/**
* Add WWW-Authenticate header to response.
*
* @param \Symfony\Component\HttpFoundation\Response $response
* @param \Illuminate\Http\Request $request
* @return void
*/
protected function addWWWAuthenticateHeader(Response $response, Request $request): void
{
// Use the named route if available (registered in MCP ai.php routes).
// The MCP package's AddWwwAuthenticateHeader middleware also checks for this
// same named route, so using it here keeps both middlewares consistent.
if (app('router')->has('mcp.oauth.protected-resource')) {
$resourceMetadataUrl = route('mcp.oauth.protected-resource', ['path' => $request->path()]);
} else {
$resourceMetadataUrl = route('oauth.well-known.protected-resource', ['path' => $request->path()]);
}
$realm = config('app.name', 'Akaunting');
// If another middleware already set a challenge (e.g. Bearer+Basic),
// preserve it and only enrich with resource_metadata when Bearer exists.
$existingHeader = $response->headers->get('WWW-Authenticate');
if (! empty($existingHeader)) {
if (str_contains($existingHeader, 'resource_metadata="')) {
return;
}
if (preg_match('/\bBearer\b/i', $existingHeader) === 1) {
$response->headers->set(
'WWW-Authenticate',
$existingHeader . ', resource_metadata="' . $resourceMetadataUrl . '"'
);
}
return;
}
// RFC 6750: 'error' should only be present when a token was provided
// but failed authentication. For unauthenticated probes (no token),
// only realm and resource_metadata are needed so the client can
// initiate the OAuth flow.
if (! empty($request->bearerToken())) {
// Token was present but invalid/expired – include error details
$error = 'invalid_token';
$errorDescription = 'OAuth: The access token is invalid or has expired';
// Try to extract error from JSON response body
$contentType = $response->headers->get('Content-Type', '');
if (str_contains($contentType, 'application/json')) {
$content = json_decode($response->getContent(), true);
if (isset($content['error'])) {
$error = $content['error'];
}
if (isset($content['error_description'])) {
$errorDescription = $content['error_description'];
}
}
$headerValue = 'Bearer'
. ' realm="' . $realm . '"'
. ', resource_metadata="' . $resourceMetadataUrl . '"'
. ', error="' . $error . '"'
. ', error_description="' . addslashes($errorDescription) . '"';
} else {
// No token provided – minimal challenge so client can start OAuth
$headerValue = 'Bearer'
. ' realm="' . $realm . '"'
. ', resource_metadata="' . $resourceMetadataUrl . '"';
}
// Set the WWW-Authenticate header
$response->headers->set('WWW-Authenticate', $headerValue);
}
}