Google Analytics API V3 / OAuth 2 - google-analytics

I've desperately tried to figure this out on my own, and did not want to come to SO with this question, but I'm at my wits end (no thanks to the api / oauth docs).
I'm working in PHP and I'm trying to avoid using the Google_Client and AnalyticsService classes, by using REST on the analytics.data.ga.get method.
STEP #1: Create an API Project for a Web Application
I go to the api console and create a project with analytics services and get an OAuth Client ID and Secret.
I'm under the impression that I can create a Client ID for an Installed Application or a Web Application because I'm doing the initial token handshaking manually. Please correct me if I'm wrong.
I create a Client ID for web applications and get my Client ID xxxxxxxxxxxxxx.apps.googleusercontent.com, Client secret yyyyyyyyyyyyyyy, and my Redirect URI is http://localhost:9002
STEP #2: Request initial API access
I enter this link; https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=xxxxxxxxxxxxxx.apps.googleusercontent.com&redirect_uri=http://localhost:9002&scope=https://www.googleapis.com/auth/analytics.readonly&access_type=offline
The access_type=offline is because I'm using REST, and do not expect "the user" (myself) to manually deal with redirects / popups every time I need a refreshed token.
The above request returns http://localhost:9002?code=4/KModH0K_xxxxxxxxxxxxxxxxxxx9Iw.gikOaYRDWywTshQV0ieZDArCOX8XdwI
Code 4/KModH0K_xxxxxxxxxxxxxxxxxxx9Iw.gikOaYRDWywTshQV0ieZDArCOX8XdwI is my permission to request the API Token.
STEP #3: Request First Token
Because of my company’s IT issues, I’m forced to use PHP 5.2.17 and I do not have access to PHP cURL, so I’m using file_get_contents and stream_context_create.
The first token is requested with a PHP file_get_contents();
$opts = array(
'http' => array(
'method' => 'POST',
'header' => 'Content-Type: application/x-www-form-urlencoded',
'content' => 'code=4/KModH0K_xxxxxxxxxxxxxxxxxxx9Iw.gikOaYRDWywTshQV0ieZDArCOX8XdwI&client_id=xxxxxxxxxxxxxx.apps.googleusercontent.com&client_secret=yyyyyyyyyyyyyyy&redirect_uri=http://localhost:9002&grant_type=authorization_code'
)
);
$context = stream_context_create($opts);
$result = file_get_contents('https://accounts.google.com/o/oauth2/token', false, $context);
var_dump($result);
The content parameters must be in a single line.
The above code returns my access_token and refresh_token in json format
string(195) "{ "access_token" : "ya29.AHES6wwwwwwwwwwwwwwwVEBXE6XRbC-Q-pP0wZWdoIm9H804ro", "token_type" : "Bearer", "expires_in" : 3600, "refresh_token" : "1/8tXvdUKcSEcaaxVqqqqqqqqqqqqqoYpj2KSS9qwWI" }"
The refresh token I must store in a safe place, like a DB or protected txt file, which is called upon when my access_token has timed out.
STEP #4: Request Analytics Data
Now from what I understand, I’m ready to roll and should be able to use my access_token to make requests to https://www.googleapis.com/analytics/v3/data/ga.
I do this by sending this request;
$request = 'https://www.googleapis.com/analytics/v3/data/ga' .
'?ids=ga%3Aaaaaaaaa' .
'&start-date=2012-12-07' .
'&end-date=2012-12-09' .
'&metrics=ga%3Avisits';
$opts = array(
'http' => array(
'method' => 'GET',
'header' => 'Content-Type: application/x-www-form-urlencoded\r\n' .
'Authorization: Bearer ya29.AHES6wwwwwwwwwwwwwwwVEBXE6XRbC-Q-pP0wZWdoIm9H804ro \r\n'
)
);
$context = stream_context_create($opts);
$result = file_get_contents($request, FALSE, $context);
var_dump($result);
This request returns a 401 Unauthorized error. I take this as meaning my request is properly formed and making the connection to https://www.googleapis.com/analytics/v3/data/ga.
Also, according to this doc Getting Full Quota, I can make the request with the access_token in the URL like this;
$request = 'https://www.googleapis.com/analytics/v3/data/ga' .
'?ids=ga%3A48564799' .
'&access_token=ya29.AHES6wwwwwwwwwwwwwwwVEBXE6XRbC-Q-pP0wZWdoIm9H804ro' .
'&start-date=2012-12-07' .
'&end-date=2012-12-09' .
'&metrics=ga%3Avisits';
$result = file_get_contents($request, FALSE);
$result = json_decode($result);
var_dump($result);
This time I receive 403 error, in which google includes the response User does not have sufficient permissions for this profile.
QUESTION #1
Am I’m missing something in the API console or a process in the token acquisition? I’m assuming I’m not, because I’m ultimately acquiring the access_token=ya29 and refresh token.
QUESTION #2
Maybe I’m completely off basis in assuming I can do this with simple https reqests? Do I have to use the Google_Client and AnalyticsService classes? I don’t think this is the case, but maybe I’m wrong.
QUESTION #3
Do I need to use a ‘key’ in my request?
&key=bbbbbbbbbbbbbbbb
QUESTION #4
By using PHP 5.2.17 am I missing something? (besides 5.3 or 5.4 themselves)
For example, in some versions of PHP, in stream_context_create, the header should be in an array and not a string, like this;
$opts = array(
'http' => array(
'method' => 'GET',
'header' => array(
'Content-Type: application/x-www-form-urlencoded',
'Authorization: Bearer ya29.AHES6wwwwwwwwwwwwwwwVEBXE6XRbC-Q-pP0wZWdoIm9H804ro '
)
)
);
But I don’t think that it’s an issue in my case. I’m just curious if these HTTP request need to be formed a different way (without using curl).
Any insights and thoughts would be greatly appreciated

