PHP - Guzzle Middleware - php

I'm using the Pole Emploi's API,but I encounter 401 error 25 minutes later, when my token expires.
I looked for a way to get a new token and retry the request, but no way for me to understand how Middlewares work, and if I should use a middleware for my needings.
On Guzzle's docs this is written :
Middleware functions return a function that accepts the next handler to invoke. This returned function then returns another function that acts as a composed handler-- it accepts a request and options, and returns a promise that is fulfilled with a response. Your composed middleware can modify the request, add custom request options, and modify the promise returned by the downstream handler.
And this is an example code from the docs :
use Psr\Http\Message\RequestInterface;
function my_middleware()
{
return function (callable $handler) {
return function (RequestInterface $request, array $options) use ($handler) {
return $handler($request, $options);
};
};
}
So I think I need to manage the "promise" to see if its HTTP code is 401, and then get a new token and retry the request ?
I'm lost, so I would appreciate if someone can explain me the logic of this with different words maybe :)
Thank you in advance.

It doesn't need to be that difficult, add a handler that takes care of the job, in combination with cache that expires.
If you don't use cache then I guess you could probably save it to a file along with a timestamp for expiration that you check against when fetching it.
class AuthenticationHandler
{
private $username;
private $password;
private $token_name = 'access_token';
public function __construct($username, $password)
{
$this->username = $username;
$this->password = $password;
}
public function __invoke(callable $handler)
{
return function (RequestInterface $request, array $options) use ($handler) {
if (is_null($token = Cache::get($this->token_name))) {
$response = $this->getJWT();
Cache::put($this->token_name, $token = $response->access_token, floor($response->expires_in));
}
return $handler(
$request->withAddedHeader('Authorization', 'Bearer '.$token)
->withAddedHeader('Api-Key', $this->api_key), $options
);
};
}
private function getJWT()
{
$response = (new Client)->request('POST', 'new/token/url', [
'form_params' => [
'grant_type' => 'client_credentials',
'username' => $this->username,
'password' => $this->password,
],
]);
return json_decode($response->getBody());
}
}
Then use it:
$stack = HandlerStack::create(new CurlHandler());
$stack->push(new AuthenticationHandler('username', 'password'));
$client = new GuzzleHttp\Client([
'base_uri' => 'https://api.com',
'handler' => $stack,
]);
Now you will always have a valid token, and you will never have to worry about it ever again.

I wouldn't recommend doing this as it can become hell to debug your application and as far as I am aware Guzzle doesn't really allow access to the client from middleware. Regardless you can use Promises to get around. If I were you I would refresh token before other requests, or refresh periodically. It might be fine if you are firing requests one by one, but in a Pool it will become a nightmare because you can end up having script fetch token too often and then some request ends up with out-dated token.
Anyway here is a rough example:
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
function my_middleware()
{
return function (callable $handler) {
return function (RequestInterface $request, array $options) use ($handler) {
/**
* #var $promise \GuzzleHttp\Promise\Promise
*/
$promise = $handler($request, $options);
return $promise->then(
function (ResponseInterface $response) use ($request, $options) {
if ($response->getStatusCode() === 404) {
var_dump($response->getStatusCode());
var_dump(strlen($response->getBody()));
// Pretend we are getting new token key here
$client = new Client();
$key = $client->get('https://www.iana.org/domains/reserved');
// Then we modify the failed request. For your case you use ->withHeader() to change the
// Authorization header with your token.
$uri = $request->getUri();
$uri = $uri->withHost('google.com')->withPath('/');
// New instance of Request
$request = $request->withUri($uri);
// Send the request again with our new header/URL/whatever
return $client->sendAsync($request, $options);
}
return $response;
}
);
};
};
}
$handlerStack = HandlerStack::create();
$handlerStack->push(my_middleware());
$client = new Client([
'base_uri' => 'https://example.org',
'http_errors' => false,
'handler' => $handlerStack
]);
$options = [];
$response = $client->request('GET', '/test', $options);
var_dump($response->getStatusCode());
var_dump(strlen($response->getBody()));
echo $response->getBody();

