I am fighting with GuzzleHTTP inside my API. I am using Lumen to develop API for communication with HashiCorp Vault API. To do this I installed GuzzleHTTP to make API calls.
For example I am trying to generate credentials for database user based on specific role. In the commandline cURL request responses to me with credentials for new user. While using Guzzle doesn't give me anything related to new created user.
I am new into Guzzle so I will be glad for any advice.
cURL request looks like this:
curl -X GET \
--header "X-Vault-Token: $VAULT_TOKEN" \
http://192.168.1.3:8200/v1/database/creds/my-role | json_pp
And the response gives me credentials I want:
{
"request_id" : "3eacc89b57b-2e2e-af6f-849f-868bdbfd2dc5e1",
"wrap_info" : null,
"renewable" : true,
"data" : {
"username" : "randomgeneratedusername",
"password" : "randomgeneratedpassword"
},
"auth" : null,
"lease_duration" : 3600,
"warnings" : null,
"lease_id" : "database/creds/my-role/randomsecretname"
}
When my Guzzle code looks like this:
class MySQLVaultAPI extends VaultAPI
{
public function createUser() {
$response = $this->getClient()->request('GET', $this->url.'/database/creds/my-role',
['headers' => $this->headers]);
}
}
And:
class VaultAPI
{
protected string $url;
protected Client $http;
protected array $headers;
public function __construct(Client $client)
{
$this->url = 'http://192.168.1.3:8200/v1';
$this->http = $client;
$this->headers = [
'X-Vault-Token' => 'secrettoken',
'Accept' => 'application/json',
];
}
}
And the response from GuzzleHTTP(I have used symfony dumper on $response object):
^ GuzzleHttp\Psr7\Response {#43 ▼
-reasonPhrase: "OK"
-statusCode: 200
-headers: array:4 [▼
"Cache-Control" => array:1 [▶]
"Content-Type" => array:1 [▶]
"Date" => array:1 [▶]
"Content-Length" => array:1 [▶]
]
-headerNames: array:4 [▼
"cache-control" => "Cache-Control"
"content-type" => "Content-Type"
"date" => "Date"
"content-length" => "Content-Length"
]
-protocol: "1.1"
-stream: GuzzleHttp\Psr7\Stream {#21 ▼
-stream: stream resource #137 ▶}
-size: null
-seekable: true
-readable: true
-writable: true
-uri: "php://temp"
-customMetadata: []
}
}
Okey,
I have found out the answer. All of the content is in GuzzleHttp\Psr7\Stream
So I had to make additional method in my VaultAPI service
public function getFullBody(Stream $responseBody)
{
$responseBody->seek(0);
$output = json_decode(trim($responseBody->getContents()), true);
if ($output === null) {
throw new \Exception('Error parsing response JSON ('.json_last_error().')');
}
return $output;
}
And then inside my MySQLVaultAPI I am calling it
public function createUser() {
$response = $this->getClient()->request('GET', $this->url.'/database/creds/my-role',
['headers' => $this->headers]);
return $this->getFullBody($response->getBody());
}
Related
I am trying to update Xero contacts, which is part of the invoice in Laravel. I am using Xero-php-oauth2. I have tried
$xero = resolve(AccountingApi::class);
$invoice = new Invoice();
$invoice->setLineItems($lineItems);
$result = $xero->updateInvoice($tenantId, $invoice_id, $invoice);
$contactId = $result->getInvoices()[0]['contact']['contact_id'];
$contact = $this->newContact();
$contact->setContactId($contactId);
//$tenantId, $contactid, and $contact are correct
$xero->updateContact($tenantId, $contactId, $contact);
The request I am trying to send is like following, but the response returns 401 AuthenticationUnsuccessful
Request {#1160 ▼
-method: "POST"
-requestTarget: null
-uri: Uri {#1212 ▼
-scheme: "https"
-userInfo: ""
-host: "api.xero.com"
-port: null
-path: "/api.xro/2.0/Contacts/134ab308-7d07-4c2d-a770-c01325947ede"
-query: ""
-fragment: ""
}
-headers: array:6 [▼
"Host" => array:1 [▼
0 => "api.xero.com"
]
"User-Agent" => array:1 [▼
0 => "[xero-php-oauth2 (2.11.0)]"
]
"xero-tenant-id" => array:1 [▼
0 => "TENANT_ID_HERE"
]
"Accept" => array:1 [▼
0 => "application/json"
]
"Content-Type" => array:1 [▼
0 => "application/json"
]
"Authorization" => array:1 [▼
0 => "Bearer BEARER_TOKEN_HERE"
]
]
-headerNames: array:6 [▶]
-protocol: "1.1"
-stream: Stream {#1217 ▼
-stream: stream resource #755 ▼
wrapper_type: "PHP"
stream_type: "TEMP"
mode: "w+b"
unread_bytes: 0
seekable: true
uri: "php://temp"
options: []
}
-size: null
-seekable: true
-readable: true
-writable: true
-uri: "php://temp"
-customMetadata: []
}
}
xero-php-oauth2 sends request along with following options to GuzzleHttp\Client, and It seems to throw error at AccountingAPI#updateContactWithHttpInfo() in following line
$response = $this->client->send($request, $options);`
I also tried to add accounting.contacts scope before calling but it does not work.
$options = ['scope' => ['openid email profile offline_access accounting.settings accounting.transactions accounting.contacts accounting.journals.read accounting.reports.read accounting.attachments']];
Error message
[401] Client error: `POST https://api.xero.com/api.xro/2.0/Contacts/134ab308-7d07-4c2d-a770-c01325947ede` resulted in a `401 Unauthorized` response: {"Type":null,"Title":"Unauthorized","Status":401,"Detail":"AuthenticationUnsuccessful","Instance":"94b725d8-435b-4a98-be (truncated...)
It is working when I create or update invoices with almost identical request. Even identical bearer token. Also creating new contact if I pass that with new invoice. But not updating already existing contact. How can I fix this?
The answer is in the 401 response: "Title":"Unauthorized". You're not unauthenticated, you're unauthorised.
From the docs:
When your app is requesting authorisation from a user it will need to ask for a set of scopes. These scopes will be displayed to the user and describe what data your app will be able to access.
You supply the relevant scopes when requesting authentication with OAuth.
Your OAuth request (i.e. when you request authentication from the user) will be missing the accounts.contacts scope. Add it, and you should be able to successfully call those API endpoints.
I am trying to send vacancies to the Google Indexing API whenever we receive them from our vacancy provider. However, despite having set all the appropriate permissions, I keep receiving 403 status codes.
I have followed the "Prerequisites for the Indexing API" guide and created a Service account. In my case, I have 4 entries for the domain in Google Search Console, so I followed MarcQuay's answer and added the service account to all 4 entries.
I use the following code to implement the Google API Client and make my calls. The method sendGoogleRequest() is called for every vacancy that we receive.
// Google API setup
function sendGoogleRequest(array $aData, int $sStatus = 0)
{
global $DOMAIN, $GOOGLE_AUTH_CONFIG, $GOOGLE_AUTH_SCOPE, $GOOGLE_API_ENDPOINT;
$googleClient = new Google_Client();
$googleClient->useApplicationDefaultCredentials();
$googleClient->addScope([$GOOGLE_AUTH_SCOPE]);
// Update Google Indexing API. This will notify Google that the URL should be crawled again.
$httpClient = $googleClient->authorize();
$endpoint = $GOOGLE_API_ENDPOINT;
$sJobUrl = $DOMAIN . '/vacancies/' . $aData['url'];
$sType = "";
if (!empty($sStatus)) {
switch ($sStatus) {
case 1:
$sType = "URL_UPDATED";
break;
case 2:
$sType = "URL_DELETED";
break;
}
}
$content = "{
\"url\": \"$sJobUrl\",
\"type\": \"$sType\"
}";
$response = $httpClient->post($endpoint, ['body' => $content]);
$status_code = $response->getStatusCode();
return $status_code;
}
I have tried debugging it and it seems that '$credentials' is empty in $googleClient->authorize()
$authHandler = $this->getAuthHandler();
if ($credentials) {
$callback = $this->config['token_callback'];
$http = $authHandler->attachCredentials($http, $credentials, $callback);
} elseif ($token) {
$http = $authHandler->attachToken($http, $token, (array) $scopes);
} elseif ($key = $this->config['developer_key']) {
$http = $authHandler->attachKey($http, $key);
}
return $http;
However I have no idea what could be the cause of this.
Using this code returns a '403' for every call. After having browsed the internet for quite some time now, I can seem to find a definite answer, except for 'make sure the service account is an owner' but as previously stated, this is already the case.
I hope someone can help me out, since I have been stuck on this for longer than I'd like to admit.
If any more information is required I'll gladly update my answer.
EDIT: As per request, here is the full error message return by the $httpClient->post() call.
Response {#268 ▼
-reasonPhrase: "Forbidden"
-statusCode: 403
-headers: array:11 [▼
"Vary" => array:3 [▼
0 => "X-Origin"
1 => "Referer"
2 => "Origin,Accept-Encoding"
]
"Content-Type" => array:1 [▼
0 => "application/json; charset=UTF-8"
]
"Date" => array:1 [▼
0 => "Tue, 24 Sep 2019 11:25:29 GMT"
]
"Server" => array:1 [▼
0 => "ESF"
]
"Cache-Control" => array:1 [▼
0 => "private"
]
"X-XSS-Protection" => array:1 [▼
0 => "0"
]
"X-Frame-Options" => array:1 [▼
0 => "SAMEORIGIN"
]
"X-Content-Type-Options" => array:1 [▼
0 => "nosniff"
]
"Alt-Svc" => array:1 [▼
0 => "quic=":443"; ma=2592000; v="46,43,39""
]
"Accept-Ranges" => array:1 [▼
0 => "none"
]
"Transfer-Encoding" => array:1 [▼
0 => "chunked"
]
]
-headerNames: array:11 [▼
"vary" => "Vary"
"content-type" => "Content-Type"
"date" => "Date"
"server" => "Server"
"cache-control" => "Cache-Control"
"x-xss-protection" => "X-XSS-Protection"
"x-frame-options" => "X-Frame-Options"
"x-content-type-options" => "X-Content-Type-Options"
"alt-svc" => "Alt-Svc"
"accept-ranges" => "Accept-Ranges"
"transfer-encoding" => "Transfer-Encoding"
]
-protocol: "1.1"
-stream: Stream {#256 ▼
-stream: stream resource #123 ▼
wrapper_type: "PHP"
stream_type: "TEMP"
mode: "w+b"
unread_bytes: 0
seekable: true
uri: "php://temp"
options: []
}
-size: null
-seekable: true
-readable: true
-writable: true
-uri: "php://temp"
-customMetadata: []
}
}
I ended up fixing this issue by using a Service account as AuthConfig
// Initialize Google_Client to submit vacancies to Indexing API
$googleClient = new Google_Client();
$googleClient->setAuthConfig($GOOGLE_AUTH_CONFIG);
$googleClient->addScope([$GOOGLE_AUTH_SCOPE]);
$googleClient->setAccessType("offline");
$googleClient is used for every call
function sendGoogleRequest(array $aData, int $sStatus = 0, Google_Client $google_Client) {
global $DOMAIN;
$sJobUrl = 'https://MY-DOMAIN.com/' . $aData['url'];
$sType = "";
if (!empty($sStatus)) {
switch ($sStatus) {
case 1:
$sType = "URL_UPDATED";
break;
case 2:
$sType = "URL_DELETED";
break;
}
}
$urlNotification = new Google_Service_Indexing_UrlNotification();
$urlNotification->setType($sType);
$urlNotification->setUrl($sJobUrl);
$indexingService = new Google_Service_Indexing($google_Client);
$response = $indexingService->urlNotifications->publish($urlNotification);
return $response;
}
This script is then called once per day and posts every URL to the Indexing API. To not exceed the limit of 250 requests per day, make sure you exclude vacancies that should no longer be indexed.
I have been looking into grabbing some data from Livecoding.tv, and i am currently reusing my Oauth2 controller that i used for the twitch API, which should be pretty straightforward.
If someone does not know, the flow used by Oauth is the following:
Redirect user to third party Oauth link with your app code.
User Authorizes.
User redirected to your website again, with an Authorized token, that you can then post to the third party to get your refresh token etc.
Now on step 3, i am running into some problems. Here is the description and example by the developer:
Getting token: https://www.livecoding.tv/o/token/
Header
A HTTP Basic auth, using the application_code as username, and application_secret as password, as seen in the example below.
POST Body
code=EXAMPLE Token gotten from redirect
grant_type=Your grant type (authorization_type)
redirect_uri=Your redirect URL
And here is an example cURL request from the documentation of a working curl request.
curl -X POST -d "grant_type=authorization_code&code=Php4iJpXGpDT8lCqgBcbfQ2yzhB0Av&client_id=vCk6rNsC&redirect_uri=http://localhost/externalapp" -u"vCk6rNsC:sfMxcHUuNnZ" https://www.livecoding.tv/o/token/
So i tried to make this in Postman(https://www.getpostman.com/), which worked out of the box, i then asked Postman to convert this to PHP, to see if i had missed something. Here is the outcome of the working Postman request:
<?php
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "https://www.livecoding.tv/o/token/",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "code=SOMECODE&client_id=SOMECLIENTID&redirect_uri=SOMEURL&grant_type=authorization_code",
CURLOPT_HTTPHEADER => array(
"authorization: Basic U09NRVVTRVI6U09NRVBBU1NXT1JE",
"cache-control: no-cache",
"content-type: application/x-www-form-urlencoded",
"postman-token: c8df4bbc-cbd0-73eb-df35-80210989db33"
),
));
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
I was pretty happy this worked out right away, so i just needed to make a Guzzle client do the same, which already works for the Twitch Oauth, here is my code for that:
{
$providers = ServiceProvider::findOrFail(2);
$client = new Client([
'base_uri' => $providers->oauth_url . '/token/',
'form_params' => [
'code' => $token,
'grant_type' => 'authorization_code',
'client_id' => $providers->application_code,
'redirect_uri' => $providers->redirection_url
],
'auth' => [
'somestring',
'someotherstring',
],
'headers' => [
'Content type' => 'application/x-www-form-urlencoded',
'cache-control' => 'no-cache'
]
]);
$response = $client->request('POST');
return ($response);
}
This would just return me with a 401, so i decided to do some debugging, and here is the request that comes, if i stop it right before it is sent:
Client {#637 ▼
-config: array:10 [▼
"base_uri" => Uri {#676 ▼
-scheme: "https"
-userInfo: ""
-host: "www.livecoding.tv"
-port: null
-path: "/o/token/"
-query: ""
-fragment: ""
}
"form_params" => array:4 [▼
"code" => "SOMECODE"
"grant_type" => "authorization_code"
"client_id" => "SOMECLIENTID"
"redirect_uri" => "http://REDIRECTURI"
]
"auth" => array:2 [▼
0 => "SOMECLIENTID"
1 => "SOMECLIENTSECRET"
]
"headers" => array:3 [▼
"Content type" => "application/x-www-form-urlencoded"
"cache-control" => "no-cache"
"User-Agent" => "GuzzleHttp/6.2.1 curl/7.26.0 PHP/5.6.27-1~dotdeb+7.1"
]
"handler" => HandlerStack {#664 ▼
-handler: Closure {#671 ▼
class: "GuzzleHttp\Handler\Proxy"
parameters: {▼
$request: {▼
typeHint: "Psr\Http\Message\RequestInterface"
}
$options: {▼
typeHint: "array"
}
}
use: {▼
$default: Closure {#669 ▼
class: "GuzzleHttp\Handler\Proxy"
parameters: {▼
$request: {▼
typeHint: "Psr\Http\Message\RequestInterface"
}
$options: {▼
typeHint: "array"
}
}
use: {▼
$default: CurlMultiHandler {#634 ▼
-factory: CurlFactory {#667 ▼
-handles: []
-maxHandles: 50
}
-selectTimeout: 1
-active: null
-handles: []
-delays: []
}
$sync: CurlHandler {#666 ▼
-factory: CurlFactory {#665 ▼
-handles: []
-maxHandles: 3
}
}
}
file: "/LARAVELPATH/vendor/guzzlehttp/guzzle/src/Handler/Proxy.php"
line: "25 to 29"
}
$streaming: StreamHandler {#670 ▼
-lastHeaders: []
}
}
file: "LARAVELPATH/vendor/guzzlehttp/guzzle/src/Handler/Proxy.php"
line: "49 to 53"
}
-stack: array:4 [▼
0 => array:2 [▼
0 => Closure {#672 ▼
class: "GuzzleHttp\Middleware"
parameters: {▶}
file: "LARAVELPATH/vendor/guzzlehttp/guzzle/src/Middleware.php"
line: "54 to 69"
}
1 => "http_errors"
]
1 => array:2 [▼
0 => Closure {#673 ▼
class: "GuzzleHttp\Middleware"
parameters: {▶}
file: "LARAVELPATH/vendor/guzzlehttp/guzzle/src/Middleware.php"
line: "148 to 150"
}
1 => "allow_redirects"
]
2 => array:2 [▼
0 => Closure {#674 ▼
class: "GuzzleHttp\Middleware"
parameters: {▶}
file: "LARAVELPATH/vendor/guzzlehttp/guzzle/src/Middleware.php"
line: "27 to 43"
}
1 => "cookies"
]
3 => array:2 [▼
0 => Closure {#675 ▼
class: "GuzzleHttp\Middleware"
parameters: {▶}
file: "LARAVELPATH/vendor/guzzlehttp/guzzle/src/Middleware.php"
line: "216 to 218"
}
1 => "prepare_body"
]
]
-cached: null
}
"allow_redirects" => array:5 [▼
"max" => 5
"protocols" => array:2 [▼
0 => "http"
1 => "https"
]
"strict" => false
"referer" => false
"track_redirects" => false
]
"http_errors" => true
"decode_content" => true
"verify" => true
"cookies" => false
]
}
As you can see here, there is just a parameter called 'auth' in here, and not the basic auth that is documented in Guzzle here: http://docs.guzzlephp.org/en/latest/request-options.html#auth
Though i would mention that the correct values are entered.
For the debugging i just did dd($client), i do not know if this will give me all the answers?
So the other possibility is to base64 encode it (like it is done in basic auth in general), and add a "Authorization" header manually, i tried that, but i am unaware if i am doing it correctly when i do:
$credentials = base64_encode($clientvariable . ':' . $clientsecretvariable)
Would that be the correct way? Though i would rather use this as a last resort if i do not get the auth parameter to work in Guzzle.
I am aware of the other Stackoverflow questions about Guzzle and basic auth, and yes i have read them, hopefully i have provided with enough information to show that.
The problem here was related to how i return the response from Guzzle.
Since i just returned $response, which is just the stream, the browser never got anything returned, and since chrome auto refresh, he runs the request twice, and on the second time, i would have just my code twice, which would have returned an error.
I ended up going crazy about this, until i changed browser, and saw that nothing ever happened, which pointed me in the right direction.
I try to test my php form in my admin with this code :
$client = $this->createAuthorizedClient();
$crawler = $client->request('GET', '/user/edit/1');
$form = $crawler->selectButton('Save')->form();
$form['sf_guard_user[lastName]'] = 'lastname';
$crawler = $client->submit($form);
$form->get('sf_guard_user[lastName]')->setValue('Richardson');
$client->submit($form);
$response = $client->getResponse();
My function createAuthorizedClient is here :
protected function createAuthorizedClient()
{
$client = static::createClient();
$container = $client->getContainer();
$session = $container->get('session');
$userManager = $container->get('fos_user.user_manager');
$loginManager = $container->get('fos_user.security.login_manager');
$firewallName = $container->getParameter('fos_user.firewall_name');
$user = $userManager->findUserByUsername('ASSELIN');
$loginManager->logInUser($firewallName, $user);
$container->get('session')->set('_security_' . $firewallName,
serialize($container->get('security.token_storage')->getToken()));
$container->get('session')->save();
$client->getCookieJar()->set(new Cookie($session->getName(), $session->getId()));
return $client;
}
But when I launch my test I have this error in my $response :
{"code":500,"message":"A Token was not found in the TokenStorage."}"
The complete stack trace is here :
Symfony\Component\HttpFoundation\Response {#2362
+headers: Symfony\Component\HttpFoundation\ResponseHeaderBag {#2372
#computedCacheControl: array:1 [
"no-cache" => true
]
#cookies: []
#headerNames: array:3 [
"cache-control" => "Cache-Control"
"content-type" => "Content-Type"
"allow" => "Allow"
]
#headers: array:3 [
"cache-control" => array:1 [
0 => "no-cache"
]
"content-type" => array:1 [
0 => "application/json;"
]
"allow" => array:1 [
0 => "PUT"
]
]
#cacheControl: []
}
#content: "{"code":500,"message":"A Token was not found in the TokenStorage."}"
#version: "1.1"
#statusCode: 500
#statusText: "Internal Server Error"
#charset: "UTF-8"
}
I try to find a solution, but I don't understand why I have this error. Is it possible to explain me my error ? The form is submit with put method directly in the api.
Best regards,
Benoit
I fix my problem with add http_basic in my security.yml :
security:
firewalls:
wsse_secured:
stateless: false
http_basic: ~
We are required to get the live data from API URL. First we need to connect with the server, in this case $uri1, then we have to use second URI (e.g: $uri2) which will the give the output in JSON format.
What I've tried to do so is:
$un = 'abc'; //username
$pa = '123'; //password
$base_uri = 'http://example.com:82/';
$uri1 = 'api/instant/connectopc';
$uri2 = 'api/instant/displaydata?site=SITE';
$cookieFile = 'jar.txt';
$cookieJar = new FileCookieJar($cookieFile, true);
$client = new Client([
'base_uri' => $base_uri,
'auth'=>[$un, $pa],
'cookie'=>$cookieJar,
'curl' => [
CURLOPT_COOKIEJAR => 'jar.txt',
CURLOPT_COOKIEFILE => 'jar.txt'
]
]);
$promises = [
'connect' => $client->getAsync($uri1),
'live' => $client->getAsync($uri2)
];
$results = Promise\unwrap($promises);
dd($results);
//$body = $results['live'];
OUTPUT
array:2 [▼
"connect" => Response {#345 ▼
-reasonPhrase: "OK"
-statusCode: 200
-headers: array:10 [▶]
-headerLines: array:10 [▶]
-protocol: "1.1"
-stream: Stream {#336 ▶}
}
"live" => Response {#357 ▼
-reasonPhrase: "OK"
-statusCode: 200
-headers: array:10 [▶]
-headerLines: array:10 [▶]
-protocol: "1.1"
-stream: Stream {#348 ▶}
}]
Data: dd($body->getBody()->getContents()); //-----> output: null
i) First we need to connect with uri 1 after that.
ii) uri 2 will give live data in realtime.
Both URIs are connecting but the content of second URI ($uri2) is NULL.
Any help will be appreciated.
Regards,
NKR
I found the solution:
....
$connect = $client->get($uri1); //<-----this will connect to server.
//Now for second URI, use JAR file to utilize the old cookie
//to ensure the URI is using previous authentication/session.
$live = $client->get($uri2, ['cookie'=>$cookieJar]);
dd($live);