Use a request header with HTTP Client to external Api server - php

Consider the following request to a Symfony controller:
http http://127.0.0.1:8000/index x-token:1000
#[Route('/index', name: 'index')]
public function index(HttpClientInterface $client, Request $request): Response
{
$client->request('GET', 'http://0.0.0.0:3001', ['headers' => ['x-token' => $request->headers->get('x-token')]]);
return new JsonResponse();
}
This code snippet is a minimal example for the usage in a controller. The controller accepts a Request, and uses the x-token header for authenticating against the 3rd Party Api (here: localhost:3001).
Is there a way, to automate this process? So basically - listen to incoming requests and inject the x-token header into a specific Scoped Client or the default client in Symfony.
The goal is, not to do this in every usage of the Http Client, but have a configured client service.
The client will be used all over the codebase, not just in a controller like in this minimal example.
I know that I can use Service Decoration and extend the clients in use. I fail how to connect the dots and make this work.

Have you tried using symfony kernel events?
First of all, if you are calling some 3rd-party api, I'd suggest you to create a separate class at the infrastructure layer, for example MyApiProvider. Using HttpClient right from your controller is not smart, because you may also want to adjust something (for example api host, etc). So it's gonna look like this:
<?php
namespace App\Infrastructure\Provider;
class MyApiProvider
{
// Of course, this also be better configurable via your .env file
private const HOST = 'http://0.0.0.0:3001';
private HttpClientInterface $client;
private ?string $token = null;
public function __construct(HttpClientInterface $client)
{
$this->client = $client;
}
public function setToken(string $token): void
{
$this->token = $token;
}
public function getSomething(): array
{
$response = $this->client->request(
'GET',
self::HOST,
['headers' => $this->getHeaders()]
);
return $response->toArray();
}
private function getHeaders(): array
{
$headers = [];
if ($this->token !== null) {
$headers['x-token'] = $this->token;
}
return $headers;
}
}
Then you need to use symfony's kernel.request event to inject token to your provider from the request:
<?php
namespace App\Event;
use Symfony\Component\HttpKernel\Event\KernelEvent;
class RequestTokenEventListener
{
private MyApiProvider $provider;
public function __construct(MyApiProvider $provider)
{
$this->provider = $provider;
}
public function onKernelController(KernelEvent $event): void
{
$request = $event->getRequest();
$token = $request->headers->get('x-token');
if ($token !== null) {
$this->provider->setToken($token);
}
}
}
And finally your controller:
#[Route('/index', name: 'index')]
public function index(MyApiProvider $provider): Response
{
$provider->getSomething();
return new JsonResponse();
}
So your provider is gonna have token context during each request, if the token is passed.

Related

How to use Laravel's service provider for external API usage which utilizes user based credentials

