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); } }