Related

Why GuzzleHttp client throws ClientException when using it to make network request on Laravel/Lumen?

I am currently building a Financial micro service application using Laravel/Lumen micro framework.Everything have been working perfectly as expected. My problem now is that i am trying to make a network request to my internal services via Api call from ApiGateway using GuzzleHttp client. The problem is that when i make request to the internal service, it always throws an exception of ClientException.
ClientException.
Client error: GET http://127.0.0.1:8081/v1/admin resulted in a 401
Unauthorized response: {"error":"Unauthorized.","code":401}
I have tried to make network request to the same internal services using postman; and it works fine. However, for some reason still fail to work with GuzzleHttp. I don't know what i am doing wrong. Please your assist will be appreciated.
Here is the httpClient.php in ApiGateway.
//Constructor method
public function __construct() {
$this->baseUri = config('services.auth_admin.base_uri');
}
public function httpRequest($method, $requestUrl, $formParams = [], $headers = []) {
//Instantiate the GazzleHttp Client
$client = new Client([
'base_uri' => $this->baseUri,
]);
//Send the request
$response = $client->request($method, $requestUrl, ['form_params' => $formParams, 'headers' => $headers]);
//Return a response
return $response->getBody();
}
//Internal Service Communication in ApiGateway**
public function getAdmin($header) {
return $this->httpRequest('GET', 'admin', $header);
}
InternalServiceController.php
public function getAdmin(Request $request) {
return $this->successResponse($this->authAdminService->getAdmin($request->header()));
}
I am using Lumen version: 5.8 and GuzzleHttp Version: 6.3
You pass your headers as formParams (third index instead of fourth).
Try below:
return $this->httpRequest('GET', 'admin', [], $header);
I am making some assumptions here which I hope should be helpful to you.
PHP does not support skipping optional parameters and thus you should pass an empty array [] when calling httpRequest().
public function httpRequest($method, $requestUrl, $formParams = [], $headers = [], $type='json', $verify = false) {
//Instantiate the GazzleHttp Client
$client = new Client([
'base_uri' => $this->baseUri,
]);
//the request payload to be sent
$payload = [];
if (!$verify) {
$payload['verify'] = $verify; //basically for SSL and TLS
}
//add the body to the specified payload type
$payload[$type] = $formParams;
//check if any headers have been passed and add it as well
if(count($headers) > 0) {
$payload['headers'] = $headers;
}
//Send the request
$response = $client->request($method, $requestUrl, $payload);
//Return a response
return $response->getBody();
}
Now you need to call it in this manner when you are not passing in any form_params or body
//Internal Service Communication in ApiGateway**
public function getAdmin($header) {
return $this->httpRequest('GET', 'admin', [], $header);
}

Symfony Oauth2 with gard

I am trying to configure guard with an OAuth 2 connection.
I am trying to do this with a redirection in the getCredentials function to the Microsoft login website but I can't make it work. I don't know how I can make it worked.
It seems there is no redirection possible in this function.
public function getCredentials(Request $request)
{
$provider = new Microsoft([
'clientId' => '0000000032624',
'clientSecret' => 'my-secret',
'redirectUri' => 'https://mysite/oauthlogin'
]);
if(!$request->query->has('code')){
// If we don't have an authorization code then get one
$authUrl = $provider->getAuthorizationUrl();
$request->getSession()->set('oauth2state', $provider->getState());
//This doesn't work
return new RedirectResponse($authUrl);
// Check given state against previously stored one to mitigate CSRF attack
}elseif ( empty($request->query->get('state')) || ($request->query->get('state')!==$request->getSession()->get('oauth2state')) ){
return null;
}else{
// Try to get an access token (using the authorization code grant)
$token = $provider->getAccessToken('authorization_code', [
'code' => $request->query->get('code')
]);
try {
//when log with microsoft, check if user is allowed
// We got an access token, let's now get the user's details
$user = $provider->getResourceOwner($token);
} catch (Exception $e) {
// Failed to get user details
}
}
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
return $userProvider->loadUserByUsername($user->getEmail());
}
public function checkCredentials($credentials, UserInterface $user)
{
// check credentials - e.g. make sure the password is valid
// no credential check is needed in this case
// return true to cause authentication success
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$url = $this->router->generate('homepage');
return new RedirectResponse($url);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$data = array(
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
// or to translate this message
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
);
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $data);
$url = $this->router->generate('login');
return new RedirectResponse($url);
}
Function getCredentials() is not supposed to return a Response, it provide the credentials used in getUser().
In the getUser() documentation :
The credentials are the return value from getCredentials()
You may throw an AuthenticationException if you wish. If you return
null, then a UsernameNotFoundException is thrown for you.
In case of exception thrown, onAuthenticationFailure() is called and here you can return your RedirectResponse.
For more detailled informations, see the source code of the \Symfony\Component\Security\Guard\GuardAuthenticatorInterface which contains a lots of explanations in its methods.