Here’s my dim witted mistake that nearly gave me a heart attack.
I typically do my development work in Chrome. And my Chrome browser was signed into my gmail account personal#gmail.com. However, my analytics account, which is under my work#gmail.com was open in FireFox (try not to laugh to hard).
I’m not 100% sure this is correct, but I think this is the general flow. When I did STEP #2: Request initial API access, I did this in my Chrome browser. The endpoint https://accounts.google.com/o/oauth2/auth was authorizing my personal#gmail.com account. And my STEP #4 API request https://www.googleapis.com/analytics/v3/data/ga was looking for an analytics profile under my personal#gmail.com account. Which of course doesn’t exist.
I literally wasted 15 hours on this. This is dumber than trying to troubleshoot an update… and forgetting to flush the cache.
Sorry for wasting your time.
EDIT REFRESH TOKENS
I've once again run into issues with this API and found out the hard way that GA will revoke Refresh Tokens if too many different clients use the token, at least I think that was the problem.
Further reading can be found here.

I got a 403 today and found the cause: in the get function I was using the account ID instead of the profile ID. Switching to the profile ID fixed it for me.

It could be a problem with CURL request. In the GoogleAnalyticsAPI.class.php > class Http > function curl (around line 720) add one more option to stop CURL from verifying the peer's certificate:
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);

Related

Webhook limitations in linkedin API (webhook is not receiving updates)

