Google analytics V4 client access to our app - google-analytics

We have a web application in PHP, for our clients we have prepared connect to google analytics UA. I use "google/apiclient": "^2.0", it works that our clients click on button in our administration and then is runned a followed code:
$this->client = new Google_Client();
$this->client->setApplicationName("xxxx");
$this->client->setClientId("xxxx");
$this->client->setClientSecret("xxxx");
$this->client->setScopes(array("https://www.googleapis.com/auth/analytics.readonly"));
$this->client->setRedirectUri("xxxx");
$this->client->setAccessType('offline');
$this->client->setApprovalPrompt("force");
The credentials i get from https://console.cloud.google.com/ -> OAuth 2.0 Client IDs
then the client is redirected to google where he log in, and allow acces to his GA data for our app. then is redirected back with code is generated access token. With this token i can get his GA UA data and show it to graphs in our administration. It works allright, but now i get information that GA UA will be end, and i need to create the same proces for UA V4. But in documentation to GA V4 what i found: https://developers.google.com/analytics/devguides/reporting/data/v1
Is not information how to process it for our clients. There is only authorisation over service account, that i must donwload my own credentials.json to service account but it allow me only acces to my private account, but i need it to work the same as before, so for other clients without having to upload credentials.json. That is, to be redirected to google via OAuth 2.0 Client IDs and allow access to our application to read their data. Is it even possible?
Thank you for help, and sorry for my bad english

This should give you a start. I am combining the OAuth2 methods from the Google API php client library and applying them to the new library.
Its not optimal but it works. Code below is for installed application not web. Its not going to work hosted.
<?php
require 'vendor/autoload.php';
use Google\Client;
use Google\Analytics\Data\V1beta\BetaAnalyticsDataClient;
putenv('GOOGLE_APPLICATION_CREDENTIALS=C:\YouTube\dev\credentials.json'); // Installed / native / desktop Client credetinals.
$credentials = getenv('GOOGLE_APPLICATION_CREDENTIALS');
$myfile = file_get_contents($credentials, "r") ;
$clientObj = json_decode($myfile);
$client = getClient();
$tokenResponse = $client->getAccessToken();
print_r($tokenResponse);
print_r($tokenResponse["access_token"]);
$service = new BetaAnalyticsDataClient( [
'credentials' => Google\ApiCore\CredentialsWrapper::build( [
'scopes' => [
'https://www.googleapis.com/auth/analytics',
'openid',
'https://www.googleapis.com/auth/analytics.readonly',
],
'keyFile' => [
'type' => 'authorized_user',
'client_id' => $clientObj->installed->client_id,
'client_secret' => $clientObj->installed->client_secret,
'refresh_token' => $tokenResponse["refresh_token"]
],
] ),
] );
$response = $service->runReport([
'property' => 'properties/[YOUR_PROPERTY_ID]'
]);
foreach ($response->getRows() as $row) {
foreach ($row->getDimensionValues() as $dimensionValue) {
print 'Dimension Value: ' . $dimensionValue->getValue() . PHP_EOL;
}
}
function getClient()
{
$client = new Client();
$client->setApplicationName('Google analytics data beta Oauth2');
$client->setScopes('https://www.googleapis.com/auth/analytics');
$client->setAuthConfig(getenv('GOOGLE_APPLICATION_CREDENTIALS'));
$client->setAccessType('offline');
// Load previously authorized token from a file, if it exists.
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
$tokenPath = 'token.json';
if (file_exists($tokenPath)) {
$accessToken = json_decode(file_get_contents($tokenPath), true);
$client->setAccessToken($accessToken);
}
// If there is no previous token or it's expired.
if ($client->isAccessTokenExpired()) {
// Refresh the token if possible, else fetch a new one.
if ($client->getRefreshToken()) {
$client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
} else {
// Request authorization from the user.
$authUrl = $client->createAuthUrl();
printf("Open the following link in your browser:\n%s\n", $authUrl);
print 'Enter verification code: ';
$authCode = trim(fgets(STDIN));
// Exchange authorization code for an access token.
$accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
$client->setAccessToken($accessToken);
// Check to see if there was an error.
if (array_key_exists('error', $accessToken)) {
throw new Exception(join(', ', $accessToken));
}
}
// Save the token to a file.
if (!file_exists(dirname($tokenPath))) {
mkdir(dirname($tokenPath), 0700, true);
}
file_put_contents($tokenPath, json_encode($client->getAccessToken()));
}
return $client;
}