XML Raw Response using ebay-sdk-php

I want to get Raw XML Response from this code. But I am getting Object Representation. I like to store the XML Response in a file. I hope there is an workaround.
<?php
//REQUIRED FILES INCLUSION
require_once(__DIR__.'/../../vendor/autoload.php');
require_once(__DIR__.'/../../../Config/Config.php');
//require_once(__DIR__.'/../../../Helper.php');
//NAMESPACE
use \DTS\eBaySDK\Constants;
use \DTS\eBaySDK\Trading\Services;
use \DTS\eBaySDK\Trading\Types;
use \DTS\eBaySDK\Trading\Enums;
//SERVICE CREATION
$Service = new Services\TradingService([
'credentials' => $Config['production']['credentials'],
'sandbox' => false,
'siteId' => Constants\SiteIds::MOTORS,
'httpOptions' => [
'verify' => false
]
]);
//CATEGORY PARAMETERS
$Parameters=array(
//'DetailLevel' => array('ItemReturnCategories'),
'DetailLevel' => array('ReturnAll'),
'WarningLevel' => 'High'
);
//REQUEST
$Request = new Types\GetCategoriesRequestType($Parameters);
$Request->RequesterCredentials = new Types\CustomSecurityHeaderType();
$Request->RequesterCredentials->eBayAuthToken = $Config['production']['authToken'];
$Response = $Service->getCategories($Request);
print_r($Response);
It is possible to pass your own HTTP handler to the SDK via the httpHandler configuration option. This means you can intercept the raw response body before letting the SDK parse it.
The example below shows how to create a simple handler that uses Guzzle to send and process the response. The class is able to save it to a file that you specify. This is better than using the toRequestXml method as that does not give you the actual XML sent by eBay. It gets the object to generate the XML and therefore will be different to the eBay response.
<?php
require __DIR__.'/vendor/autoload.php';
$config = require __DIR__.'/configuration.php';
use \DTS\eBaySDK\Constants;
use \DTS\eBaySDK\Trading\Services;
use \DTS\eBaySDK\Trading\Types;
use \DTS\eBaySDK\Trading\Enums;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class ResponseLogger
{
private $client;
private $logPath;
public function __construct($logPath)
{
$this->logPath = $logPath;
$this->client = new Client();
}
/**
* This will be called by the SDK and will handle sending the request to the API
* Because of this it will be able to handle saving the response to a file.
*/
public function __invoke(RequestInterface $request, array $options)
{
return $this->client->sendAsync($request)->then(
function (ResponseInterface $response) use ($request) {
$stream = $response->getBody();
file_put_contents($this->logPath, $stream);
/**
* We have to rewind to the start of the steam before giving back to the SDK to process!
* If we don't the SDK will try and parse from the end of the response body.
*/
$stream->rewind();
return $response;
}
);
}
}
$service = new Services\TradingService([
'credentials' => $config['production']['credentials'],
'authToken' => $config['production']['authToken'],
'siteId' => Constants\SiteIds::MOTORS,
'httpHandler' => new ResponseLogger(__DIR__.'/categories.xml')
]);
$response = $service->getCategories(
new Types\GetCategoriesRequestType([
'DetailLevel' => ['ReturnAll'],
'WarningLevel' => 'High'
])
);
if (isset($response->Errors)) {
foreach ($response->Errors as $error) {
printf(
"%s: %s\n%s\n\n",
$error->SeverityCode === Enums\SeverityCodeType::C_ERROR ? 'Error' : 'Warning',
$error->ShortMessage,
$error->LongMessage
);
}
}
I haven't used this package before, but looking at the code on GitHub it looks like \DTS\eBaySDK\Trading\Services\TradingService::getCategories returns an instance of \DTS\eBaySDK\Types\BaseType which contains a method called toRequestXml which you might be able to use.
From GitHub:
/**
* Converts the object to a XML request string.
*
* #return string The XML request string.
*/
public function toRequestXml()
{
return $this->toXml(self::$requestXmlRootElementNames[get_class($this)], true);
}