LinkedIN has recently released support for webhooks and we are successful in creating a webhook url. We are able to authorise a user administrator of a company to our app and get permissions to write and read from the REST API.
However we are not receiving any webhook updates from the app for that company. And there is no documentation on how to subscribe to a particular company like in other social-media API:s witch we have vast experience from (fb,IG,Twitter).
The documentation on LinkedIn is very limited on the subject. And we are not sure what we can expect from the webhook requests from linkedIn. What is the reason we are not getting Webhooks for that company?
We dont even get webhook calls for the organisation owning the app.
Any help appreciated.
https://learn.microsoft.com/en-us/linkedin/shared/api-guide/webhook-validation?context=linkedin/context
I figure it out I need to add header json to output for validation.
Here is my code in php
if (isset($_REQUEST['challengeCode'])) {
header('Content-Type: application/json');
echo json_encode([
'challengeCode' => $_REQUEST['challengeCode'],
'challengeResponse' => hash_hmac('sha256', $_REQUEST['challengeCode'], 'client secret'),
]);
exit;
}
and for webhook subscription
$api->setApiHeaders([
'X-Restli-Protocol-Version' => '2.0.0',
]);
$developerUrn = urlencode("urn:li:developerApplication:developerid");
$personUrn = urlencode("urn:li:person:personid");
$orgUrn = urlencode(""urn:li:organization:pageid");
$endpoint = "(developerApplication:$developerUrn,user:$personUrn,entity:$orgUrn,eventType:ORGANIZATION_SOCIAL_ACTION_NOTIFICATIONS)";
$api->api("eventSubscriptions/$endpoint", ['webhook' => "WEBHOOK_URL"], 'PUT');
Webhooks are a closed beta feature at this time:
Who can use this: Any developer using a webhooks API (currently available only for social action notifications on company posts to beta partners)
Source: https://learn.microsoft.com/en-us/linkedin/marketing/integrations/recent-changes

Reusing Symfony's HTTP Client results in error, but executing the request with a new instance works as expected

My system needs to authenticate with an external service (out of my control in any way), and the authentication requires two HTTP requests in very short sequence (the token I get in the first request expires in 2 seconds and I have to use it to finish the auth process).
So I'm basically doing this:
// create client
$client = HttpClient::create();
$baseUrl = "https://example.com/webservice.php";
$username = "foo";
$token = "foobarbaz";
// first request
$tokenResponse = $client->request( Method::GET, $baseUrl . '?operation=getchallenge&username=' . $username );
$decoded = $tokenResponse->toArray();
// second request
$loginResponse = $client->request( Method::POST, $this->baseUrl, [
'body' => [
'operation' => 'login',
'username' => $username,
'accessKey' => md5( $decoded['result']['token'] . $token ),
],
]
);
$sessionData = $loginResponse->toArray();
Doing this, I get an exception ( TransportExceptionInterface):
Failed sending data to the peer for "https://example.com/webservice.php"
But, if instead of using the same $client I instantiate a new one ($client2 = HttpClient::create();) and I use that one to make the second request, without making any other change to the code, it will work flawlessly.
My guess would be that this is related to the default request concurrency that HTTP Client uses, but I checked the docs and the options available on HttpClientInterface but I haven't been anyway to change the client behaviour in a way that doesn't require me to create a new instance (nor a way to increase error logging verbosity).
I have the cURL extension installed, so the client being created is CurlHttpClient and not NativeHttpClient.
Is there a way around this, without having to create a new client instance?
Here is what docs say.
Even when doing regular synchronous calls, this design allows keeping
connections to remote hosts open between requests, improving
performance by saving repetitive DNS resolution, SSL negotiation, etc.
To leverage all these design benefits, the cURL extension is needed.
In order to use php streams the example says you use NativeClient as follows:
use Symfony\Component\HttpClient\CurlHttpClient;
use Symfony\Component\HttpClient\NativeHttpClient;
// uses native PHP streams
$client = new NativeHttpClient();
// uses the cURL PHP extension
$client = new CurlHttpClient();
This way you can select specific client without framework making you use curl client.
Once you get token, close the connection and create an new one from same client, this will ensure you are using same Client object but initiating new connection requirements, which mimics the behavior server requires in order to authenticate.
It is always good to check server response before parsing body. Please check if server has sent 200 response with token and branch out your logic. Try catch or checking headers is always a bonus and good practice in error handling.

WooCommerce - woocommerce_rest_cannot_view - Status 401

I have generated a consumer key and consumer secret. The website has SSL installed. I have also installed plugins required for JSON and REST services. This is how the url looks like:
https://<url>/wp-json/wc/v1/products
When I am trying to get(GET) the product details using Basic Auth by using POSTMAN, a Chrome plugin, I get a JSON response like:
{
"code": "woocommerce_rest_cannot_view",
"message": "Sorry, you cannot list resources.",
"data": {
"status": 401
}
}
I have both the READ and WRITE permissions corresponding to the Consumer key.
The 401 error you are getting is because you are using basic auth even though your website is not secure (does not have https).
The solution in postman is to use OAuth 1.0. Just add the consumer key and consumer secret and send the request.
I met same problem.
Here is how I solve it:
require "woocommerce_api"
woocommerce = WooCommerce::API.new(
"https://example.com",
"consumer_key",
"consumer_secret",
{
wp_json: true,
version: "wc/v1",
query_string_auth: true
}
)
The key is query_string_auth: true
you need to force basic authentication as query string true under HTTPS
This is how i stopped worrying and moved on.
In short, the woocommerce rest controllers pretty much all have a SOMEWPRESTCLASS::get_item_permissions_check() method which in turn calls wc_rest_check_post_permissions() to decide if it returns that error;
So you hook into that and validate whichever way you want:
add_filter( 'woocommerce_rest_check_permissions', 'my_woocommerce_rest_check_permissions', 90, 4 );
function my_woocommerce_rest_check_permissions( $permission, $context, $object_id, $post_type ){
return true;
}
Trying to help others:
I was struggling with the 401 response while trying to CURL, and also with VBA trying to request as content-type "application/json"
However, I was able to pull a valid response by just entering this in my browser address bar:
https://mywebsite.com/wp-json/wc/v2/products?consumer_key=ck_blahblah&consumer_secret=cs_blahblah
Following this line of thought, I went back to my VBA app and changed the content type to "application/text" and was able to pull a valid response text with response code 200.
Hope this helps someone.
Try this, I had the same issue with the automattic/woocommerce library and I just got it working by appending the customer_key and customer_secret to the query.
$woocommerce->get("customers/$userId?consumer_key={$this->key}&consumer_secret={$this->secret}");
Quick Edit
The above method works but I found a better solution for the automattic/woocommerce library.
Set query_string_auth to true
Had to dig into the code to find this setting.
Found nothing on it in the docs
return new Client($this->url, $this->key, $this->secret, [
"query_string_auth" => true
]);
I just ran into this. Apparently something was funny with how curl was handling the url, so I had to encapsulate it in double quotes.
This doesn't work:
curl https://www.my-site.com/wp-json/wc/v3/orders?consumer_key=ck_40097dbc2844ce7712e1820bcadf0149c2bedegh&consumer_secret=cs_ab57e19263af0b9ab4c596c310f1e7904bb20123
This does work:
curl "https://www.my-site.com/wp-json/wc/v3/orders?consumer_key=ck_40097dbc2844ce7712e1820bcadf0149c2bedegh&consumer_secret=cs_ab57e19263af0b9ab4c596c310f1e7904bb20123"
You can try Oauth 1.0 with postman:
Problem solved by adding this line below to the end of .htaccess file
All you need to add this line to .htaccess , this work with me
SetEnv HTTPS on
And make sure use OAuth 1.0 for Authorization
Try making the request using query parameter, like this:
https://www.exemple.com/wp-json/wc/v3/orders?consumer_key=ck_01234567890&consumer_secret=cs_01234567890
here: https://www.exemple.com you'll need to fill your url domain.
here: consumer_key and consumer_secret is your ck and cs that was previous genereted on WooCommerce > Settings > Advanced > REST API
Here is a modified answer to Quickredfox's anwer:
add_filter('woocommerce_rest_check_permissions', 'my_woocommerce_rest_check_permissions', 90, 4);
function my_woocommerce_rest_check_permissions($permission, $context, $object_id, $post_type) {
if($_GET['consumer_key'] == 'asdfghj' && $_GET['consumer_secret'] == 'qwerty') {
return true;
}
return $permission;
}
The downside to this is that the flexibility of adding and revoking access for users using a gui is lost. However, if nothing else works and you just can't figure out why, this will work and does not expose the API to the whole world.
Oh, and this requires passing the key and secret as parameters a la:
https://foo.bar.com/wp-json/wc/v3/products/123&consumer_key=asdfghj&consumer_secret=qwerty
This will work without https, but if you use it without https, remember that any credentials you send along with your request will be sent in plain text.
I just ran into this, I was getting the exact same error message as OP. I was using https and OAuth 1. The problem ended up being the domain. I was trying to access example.com when the correct domain for the site was www.example.com.
This URL returns 401 woocommerce_rest_cannot_view error:
https://example.com/wp-json/wc/v3/products
This URL works and returns results:
https://www.example.com/wp-json/wc/v3/products
For local development (localhost) you can also use Basic Auth (e.g. for Postman) instead of Consumer Key & Consumer Secret. It works seamlessly.
Add this code to function.php to fix the problem:
add_filter( 'woocommerce_rest_check_permissions', 'my_woocommerce_rest_check_permissions', 90, 4 );
function my_woocommerce_rest_check_permissions( $permission, $context, $object_id, $post_type ){
return true;
}
in node js code would be
const WooCommerceRestApi = require("#woocommerce/woocommerce-rest-api").default;
const api = new WooCommerceRestApi({
url: "http://example.com",
consumerKey: "ck_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
consumerSecret: "cs_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
queryStringAuth: true,
version: "wc/v3"
});
It's sometimes an error with wordpress htaccess configuration (only if you are accessing website by https).
For some reason woocommerce want you to authorize with basic authentication when your are connecting through https which some hosting blocks so you need to unlock it.
you need to change
RewriteRule ^index\.php$ - [L]
To
RewriteRule ^index\.php$ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L]