And please how to get list streams of listed properties, I have a code to get GA4 properties:
$this->SetGa4ClientAdmin();
$accounts = $this->ga4_admin->listAccounts();
$this->data['accounts_v4'] = array();
foreach ($accounts as $account) {
$this->data['accounts_v4'][$account->getName()] = array('name' => $account->getDisplayName(), 'childrens' => array());
try {
$properties = $this->ga4_admin->ListProperties('parent:' . $account->getName());
foreach ($properties AS $property) {
$this->data['accounts_v4'][$account->getName()]['childrens'][$property->getName()] = $property->getDisplayName();
}
} catch (Exception $ex) {
die("error: " . $ex->getMessage());
}
}
At every property I need to get measurement ID of GA4 streams.
I need to get this

Related

PHPunit: can't assert API login test

I'm trying to follow best practices using TDD to build the auth logic with Sanctum. But haven't been able to pass the login test.
Using postman the route works well:
But the test is not behaving as expected. It'd simply fail.
1) Tests\Feature\LoginTest::test_login_route_api
Expected status code 200 but received 401.
Failed asserting that 200 is identical to 401.
I tried to pass a token as header or body and it has not helped.
The test
public function test_login_route_api()
{
$this->withoutExceptionHandling();
//using RefreshDatabase
$user = User::factory()->create();
$response = $this->post('/api/login', [
'email' => $user->email,
'password' => $user->password,
], []);
$response->dumpHeaders();
$response->dumpSession();
$response->dump();
$response->assertStatus(200);
LoginController (method as advised in Laravel Documentation https://laravel.com/docs/8.x/authentication#authenticating-users):
public function login(Request $request)
{
$credentials = $request->only('email', 'password');
if (Auth::attempt($credentials)) {
// $request->session()->regenerate();
$user = Auth::user();
// $success['token'] = $user->createToken('MyApp')->plainTextToken;
$success['token'] = $request->token;
$success['name'] = $user->name;
return response([$success, 'logged in'], 200);
}
return response('fail!', 401);
}
The route:
Route::name('api')->group(function () {
Route::post('/login', [LoginController::class, 'login']);
// some routes
});
phpunit.xml uncommented
<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>
Not sure what's going on, I've read some similar topics but none has helped me.
Thank you,
Ok, solved it.
Somehow, using factory()->make() or factory()->create() with RefreshDatabase wasn't accepting the $user as valid $credentials.
I had to use an user from with a seeded database like so: $user = User::first();
This would make the test pass.
Any idea why factory users are not passing the test?

Symfony 5 / Mercure > Private update. Fetch CORS

I try to integrate Mercure to symfony 5 project. And there are two trouble.
I try to send update to private, but it's not work, but if try to send to not private everything works.
Command that run mercure:
./bin/mercure --jwt-key='homesphere_secret_token' --addr='localhost:3000' --allow-anonymous --cors-allowed-origins='*'
.env file
MERCURE_PUBLISH_URL=http://localhost:3000/.well-known/mercure
MERCURE_JWT_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.P0f5r123SLTru4DiE4X9q0EIoKahds-nI8jpo8uKKQQ
MERCURE_SECRET_KEY=homesphere_secret_token
Backend that gives url and jwt token
$user = $this->getUser();
$hubUrl = $this->getParameter('mercure.default_hub');
$link = new Link('mercure', $hubUrl);
$token = JWT::encode([
'mercure' => [
'publish' => [sprintf("/chat/%s", $user->getId())],
],
], $this->getParameter('mercure_secret_key'));
Update code
$message = $serializer
->serialize(['message' => $message], 'json');
$update = new Update(
sprintf("/chat/%s", $user->getId()),
$message,
true // if false then work
);
$publisher($update);
Frontend part
const hub = new URL('http://localhost:3000/.well-known/mercure');
hub.searchParams.append('topic', '/chat/1');
const eventSource = new EventSourcePolyfill(hub.href, {
'Authorization': 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiXC9jaGF0XC8xIl19fQ.yg3DodPjaPzWVIOhKMo30xXzS3L4oPckbL9pcA4tMck'
});
eventSource.onmessage = event => {
console.log(JSON.parse(event.data));
}
When frontend receives message by update then next requests start rising CORS trouble
image
could anyone help me with these, please?
Try with --cors-allowed-origins='http://localhost:8000'

Any API to add an authorized domain to Firebase Auth?

Just want to check, is there any API to add the authorized domain in a programmatical way instead of adding it manually by going to Firebase console?
Also, is there any limit on how many domains can be added as the authorized domains?
JavaScript in Cloud Functions solution
import { google } from "googleapis";
(async () => {
/**
* ! START - Update Firebase allowed domains
*/
// Change this to whatever you want
const URL_TO_ADD = "engineering.acme-corp.net";
// Acquire an auth client, and bind it to all future calls
const auth = new google.auth.GoogleAuth({
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
});
const authClient = await auth.getClient();
google.options({ auth: authClient });
// Get the Identity Toolkit API client
const idToolkit = google.identitytoolkit("v3").relyingparty;
/**
* When calling the methods from the Identity Toolkit API, we are
* overriding the default target URLs and payloads (that interact
* with the v3 endpoint) so we can talk to the v2 endpoint, which is
* what Firebase Console uses.
*/
// Generate the request URL
const projectId = await auth.getProjectId();
const idToolkitConfigUrl = `https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/config`;
// Get current config so we can use it when we later update it
const currentConfig = await idToolkit.getProjectConfig(undefined, {
url: idToolkitConfigUrl,
method: "GET",
});
// Update the config based on the values that already exist
await idToolkit.setProjectConfig(undefined, {
url: idToolkitConfigUrl,
method: "PATCH",
params: { updateMask: "authorizedDomains" },
body: JSON.stringify({
authorizedDomains: [
...(currentConfig.data.authorizedDomains || []),
URL_TO_ADD,
],
}),
});
})();
A quick note on other languages
The principles should be the same:
Find a way to interact with Google's identify toolkit API (maybe Google offers an SDK to your language)
Get current config
Set new config
If you can't find an SDK, you can also work with raw http requests: https://cloud.google.com/identity-platform/docs/reference/rest/v2/projects/getConfig (it's just a bit trickier to do authentication when doing everything manually)
There is no API for this - you must do it through the console. You can also file a feature request with Firebase support if you want.
There doesn't appear to be any documentation stating limits of number of domains. Again, reach out to Firebase support if the documentation is unclear.
Thanks #Jean Costa
Totally working for me.
Here is C# implementation
using Google.Apis.Auth.OAuth2;
using Newtonsoft.Json;
var serviceAccountJsonFile = "path to service account json";
var projectId = "your project ids";
var authorizedDomains = new
{
authorizedDomains = new string[] {
"localhost",
"******.firebaseapp.com",
"*********.web.app",
"abc.def.com"
}
}; // your desire authorized domain
List<string> scopes = new()
{
"https://www.googleapis.com/auth/identitytoolkit",
"https://www.googleapis.com/auth/firebase",
"https://www.googleapis.com/auth/cloud-platform"
};
var url = "https://identitytoolkit.googleapis.com/admin/v2/projects/" + projectId + "/config";
using var stream = new FileStream(serviceAccountJsonFile, FileMode.Open, FileAccess.Read);
var accessToken = GoogleCredential
.FromStream(stream) // Loads key file
.CreateScoped(scopes) // Gathers scopes requested
.UnderlyingCredential // Gets the credentials
.GetAccessTokenForRequestAsync().Result; // Gets the Access Token
var body = JsonConvert.SerializeObject(authorizedDomains);
using (var client = new HttpClient())
{
var request = new HttpRequestMessage(HttpMethod.Patch, url) {
Content = new StringContent(body,System.Text.Encoding.UTF8)
};
request.Headers.Add("Accept", "application/json");
request.Headers.Add("Authorization", "Bearer " + accessToken);
try
{
var response = client.SendAsync(request).Result;
Console.WriteLine(response.Content.ReadAsStringAsync().Result);
}
catch (HttpRequestException ex)
{
// Failed
}
}
Thanks #Jean Costa and #Yan Naing
here is my php implemetation
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Exception\TransferException;
use Google\Service\IdentityToolkit;
use Google\Service\IAMCredentials;
$KEY_FILE_LOCATION = storage_path('/app/credentials/service-account-1.json') ;
if (!file_exists($KEY_FILE_LOCATION)) {
throw new Exception(sprintf('file "%s" does not exist', $KEY_FILE_LOCATION));
}
$json= file_get_contents($KEY_FILE_LOCATION);
if (!$config = json_decode($json, true)) {
throw new Exception('invalid json for auth config');
}
$client = new \Google\Client();
$client->setAuthConfig($config );
$client->setScopes([ "https://www.googleapis.com/auth/identitytoolkit",
"https://www.googleapis.com/auth/firebase",
"https://www.googleapis.com/auth/cloud-platform"]);
$service = new IdentityToolkit($client);
// Get the Identity Toolkit API client
$idToolkit = $service->relyingparty;
//Get current config
$current_config= $idToolkit->getProjectConfig();
//Get service account access token
$access_token_req = new IAMCredentials\GenerateAccessTokenRequest();
$access_token_req->setScope( "https://www.googleapis.com/auth/firebase");
$credentials = new IAMCredentials($client);
$access_token = $credentials->projects_serviceAccounts->generateAccessToken("projects/-/serviceAccounts/{$config["client_email"]}" , $access_token_req )->getAccessToken();
// Generate the request URL (https://cloud.google.com/identity-platform/docs/reference/rest/v2/projects/updateConfig)
$idToolkitConfigUrl = "https://identitytoolkit.googleapis.com/admin/v2/projects/{$config["project_id"]}/config";
$authorized_domains = [ 'authorizedDomains' => array_merge( ['twomore.com'],$current_config->authorizedDomains)];
$client = new GuzzleClient( );
$response = null;
try {
$response = $client->request('PATCH', $idToolkitConfigUrl, [
'verify' => Helpers::isProduction() ? true : false ,
'http_errors'=> false, //off 4xx and 5xx exceptioins
'json' => $authorized_domains ,
'headers' => [
"Authorization" => "Bearer " . $access_token ,
"Accept" => "application/json",
]
]);
} catch (TransferException $e) {
throw new Exception( $e->getMessage());
}
$data = json_decode($response->getBody()->getContents(),true);
if($response->getStatusCode()!==200){
throw new Exception($response->getReasonPhrase() . ( isset($data['exception']['message']) ? " - " . $data['exception']['message'] : ""));
}
return response()->json(['data' => [
'authorized_domains' => $data['authorizedDomains']
]]);

google calendar refresh token and codeigniter

I'm using Google Calendar API to display events on fullcalendar (so using a json object in my view). I'm using codeigniter php framework, and I have a few functions in my controller to create a new client then I use that in the oauth2callback() function to exchange my code for an access_token then I start calling the service in gcalendar() and gcalendar_events. I have set the accessType to offline but that doesn't seem to make me access the events offline. It works great except that I'm redirected to log in again every time the session ends. I don't want that, I want them to display all the time after the session ends. I am trying to use a refresh token in case the access_token expires to see if that would fix the problem.
this is the code in my controller
function getClient() {
$client = new Google_Client();
$client->setApplicationName("DL Calendar");
$client->setAuthConfig('application/client_secrets.json');
$client->addScope('profile');
$client->setIncludeGrantedScopes(true);
$client->setAccessType('offline');
return $client;
}
function gcalendar() {
$this->load->add_package_path(APPPATH . 'vendor/autoload');
$client = $this->getClient();
//$client->setRedirectUri(site_url('calendar/index'));
$client->addScope(Google_Service_Calendar::CALENDAR);
if (isset($_SESSION['access_token']) && $_SESSION['access_token']) {
$client->setAccessToken($_SESSION['access_token']);
$access_token = $_SESSION['access_token'];
$service = new ]Google_Service_Calendar($client);
$calendar = new Google_Service_Calendar_Calendar();
//$calendarList = $service->calendarList->listCalendarList();
$calendar = $service->calendars->get('primary');
$params = array(
'owner_id' => get_current_user_id(),
'title' => get_current_user(). ' ' .'Google Calendar',
'type' => 'gcal',
'url' => $calendar->id,
);
$calendar_id = $this->Calendar_model->add_calendar($params);
redirect('calendar/index');
} else {
$redirect_uri = site_url('calendar/oauth2callback');
header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL));
}
$this->session->set_flashdata('success', 'Event Successfully Added');
}
function oauth2callback() {
//Build the client object
$client = $this->getClient();
$client->addScope(Google_Service_Calendar::CALENDAR);
$service = new Google_Service_Calendar($client);
$url = parse_url($_SERVER['REQUEST_URI']); parse_str($url['query'], $params);
$code = $params['code'];
//To exchange an authorization code for an access token, use the authenticate method:
if (! isset($code)) {
$auth_url = $client->createAuthUrl();
header('Location: ' . filter_var($auth_url, FILTER_SANITIZE_URL));
} else {
$token = $client->fetchAccessTokenWithAuthCode($code);
$client->setAccessToken($token);
$client->authenticate($code);
$_SESSION['access_token'] = $client->getAccessToken();
$redirect_uri = site_url('calendar/gcalendar');
header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL));
}
}
function gcalendar_events() {
$client = $this->getClient();
$client->addScope(Google_Service_Calendar::CALENDAR);
// $client->setRedirectUri(site_url('calendar/gcalendar'));
$client->setAccessType('offline'); //need calendar events to appear even if not logged in to google
if (isset($_SESSION['access_token']) && $_SESSION['access_token']) {
$client->setAccessToken($_SESSION['access_token']);
$access_token = $_SESSION['access_token'];
$service = new Google_Service_Calendar($client);
$id = 'primary';
$calendar = new Google_Service_Calendar_Calendar();
$calendar = $service->calendars->get('primary');
$event = new Google_Service_Calendar_Event();
$events = $service->events->listEvents($id);
foreach ($events->getItems() as $event) {
$startTime = strtotime($event->getStart()->dateTime) ;
$endTime = strtotime($event->getEnd()->dateTime);
$start = date('Y-m-d H:i:s', $startTime);
$end = date('Y-m-d H:i:s', $endTime);
$eventsArr[] = array(
'title' => $event->getSummary(),
'start'=> $start,
'end' => $end,
);
}
// Return a single `events` with all the `$eventsArr`
echo json_encode($eventsArr);
}
}
Is the problem in my session ending? or does the access token expire and I need a refresh token? where do I set the refresh token cause I tried putting it in more that one place and I get an error message that refresh token has to be set as part off setAccessToken. I put it all over and still got error messages.
Here is the code I used
if ($client->isAccessTokenExpired()) {
$refresh_token = $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
$client->setAccessToken($refresh_token);
$_SESSION['access_token'] = $refresh_token;
$this->load->helper('file');
write_file('application/client_secrets.json', json_encode($client->getAccessToken()));
} else {
$access_token = $_SESSION['access_token'];
}
I just noticed that it once used to say 'grant offline access' in my authorization but now it no longer mentions that while google documentation says
"After a user grants offline access to the requested scopes, you can continue to use the API client to access Google APIs on the user's behalf when the user is offline. The client object will refresh the access token as needed."
During your first authorization with Google, you will receive a token that will expire in 3600 seconds or one hour. So you need to use refresh token to get a new working token.
It is something like this SO question.
$token = $client->getAccessToken();
$authObj = json_decode($token);
if(isset($authObj->refresh_token)) {
save_refresh_token($authObj->refresh_token);
}
Make sure you save this refresh_token.
You can update it with:
$client->refreshToken($your_saved_refresh_token);
And then set your new access token to the session:
$_SESSION['access_token'] = $client->getAccessToken();
I also suggest you to visit this quickstart of Google Calendar for PHP.
For more information, check this related SO question.
How to refresh token with Google API client?