So I'm working on a Laravel admin application, which consumes an external API, let's call it PlatformAPI. The way Platform works, is that the users of my application have an account on Platform. My Laravel application will function as a admin dashboard, so the users can view some basic reports, which are fetched from PlatformAPI.
Every user in my application has to add their client ID and client secret, which they can create in Platform. In this way, my application will be able to perform requests to PlatformAPI on their behalf, using the users' credentials.
I've read some articles and tutorials on basically setting up the credentials/tokens for a Service and bind that service to the ServiceProvider like so:
<?php
namespace App\Services\PlatformAPI;
class Client
{
protected string $clientId;
protected string $clientSecret;
public function __construct(string $clientId, string $clientSecret)
{
$this->clientId = $clientId;
$this->clientSecret = $clientSecret;
}
public function getSales(string $month)
{
// ...
}
}
<?php
use App\Services\PlatformApi\Client;
class PlatformApiServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(Client::class, function ($app) {
return new Client(
clientId: config('services.platform-api.client-id'),
clientSecret: config('services.platform-api.client-secret'),
);
});
}
}
This way, you wouldn't have to set the client credentials each time you want to consume PlatformApi and I could call the service like so:
<?php
namespace App\Http\Controllers;
use App\Services\PlatformApi\Client;
class RandomController extends Controller
{
protected Client $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function index()
{
$this->client->getSales(string $month);
}
}
However, since I need to perform requests to PlatformApi on behalf of my application's users, with the credentials that they have provided (and are stored in my application's database), I'm not sure whether this same approach would work, since a singleton only instances once?
Also, in order to consume PlatformApi, I need to get the access token, using the users' credentials. This access token will need to be stored somewhere as well (I'm thinking in the cache).
I'm kinda stuck on how to approach this. Any pointers would be much appreciated.
I assume all of your application will be using this Client service. If so, you can keep using the singleton design pattern for it (to stop further oauth requests), but try to separate the logic from the provider register method. You can instanciate the Client class after calling a private method that returns a valid access_token (checks the DB / Cache if there's a valid token by the expires_in timestamp value and returns it, or requests a new one with the user client/secret and returns it)
/**
* Register any application services.
*
* #return void
*/
public function register(): void
{
$this->app->singleton(Client::class, function () {
return new Client(
accessToken: $this->getUserToken()->access_token
);
});
}
/**
* Tries to get the user token from the database.
*
* #return ClientCredentials
*/
private function getUserToken(): ClientCredentials
{
$credentials = ClientCredentials::query()
->latest()
->find(id: auth()->user()->id);
if ($credentials === null || now() > $credentials->expires_at) {
$credentials = $this->requestUserToken();
}
return $credentials;
}
/**
* Requests a new token for the user & stores it in the database.
*
* #return ClientCredentials
*/
private function requestUserToken(): ClientCredentials
{
$tokenResponse = API::requestToken(
client: auth()->user()->client,
secret: auth()->user()->secret,
);
return ClientCredentials::query()->create(
attributes: [
'user_id' => auth()->user()->id,
'access_token' => $tokenResponse['access_token'],
'refresh_token' => $tokenResponse['refresh_token'],
'token_type' => 'Bearer',
'expires_at' => new DateTime(datetime: '+' . $tokenResponse['expires_in'] . ' seconds')
],
);
}

Send responseon EventSubscriber Symfony 3.4

I'm trying to set a response on an eventsubscriber that checks if an API authorization token it's correct
class TokenSubscriber implements EventSubscriberInterface
{
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if ($controller[0] instanceof TokenAuthenticatedController) {
$apiKey = $this->em->getRepository('AppBundle:ApiKey')->findOneBy(['enabled' => true, 'name' => 'apikey'])->getApiKey();
$token = $event->getRequest()->headers->get('x-auth-token');
if ($token !== $apiKey) {
//send response
}
}
}
public static function getSubscribedEvents()
{
return [
KernelEvents::CONTROLLER => 'onKernelController',
];
}
}
But I cant stop the current request and return a respone as a controller, what is the correct way to send a response with an error message and stop the current request
You can not do that using the FilterControllerEvent Event. On that moment, symfony already decided which controller to execute. I think you might want to look into the Symfony Security component. It can protect routes like what you want, but in a slightly different way (access_control and/or annotations).
If you want to block access to an API (eg. JSON), you easily follow this doc. You can also mix it using the Security annotations on your controllers or actions using this doc
I think you can throw an error here
throw new AccessDeniedHttpException('Your message here!');

Action executing twice when middleware is added Slim framework

I'm building a simple app based on the excellent Slim 4 Tutorial https://odan.github.io/2019/11/05/slim4-tutorial.html
The structure is still fairly similar to the tutorial.
Every time I call any of my endpoints using Postman it executes my Actions twice (ie: \App\Action\UserGetAction) for a Get. I can confirm this is happening in my logs.
if I comment out the AuthMiddleware file in config/middleware.php the duplicate action stops happening.
Any ideas?
I have created my own Auth middleware and added it in the config/middleware.php file:
use Selective\BasePath\BasePathMiddleware;
use Slim\App;
use Slim\Middleware\ErrorMiddleware;
use SlimSession\Helper;
use App\Factory\SessionFactory;
use App\Factory\AuthMiddleware;
return function (App $app) {
// Parse json, form data and xml
$app->addBodyParsingMiddleware();
// Add the Slim built-in routing middleware
$app->addRoutingMiddleware();
$app->add(BasePathMiddleware::class);
// Catch exceptions and errors
$app->add(ErrorMiddleware::class);
$app->add(AuthMiddleware::class); // <--- here
$loggerFactory = $app->getContainer()->get(\App\Factory\LoggerFactory::class);
$logger = $loggerFactory->addFileHandler('error.log')->createInstance('error');
$errorMiddleware = $app->addErrorMiddleware(true, true, true, $logger);
};
for simplicity I have stripped out pretty much everything in the AuthMiddleware:
namespace App\Factory;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
use GuzzleHttp\Client;
use Exception;
use App\Factory\LoggerFactory;
class AuthMiddleware
{
/**
* #var LoggerInterface
*/
private $logger;
public function __construct(LoggerFactory $logger)
{
$this->logger = $logger
->addFileHandler('user_edit.log')
->addConsoleHandler()
->createInstance('user_edit');
}
/**
* Example middleware invokable class
*
* #param ServerRequest $request PSR-7 request
* #param RequestHandler $handler PSR-15 request handler
*
* #return Response
*/
public function __invoke(Request $request, RequestHandler $handler): Response
{
$response = $handler->handle($request);
$headers = $request->getHeaders();
$eauth = $headers["Authorization"][0];
$this->logger->info($eauth);
return $handler->handle($request);
}
} //Class
here are my routes in config/routes.php:
$app->post('/users', \App\Action\UserCreateAction::class);
$app->map(['PUT', 'PATCH'],'/users/{id}[/{system}]', \App\Action\UserUpdateAction::class);
$app->delete('/users/{id}[/{system}]', \App\Action\UserDeleteAction::class);
$app->get('/users/{id}[/{system}]', \App\Action\UserGetAction::class);
$app->get('/extid/{id}[/{system}]', \App\Action\ExtidGetAction::class);
I have confirmed that I'm only running $app-run() once --- at the end of my index.php
require __DIR__ . '/../config/bootstrap.php';
$app->run();
There is error in __invoke method of your middleware.
You call handler queue twice:
public function __invoke(Request $request, RequestHandler $handler): Response
{
// here you call handler queue first time
// so you get response
$response = $handler->handle($request);
$headers = $request->getHeaders();
$eauth = $headers["Authorization"][0];
$this->logger->info($eauth);
// here you call handle queue for the second time
// so you get the second duplicated version of response
// this is the reason why you actions called twice
return $handler->handle($request);
}
One of the solutions is below:
public function __invoke(Request $request, RequestHandler $handler): Response
{
// here you call handler queue first time
// so you get response
$response = $handler->handle($request);
$headers = $request->getHeaders();
$eauth = $headers["Authorization"][0];
$this->logger->info($eauth);
// return response you already have
return $response;
}