Drupal 7 feeds, permission-error with oauth for instagram ("public_content")

earlier in 2015 i started creating a website with drupal 7 that imports instagram-content (images, likes, comments etc.) via Drupal Feeds. Everything worked finde, but the projects stopped then.
Now it seems we start that again but suddenly the import is not working anymore. I always get the following error:
{"meta": {"error_type": "OAuthPermissionsException", "code": 400,
"error_message": "This request requires scope=public_content, but this
access token is not authorized with this scope. The user must
re-authorize your application with scope=public_content to be granted
this permissions."}}
I didnt had to send the "public_content" earlier, so i was just sending "basic"-scope access. And as i said, everything worked well.
Now i inserted also the scope for "public_content" along with "basic" within the oauth-Module for feeds. But still getting the error-message above.
Any hints on that?
Thanks in advance and regards,
Fab
This is due to a Instagram Platfrom Update
You'll have to add public_content scope as Joshi has pointed out - and also you'll need to renew your auth token in the settings page.
Then you'll be good to go.
Here is the solution:
Use following code in instagram_social_feed.module
Function: instagram_social_feed_settings()
if (variable_get('instagram_social_feed_client_id', '') != '' && variable_get('instagram_social_feed_redirect_uri', '') != '') {
$form['authenticate'] = array(
'#markup' => l(t('Click here to authenticate via Instagram and create an access token'),
'https://api.instagram.com/oauth/authorize/?client_id=' . variable_get('instagram_social_feed_client_id') . '&redirect_uri=' . variable_get('instagram_social_feed_redirect_uri') . '&response_type=code&scope=public_content'
)
);
}
This will solve the issus

Gravity forms submit to third party and redirect using wp_http()

I am trying to get a gravity form from my wordpress website to serve as a login form for another application (CakePhp website). The form has two fields-username and password. I have added a hook to submit the form to the other application using gform_after_submission as follows:
add_action( 'gform_after_submission_6', 'mysite_gform_after_submission', 10, 2 );
function mysite_gform_after_submission( $entry, $form ) {
$post_url = 'http://otherapplicationurl.com/login';
$body = array(
"data[User][username]" => $entry[1],
"data[User][password]" => $entry[2],
);
$request = new WP_Http();
$response = $request->post($post_url, array('body' => $body));
//this is to delete the entry
GFAPI::delete_entry( $entry['id'] );
}
The form's confirmation setting is to display some text. But what I essentially want it to do is login the user to the other application and show the homepage of that application i.e redirect to the url "http://otherapplicationurl.com/home".
I keep getting the following error.
WP_Error Object
(
[errors] => Array
(
[http_request_failed] => Array
(
[0] => Too many redirects.
)
)
[error_data] => Array()
)
I don't know how to get the form to log the user in and redirect to the other applications home page.
Thanks in advance.
You've hit a very tricky process here. What you're trying to do is two things simultaneously:
Log in to an external service
Redirect to the user to the service
Technically, this can't be done the way you're looking to do it. And here's why:
You're dealing with cookies. When you log in, data is stored as a cookie/session that will remember who you are as you navigate through the site. The info is also only available to the relevant domain/path, and can only be set from that same domain/path.
The server, not the user, is logging in.
In other words, you're trying to log the user into the service from another domain via the server. In this case, the server will log in on behalf of the user (as it's the server doing the request), but will do nothing with the cookies. Even if we sent the cookies back to the user, they would apply under the original domain and we'd be no closer to being logged in.
Solution 1: Simple, but insecure
What you could do, is make the redirect and the login process the same thing. That is, using a URL such as http://otherapplication.com/login?user=adomnom&pass=awesome (though I strongly discourage that for security reasons).
Solution 2: Secure, but (you guessed it) complex
A safer approach would be to use the structure you have at the moment to generate a one-time login code. That is, the server will request a unique, one-time 'token' from the other application using the login details and use these as part of the redirect. The user is taken to this other page and is logged in using this token as a substitute for credentials. After this, the token should then become invalid.
That way, the other application is the one setting the cookies and no sensitive information is being directly transferred.
And here's how I'd do it...
1. Create new endpoint on the CakePHP side: /get-token
This endpoint will receive the username and password from GET data, then generate, store and return a unique token.
2. Extend /login on the CakePHP side to allow for a 'token' GET variable
Submitting 'token' to /login should also log in the user and delete the token - preventing it from being used again.
3. Update the submission process to use the correct hook
You'll want to use the gform_confirmation hook to do this - it's the hook that deals with redirects and thank you page contents (ie. the stuff that the user is shown after submitting the form).
add_action( 'gform_confirmation_6', 'mysite_gform_confirmation', 10, 3 );
function mysite_gform_confirmation( $confirmation, $form, $entry ) {
// Send login request
$token = wp_remote_post(
'http://otherapplicationurl.com/login',
array(
'body' => array(
"data[User][username]" => $entry[1],
"data[User][password]" => $entry[2]
)
)
);
// Delete entry
GFAPI::delete_entry( $entry['id'] );
// Redirect
return array('redirect', "http://otherapplicationurl.com/login?token=$token");
}
Hope that helps! Good luck!

Resources