Refresh Token does not work in Google Analytics API

So I define all parameters on the top of my page:
<?php
session_start();
$client = new Google_Client();
$client->setAuthConfigFile('client_secrets.json');
$client->addScope(Google_Service_Analytics::ANALYTICS_READONLY);
$client->setAccessType('offline');
$client->setApprovalPrompt('force');
if (isset($_SESSION['access_token']) && $_SESSION['access_token']) {
$client->setAccessToken($_SESSION['access_token']);
$client->getAccessToken();
} else {
$redirect_uri = 'http://' . $_SERVER['HTTP_HOST'] . '/oauth2callback.php';
header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL));
}
//get the access token
$myToken = json_decode($client->getAccessToken());
if ($client->getAuth()->isAccessTokenExpired()) {
$token = $myToken->refresh_token;
echo 'token expired';
} else {
$token = $myToken->access_token;
echo 'token not yet expired';
}
?>
Down at the bottom I got java script:
<script>
gapi.analytics.ready(function() {
var CLIENT_ID = 'my-client-id-goes-here';
gapi.analytics.auth.authorize({
'serverAuth': {
'access_token': '<?php echo $token; ?>'
}
});
This works however until access token expires, which is in 60 minutes. After that reports are not showing anymore. What did I do wrong and why it doesn't use a refresh token?
This is actually the expected behavior (unfortunately). The Embed API does not take a refresh token because generally you never want to expose those tokens publicly (which you would be doing if they were in your HTML source).
If you wanted to work around this limitation, you could set a timer on the page that updated the access token every 50 minutes or so (they expire after 60, as you pointed out).
If you had an endpoint on your server that returned a new access token, you could do something like this:
setInterval(function() {
makeRequestToGetNewAccessToken().then(function(access_token) {
gapi.auth.setToken({
access_token: access_token
});
});
}, 1000 * 60 * 50);
Note, the key above is calling setToken with the new access token. That will allow the Embed API to continue to work as normal.

Resources