Guzzle/Curl weird error when sending twice

I'm having problem sending on the second request. I tried to use Curl but the problem still persist.
I have a class that use guzzle to call to an existing api it look like this.
class HttpCall
{
public static function request($method, $endpoint, $payload)
{
$client = new Client();
$response = $client->request(...);
return $response;
}
}
Then I have a service class that use the HttpCall to fetch some data on another server
The flow is like this
Search Name (Request to another endpoint) > Update Data (Request to another endpoint)
so this two flow search name and update data each request a token to the server
Authenticate (Request to login and get token) > Search Name
Authenticate (Request to login and get token) > Update data
My service class is like this
class MyService
{
public function searchName($name)
{
$request = HttpCall::request(...);
return $request;
}
public function updateData($payload)
{
$request = HttpCall::request(...);
return $request;
}
}
then in my Class that actually interact with the events
class MyClass
{
public function __construct(MyService $service)
{
$this->service = $service;
}
public function update()
{
// When I remove this, It's working and hardcoded some data
$data = $this->service->searchName('test');
$updateData = [...];
$this->service->updateData($updateData);
}
}
I'm not quite sure what's happening why the request seem's to fail (sometimes)
Thanks for help guys

Laravel cookies not available in service provider during unit testing

I have a service provider which instantiates a CartCookie class which generates a unique cookie for saving shopping carts. It's a singleton class and it's injected into the service container.
CartCookieServiceProvider.php
public function boot(Request $request)
{
$this->app->singleton(CartCookie::class, function ($app) use ($request) {
return new CartCookie($request);
});
}
CartCookie.php
use App\Cart;
use Illuminate\Http\Request;
class CartCookie
{
private $id;
private $request;
function __construct(Request $request)
{
$this->request = $request;
if ($request->cookie('cart_id')) {
$this->id = $request->cookie('cart_id');
} else {
$this->id = $this->generateUniqueCartId();
}
}
public function id()
{
return $this->id;
}
private function generateUniqueCartId()
{
do {
$id = md5(time() . 'cart' . rand(100000000000000, 9999999999999999));
} while (Cart::find($id));
return $id;
}
}
In the CartCookie class I check for the existence of a cart_id cookie. Works perfectly fine when using the application!
My issue is that during unit tests, the cart_id cookie is empty, but only when the Request comes from the service provider. If I obtain the Request from a Controller later on in the lifecycle for example, the cookie is present.
Here is an example of a test:
/** #test */
public function get__store_checkout__checkout_displays_database_cart_correctly()
{
$cart = $this->createDatabaseCart();
$cookie = ['cart_id' => Crypt::encrypt($this->cartCookie)];
$response = $this->call('get', route('root.store.checkout'), [
'seller_id' => $cart->seller->id,
], $cookie);
$cart->seller->items()->each(function ($item) use ($response) {
$this->assertContains($beat->item, $response->getContent());
});
}
I can tell the existence when I dd() the request cookies in both the service provider and the controller that handles the cart functionality. For some reason, only during unit tests, the request doesn't contain the cookie yet in the service provider.
Hope this makes sense.
From here: link
Try:
/** #test */
public function get__store_checkout__checkout_displays_database_cart_correctly()
{
$cart = $this->createDatabaseCart();
$cookie = ['cart_id' => Crypt::encrypt($this->cartCookie)];
//#TODO you must get the current request
//#TODO you must set $cookie to $request
//Or simply find a way to create the CartCookie you need using the $cookie from above
$cartCookie = new CartCookie($request);
//hopefully will swap the CartCookie::class instance
app()->instance(CartCookie::class, $cartCookie);
//Now that you have the CartCookie
$response = $this->call('get', route('root.store.checkout'), [
'seller_id' => $cart->seller->id,
], $cookie);
$cart->seller->items()->each(function ($item) use ($response) {
$this->assertContains($beat->item, $response->getContent());
});
}

Categories