Mock response and use history middleware at the same time in Guzzle

Is there any way to mock response and request in Guzzle?
I have a class which sends some request and I want to test.
In Guzzle doc I found a way how can I mock response and request separately. But how can I combine them?
Because, If use history stack, guzzle trying to send a real request.
And visa verse, when I mock response handler can't test request.
class MyClass {
public function __construct($guzzleClient) {
$this->client = $guzzleClient;
}
public function registerUser($name, $lang)
{
$body = ['name' => $name, 'lang' = $lang, 'state' => 'online'];
$response = $this->sendRequest('PUT', '/users', ['body' => $body];
return $response->getStatusCode() == 201;
}
protected function sendRequest($method, $resource, array $options = [])
{
try {
$response = $this->client->request($method, $resource, $options);
} catch (BadResponseException $e) {
$response = $e->getResponse();
}
$this->response = $response;
return $response;
}
}
Test:
class MyClassTest {
//....
public function testRegisterUser()
{
$guzzleMock = new \GuzzleHttp\Handler\MockHandler([
new \GuzzleHttp\Psr7\Response(201, [], 'user created response'),
]);
$guzzleClient = new \GuzzleHttp\Client(['handler' => $guzzleMock]);
$myClass = new MyClass($guzzleClient);
/**
* But how can I check that request contains all fields that I put in the body? Or if I add some extra header?
*/
$this->assertTrue($myClass->registerUser('John Doe', 'en'));
}
//...
}
#Alex Blex was very close.
Solution:
$container = [];
$history = \GuzzleHttp\Middleware::history($container);
$guzzleMock = new \GuzzleHttp\Handler\MockHandler([
new \GuzzleHttp\Psr7\Response(201, [], 'user created response'),
]);
$stack = \GuzzleHttp\HandlerStack::create($guzzleMock);
$stack->push($history);
$guzzleClient = new \GuzzleHttp\Client(['handler' => $stack]);
First of all, you don't mock requests. The requests are the real ones you are going to use in production. The mock handler is actually a stack, so you can push multiple handlers there:
$container = [];
$history = \GuzzleHttp\Middleware::history($container);
$stack = \GuzzleHttp\Handler\MockHandler::createWithMiddleware([
new \GuzzleHttp\Psr7\Response(201, [], 'user created response'),
]);
$stack->push($history);
$guzzleClient = new \GuzzleHttp\Client(['handler' => $stack]);
After you run your tests, $container will have all transactions for you to assert. In your particular test - a single transaction. You are interested in $container[0]['request'], since $container[0]['response'] will contain your canned response, so there is nothing to assert really.

Symfony 2.8 WebTestCase: Cannot create logged-in session

I am currently working on unit testing my symfony 2.8 based admin area.
So I wrote a small basic test for the dashboard: The tests checks that
a) If the user is currently not logged in, there should be a redirect to the login page.
b) If the user is logged in, the dashboard should be shown.
In order to "log in" the user ( = to create an active session) I came up with a small helper function that is based on the respective cookbook article from the symfony documentation: How to Simulate Authentication with a Token in a Functional Test
This however does not seem to work. My initial tests fails. So I have added another request to a dummy controller that only prints out session information. This shows that while the seems to be set correctly, the current user information and the security token storage seem to be incorrect - as I get the default "AnonymousToken" and a null return from the getUser method.
Here is my test code:
<?php
namespace GOC\Bundle\AdminBundle\Tests\Controller;
use FOS\RestBundle\Util\Codes;
use Symfony\Bundle\FrameworkBundle\Client;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use GOC\Bundle\FrameworkBundle\Model\ContextInterface;
use GOC\Bundle\FrameworkBundle\Tests\WebTestCase;
class DashboardControllerTest extends WebTestCase
{
const BASE_URL = '';
/**
* #var ContextInterface|null
*/
protected $context;
public static function setUpBeforeClass()
{
$client = static::createClient([
'environment' => 'test',
'debug' => false,
], [
'HTTP_HOST' => 'demo-cms.dev',
]);
$container = $client->getContainer();
$kernel = $container->get('kernel');
$application = new Application($kernel);
$application->setAutoExit(false);
$input = new ArrayInput(array(
'command' => 'doctrine:mongodb:fixtures:load',
));
$output = new NullOutput();
$application->run($input, $output);
}
/**
* Testing whether the backend is not publicly accessible
*/
public function testFirewallRedirect()
{
$client = static::createClient();
$client->request('GET', $this->buildUrl('/admin/dashboard'));
$response = $client->getResponse();
$this->assertTrue($client->getResponse()->isRedirect($this->buildUrl('http://localhost/admin/login')));
$crawler = $client->request('GET', $response->headers->get('location'));
$this->assertEquals(
1,
$crawler->filter('html:contains("Willkommen")')->count()
);
}
/**
* Testing whether the backend is not publicly accessible
*/
public function testFirewallAccess()
{
$client = static::createClient([
'environment' => 'test',
'debug' => false,
], [
'HTTP_HOST' => 'demo-cms.dev',
]);
$this->logIn($client);
$client->request('GET', $this->buildUrl('/admin/dashboard'));
$response = $client->getResponse();
// This fails...
//$this->assertEquals(Codes::HTTP_OK, $response->getStatusCode());
// Debug statements
$client->request('GET', $this->buildUrl('/forgot-password-sent'));
$response = $client->getResponse();
dump($response);
}
/**
* #param Client $client
*/
protected function logIn(Client $client)
{
$container = $client->getContainer();
$repository = $container->get('goc_account.user_manager')->getRepository();
$user = $repository->getUserByUsername('admin#gardenofconcepts.com', $this->getContext($client));
$firewall = 'main';
$token = new UsernamePasswordToken($user, null, $firewall, $user->getRoles());
$container->get('security.token_storage')->setToken($token);
$session = $container->get('session');
// Saw this somewhere else, makes no difference though
//$session = new Session(new MockArraySessionStorage());
//$session->start();
//$container->set('session', $session);
$session->set('_security_'.$firewall, serialize($token));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
$client->getCookieJar()->set($cookie);
}
/**
* #param $url
*
* #return string
*/
protected function buildUrl($url)
{
return $this::BASE_URL . $url;
}
/**
* #param Client $client
*
* #return ContextInterface
*/
protected function getContext(Client $client)
{
if ($this->context) {
return $this->context;
}
$this->context = $client->getContainer()->get('goc_framework.context_manager')->getContextByName('demo');
return $this->context;
}
}
Thanks in advance for any help - it's highly appreciated !
We are not trying to mock the token but actually create one with the following function (which might be adaptable to your situation):
protected function createAuthenticatedClient($username, $password)
{
$client = static::createClient();
$client->request(
'POST',
'/api/v1/login_check',
array(
'username' => $username,
'password' => $password,
)
);
$data = json_decode($client->getResponse()->getContent(), true);
$client = static::createClient();
$client->setServerParameter('HTTP_Authorization', sprintf('Bearer %s', $data['token']));
return $client;
}
After using this like
$client = $this->createAuthenticatedClient('user', 'password');
every request sent will be attached with our actual Bearer.

Categories