I've used google drive api v3 earlier without any issue now after long time I'm using it again to authenticate user with access token but when I call authenticate method of google drive client it states this method is deprecated, also I've observed google has changed many other features.
$file = 'credentials.json';
$client = new Google_Client();
$client->setScopes([
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/drive.metadata',
'https://www.googleapis.com/auth/drive.file',
]);
$client->setAuthConfig($file);
if (isset($_REQUEST['code'])){
$accessToken = $client->authenticate($_GET['code']);
$client->setAccessToken(json_decode($accessToken, true));
return $client;
}
$authUrl = $client->createAuthUrl();
Login
as authenticate is giving null response so its not letting to set access token and giving error json_decode() expects parameter 1 to be string, array given.
For anyone looking for the new command, instead use:
$accessToken = $client->fetchAccessTokenWithAuthCode($_GET['code']);
Related
With the release of GA4, from reading it seems there is a new API client that we must use to manage GA4 accounts and properties. I am working with PHP and talking about this client: https://github.com/googleapis/php-analytics-admin
For Google UA (v3), the API allowed us to use OAuth access tokens so that users can grant an application access to their UA accounts. Now, with the new GA4 Admin API, it allows us to add service credentials but I can't see how I can use OAuth access tokens?
For example here is my PHP code:
$ga4 = new AnalyticsAdminServiceClient(['credentials' => $cred]);
$accounts = $ga4->listAccounts();
foreach ($accounts as $account) {
print 'Found account: ' . $account->getName() . PHP_EOL;
}
The passed in $cred variable is just a decoded keyFile array. But that does not accept an OAuth token?
With the v3 Analytics API, it was so simple, you were able to create a new Google Client like this and pass an access token:
$client = new Google_Client();
$client->setAuthConfig($keyFile);
$client->addScope(Google_Service_Analytics::ANALYTICS_READONLY);
$client->setAccessToken($token);
And then that new client could be passed to the Analytics client...
So with the new GA4 API, how do I set an access token obtained from OAuth setup with a user that grants permissions for me to access their GA account?
I have figured it out. You have to call \Google\ApiCore\CredentialsWrapper::build to create a credentials object. Which you then use to authenticate with a refreshed OAuth token.
$credentials = \Google\ApiCore\CredentialsWrapper::build([
'keyFile' => $keyFile,
'scopes' => [
'https://www.googleapis.com/auth/analytics',
'https://www.googleapis.com/auth/analytics.readonly',
]
]);
Then the $keyFile needs to be changed to not be a service account but an authorized_user:
$keyFile = [
'type' => 'authorized_user',
'client_id' => <YOUR_GOOGLE_ANALYTICS_CLIENT_ID>,
'client_secret' => <YOUR_GOOGLE_ANALYTICS_CLIENT_SECRET>,
'refresh_token' => $accessToken
];
Then you can pass it to a new GA4 object like so:
$ga4Client = new AnalyticsAdminServiceClient(['credentials' => $credentialsd]);
A little different compared to how the UA client works.
I created the app in Tenant A and added it to Tenant B. I have granted the permissions in both tenants. Why am I getting this response every time I make an API call to the app?
resulted in a `401 Unauthorized` response: {"error":{"code":"NoPermissionsInAccessToken","message":"The token contains no permissions, or permissions can not be un (truncated...)
Here is the PHP request that I'm making (I am using the client id and client secret from the app in Tenant A):
<?php
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use Microsoft\Graph\Graph;
$guzzle = new \GuzzleHttp\Client();
$tenantId = 'common';
$clientId = 'ccc-ddd-fff';
$clientSecret = 'xxx-yyy-zzz';
$url = 'https://login.microsoftonline.com/' . $tenantId . '/oauth2/token?api-version=1.0';
try {
$token = json_decode($guzzle->post($url, [
'form_params' => [
'client_id' => $clientId,
'client_secret' => $clientSecret,
'resource' => 'https://graph.microsoft.com/',
'grant_type' => 'client_credentials',
],
])->getBody()->getContents());
$accessToken = $token->access_token;
} catch (\Exception $e) {
print $e->getMessage();
}
$graph = new Graph();
$graph->setAccessToken($accessToken);
try {
print_r($graph->createRequest("GET", '/users/email#email.com/messages/xxxxxxxxxxxxx==')->execute());
} catch (\Exception $e) {
print $e->getMessage();
}
Both tenants have these permissions granted:
Permissions in Tenant A and B
Firstly, according to the api document, you'd better to add Mail.ReadBasic.All application permission, but I'm not sure if it influenced your calling here.
Your issue mainly came from that you used $tenantId = 'common';, you should set it as the tenant name (such as xxx.onmicrosoft.com) you'd like to calling the api for. I mean that if you wanna call /users/xxx#tenantA.com/messages, you should put tenantA's tenant id/name to the $tenantId variable, but when you wanna call /users/yyy#tenantB.com/messages, you should set tenantB's.
Screenshot below showed the scenario when I used a token generated by common but not my tenant name.
401 Unauthorized error: Is your token valid?
Make sure that your application is presenting a valid access token to Microsoft Graph as part of the request. This error often means that the access token may be missing in the HTTP authenticate request header or that the token is invalid or has expired. We strongly recommend that you use the Microsoft Authentication Library (MSAL) for access token acquisition. Additionally, this error may occur, if you try to use a delegated access token granted to a personal Microsoft account, to access an API that only supports work or school accounts (organizational accounts).
You can always leverage jwt.ms to check claims on your token. Use this and check if you have the necessary permissions to call an API endpoint.
What I needed to do was prompt the Microsoft admin to grant permissions using a link similar to this one. Merely adding the enterprise from the Microsoft
https://login.microsoftonline.com/common/oauth2/v2.0/authorize?scope=offline_access+openid+profile+User.Read+Mail.ReadWrite+Mail.Send
&response_type=code
&client_id=a62b0808-2b1f-4efc-a3d6-ad1223dc06a9
&redirect_uri=https://myurl/blah.html
&response_mode=query
It's been such a major headache getting my service account to authenticate on the same webapp where I have users logging in via oauth2 as well.
So I'm wondering, is this even possible?
If not, should one just stick with the service account? Does one have to then authenticate the users on one's own - old school style? Haha
Thanks.
Regarding the service account, I have enabled the domain wide delegation, enabled the client key + api scope in my G suite admin console, and have gotten the php sample with the books api working. However any time I try any other api, other than books, I get the error,
client is unauthorized to retrieve access tokens using this method
UPDATE: I've tried to use #dalmto's example, and have added a few lines to test the gmail api, for example:
putenv('GOOGLE_APPLICATION_CREDENTIALS=credentials.json');
$user = 'email#domain.de';
function getGoogleClient() {
return getServiceAccountClient();
}
function getServiceAccountClient() {
try {
// Create and configure a new client object.
$client2 = new Google_Client();
$client2->useApplicationDefaultCredentials();
$client2->setScopes(array('https://www.googleapis.com/auth/userinfo.email','https://www.googleapis.com/auth/admin.directory.user.readonly','https://www.googleapis.com/auth/userinfo.profile','https://www.googleapis.com/auth/gmail.readonly','https://www.googleapis.com/auth/calendar'));
$client2->setAccessType('offline');
$client2->setSubject($user);
return $client2;
} catch (Exception $e) {
print "An error occurred: " . $e->getMessage();
}
}
$newGoogleClient = getGoogleClient();
$service3 = new Google_Service_Gmail($newGoogleClient);
$results3 = $service3->users_labels->listUsersLabels($user);
But am now just receiving "400: Bad Request" errors
EDIT: After some more digging there is a note: 'failedPrecondition' - any idea which precondition that could be? I've allowed the following scopes for the client in my admin console:
hxxps://www.googleapis.com/auth/gmail.metadata,
hxxps://www.googleapis.com/auth/userinfo.email,
hxxps://www.googleapis.com/auth/userinfo.profile,
hxxps://www.googleapis.com/auth/gmail.modify,
hxxps://www.googleapis.com/auth/gmail.readonly,
hxxps://www.googleapis.com/auth/gmail.labels,
hxxps://mail.google.com/
And enabled the apis and enabled the scope in the 'OAuth Consent Screen'
DWD is also enabled: Service Account Overview Screenshot
EDIT2: Okay so I found the missing precondition was the "setSubject".
Once I added that it went a step further, but still failed again at '"error": "unauthorized_client",\n "error_description": "Client is unauthorized to retrieve access tokens using this method.'
FYI: When creating the service account, I gave it the "project -> owner" role. Is that sufficient? Does one have to add more?
EDIT3: I've also just checked logger and it says that DWD is enabled.. Im at my whits end here haha
client: {
adminState: {
updateTime: "2018-11-23T00:29:44.810Z"
}
assertionMatchExistingGrant: "MATCH_GRANT_DISABLED"
authType: "PUBLIC_KEY"
brandId: "aaaaaaaaaaaaaa"
clientId: "aaaaaaaaaaaaaaaaaa"
consistencyToken: "2018-11-23T00:29:44.953175Z"
creationTime: "2018-11-23T00:29:44.810Z"
displayName: "Client for servicemaint1"
domainWideDelegation: "DELEGATION_ENABLED"
projectNumber: "aaaaaaaaaaaaaaaa"
threeLeggedOauth: "DISABLED"
updateTime: "2018-11-23T00:29:44.953175Z"
}
EDIT4: FINALLY WORKING!
So I had been trying this in a new project I created for testing all morning / last night. But my oauth2 user authenticating was running through a different project (where I also couldn't get the service account working all of yesterday morning / afternoon).
So anyway, I noticed in: https://myaccount.google.com/permissions "Apps with Access to your account" - only my old project / app was authorized. So I switched back to my first project, created a new service account client ID .json file and it finallyyy worked to authenticate both! :)
I must have that authorized that somewhere extra along the line which I had not done with the second project.
Thanks again.
EDIT5: One more quick question - is this the correct way to do this on stackoverflow? With constantly going back to edit?
Also for others stumbling upon this later, here's my total authentication block (sorry its a bit long):
putenv('GOOGLE_APPLICATION_CREDENTIALS=maintenanceapp.json');
$user = 'xyz#abc.com';
function getGoogleClient() {
return getServiceAccountClient();
}
function getServiceAccountClient() {
$user = 'xyz#abc.com';
try {
// Create and configure a new client object.
$client2 = new Google_Client();
$client2->useApplicationDefaultCredentials();
$client2->setScopes(['https://www.googleapis.com/auth/gmail.metadata','https://www.googleapis.com/auth/userinfo.email','https://www.googleapis.com/auth/userinfo.profile','https://www.googleapis.com/auth/gmail.modify','https://www.googleapis.com/auth/gmail.readonly','https://www.googleapis.com/auth/gmail.labels']);
//$client2->setAccessType('offline');
$client2->setSubject($user);
return $client2;
} catch (Exception $e) {
echo "An error occurred: " . $e->getMessage();
}
}
$newGoogleClient = getGoogleClient();
$service3 = new Google_Service_Gmail($newGoogleClient);
$results3 = $service3->users_labels->listUsersLabels($user);
/*************************************************
* Ensure you've downloaded your oauth credentials
************************************************/
if (!$oauth_credentials = getOAuthCredentialsFile()) {
echo missingOAuth2CredentialsWarning();
return;
}
/************************************************
* NOTICE:
* The redirect URI is to the current page, e.g:
* http://localhost:8080/idtoken.php
************************************************/
$redirect_uri = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'];
$client = new Google_Client();
// USER AUTH
$client->setAuthConfig($oauth_credentials);
$client->setRedirectUri($redirect_uri);
$client->setScopes(array('https://www.googleapis.com/auth/userinfo.email','https://www.googleapis.com/auth/userinfo.profile','https://www.googleapis.com/auth/gmail.readonly','https://www.googleapis.com/auth/calendar'));
$client->setApprovalPrompt('auto');
$client->setAccessType('offline');
$plus = new Google_Service_Plus($client);
/************************************************
* If we're logging out we just need to clear our
* local access token in this case
************************************************/
if (isset($_REQUEST['logout'])) {
unset($_SESSION['id_token_token']);
}
/************************************************
* If we have a code back from the OAuth 2.0 flow,
* we need to exchange that with the
* Google_Client::fetchAccessTokenWithAuthCode()
* function. We store the resultant access token
* bundle in the session, and redirect to ourself.
************************************************/
if (isset($_GET['code'])) {
$token = $client->fetchAccessTokenWithAuthCode($_GET['code']);
// store in the session also
$_SESSION['id_token_token'] = $token;
// redirect back to the example
header('Location: https://abc.de/index.php');
// return;
}
/************************************************
If we have an access token, we can make
requests, else we generate an authentication URL.
************************************************/
if (
!empty($_SESSION['id_token_token'])
&& isset($_SESSION['id_token_token']['id_token'])
) {
$client->setAccessToken($_SESSION['id_token_token']);
} else {
$authUrl = $client->createAuthUrl();
//header('Location: ' . $authUrl);
}
/************************************************
If we're signed in we can go ahead and retrieve
the ID token, which is part of the bundle of
data that is exchange in the authenticate step
- we only need to do a network call if we have
to retrieve the Google certificate to verify it,
and that can be cached.
************************************************/
if ($client->getAccessToken()) {
$token_data = $client->verifyIdToken();
}
In google developer console when you create your project and the credentials you must choose which type of client you are going to create for which type of application.
There are several different ways to authenticate to google.
OAuth2 native
OAuth2 web
Mobile
Service account
The code to use these clients is also different. You cant create a web OAuth2 client and use it for the code meant to be calling a service account.
"client is unauthorized to retrieve access tokens using this method".
Means exactly that. The client you have set up on Google developer console is either not a service account client or the code you are using is not meant for a service account client.
This is my serviceaccount.php sample. If your code needs to look something like this and you need to make sure that the client you created on the google developer console is a service account client.
require_once __DIR__ . '/vendor/autoload.php';
// Use the developers console and download your service account
// credentials in JSON format. Place the file in this directory or
// change the key file location if necessary.
putenv('GOOGLE_APPLICATION_CREDENTIALS='.__DIR__.'/service-account.json');
/**
* Gets the Google client refreshing auth if needed.
* Documentation: https://developers.google.com/identity/protocols/OAuth2ServiceAccount
* Initializes a client object.
* #return A google client object.
*/
function getGoogleClient() {
return getServiceAccountClient();
}
/**
* Builds the Google client object.
* Documentation: https://developers.google.com/api-client-library/php/auth/service-accounts
* Scopes will need to be changed depending upon the API's being accessed.
* array(Google_Service_Analytics::ANALYTICS_READONLY, Google_Service_Analytics::ANALYTICS)
* List of Google Scopes: https://developers.google.com/identity/protocols/googlescopes
* #return A google client object.
*/
function getServiceAccountClient() {
try {
// Create and configure a new client object.
$client = new Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope([YOUR SCOPES HERE]);
return $client;
} catch (Exception $e) {
print "An error occurred: " . $e->getMessage();
}
}
Developer console
Under clients check that the client you are using is one that can be found under service account keys. If not then it is the wrong client type and will not work with your code. Create a new service account client and set up domain wide delegation with that client id.
response_type=code
client_id=348268306866-9dl0kdgn2f9bjhoge7pris1jo8u9si47.apps.googleusercontent.com
redirect_uri=https://degoo.com/me/googleoauth2callback
access_type=offline
scope=https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/contacts.readonly
state={"RedirectUrl":"/me/chooseaccount","RegisterIfNotExists":true}
That’s all we know.
I am trying to get my web application to use a dedicated email account to upload files to google drive through the API. The problem being that it keeps requiring the potential users to authenticate the account that I prepared for it, which is exactly what I am trying to prevent the need for.
Any solutions I came across directed me at using the refresh token to ensure the account stays authorized. This still comes with the issue that on the first time use, an user has to authenticate said account. Saving the access token with the refresh token locally didn't work, as it still forced new users to authenticate.
the snippet below displays the code used to authenticate.
class AccessDrive
{
private $client;
//initialize the client data provided on the call of the function
private function initClient($scopes, $clientsecret, $returnUrl)
{
$client = new Google_Client();
try{$client->setAuthConfig($clientsecret);}catch(Google_Exception $e){echo $e;}
$client->setClientId(CLIENT_ID);
$client->setApplicationName(APPLICATION_NAME);
$client->setAccessType('offline');
//$client->setApprovalPrompt('force');
$client->setIncludeGrantedScopes(true); // incremental auth
foreach ($scopes as $scope) {
$client->addScope($this->getScope($scope)); //assume one or multiple from the google drive scopes
}
$client->setRedirectUri( $returnUrl);
return $client;
}
public function Login()
{
if (!$_SESSION['code']) {
$auth_url = $this->client->createAuthUrl();
header('Location: ' . filter_var($auth_url, FILTER_SANITIZE_URL));
} else {
error_reporting(-1 & ~E_WARNING);
$this->client->authenticate($_SESSION['code']);
}
return false;
}
public function __construct($returnUrl, $scopes, $clientSecret)
{
$this->client = $this->initClient($scopes, $clientSecret, $returnUrl);
$this->Login();
}
}
?>
How does one make sure that the application will not need to prompt users to authenticate, staying authenticated outside the functionality the user is supposed to access?
--> Edit:
After attempting to implement the suggested method, by DaImTo, the following code generates a Google_Service_Exception, stating that the client is unauthorized to retrieve access tokens with this method. I am not sure where that goes wrong.
class AccessDrive
{
private $client;
public function __construct( $scopes, $credentials, $email, $toImpersonate )
{
putenv( "GOOGLE_APPLICATION_CREDENTIALS=".$credentials);
$scopelist =[];
foreach ($scopes as $scope) {
array_push($scopelist, $this->getScope($scope));
}
$client = new Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope($scopelist);
$client->setSubject($toImpersonate);
$this->client = $client;
}
--> Edit:
If you downvote, I would like to know why so I can improve my current and future questions.
You are currently authencating using Oauth2. Oauth2 allows you to request access from a user to access their google drive account. Assuming they grant you access you are given an access token which allows you to access their data for an hour. If you request offline access you will be given a refresh token which you can use to request a new access token when ever the access token expires.
In your case becouse you are trying to access your own drive account always i sugest you look into using a service account instead. Service accounts are pre approved you share a folder on your google drive account with it and it will have access to that drive account as long as you dont remove the permissions. I have a post about how service accounts work Google developer for beginners service account
Example:
// Load the Google API PHP Client Library.
require_once __DIR__ . '/vendor/autoload.php';
// Use the developers console and download your service account
// credentials in JSON format. Place the file in this directory or
// change the key file location if necessary.
putenv('GOOGLE_APPLICATION_CREDENTIALS='.__DIR__.'/service-account.json');
/**
* Gets the Google client refreshing auth if needed.
* Documentation: https://developers.google.com/identity/protocols/OAuth2ServiceAccount
* Initializes a client object.
* #return A google client object.
*/
function getGoogleClient() {
return getServiceAccountClient();
}
/**
* Builds the Google client object.
* Documentation: https://developers.google.com/api-client-library/php/auth/service-accounts
* Scopes will need to be changed depending upon the API's being accessed.
* array(Google_Service_Analytics::ANALYTICS_READONLY, Google_Service_Analytics::ANALYTICS)
* List of Google Scopes: https://developers.google.com/identity/protocols/googlescopes
* #return A google client object.
*/
function getServiceAccountClient() {
try {
// Create and configure a new client object.
$client = new Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope([YOUR SCOPES HERE]);
return $client;
} catch (Exception $e) {
print "An error occurred: " . $e->getMessage();
}
}
code ripped form my sample project serviceaccount.php
Usage:
require_once __DIR__ . '/vendor/autoload.php';
session_start();
require_once __DIR__ . '/ServiceAccount.php';
$client = getGoogleClient();
$service = new Google_Service_Drive($client);
All your requests then get sent using $service
I'm trying to get an OAuth access token to import some data into the fusion table. I'm trying to use the Google API PHP client. I have created a service account for that purpose, and am using the code, mostly from the serviceAccount example:
function access_token()
{
$client = new Google_Client();
$client->setAuthClass ('Google_OAuth2');
// ^ Don't know if this line is required,
// ^ but it fails just as well without it.
$client->setApplicationName ('Mysite.dom.ain');
$client->setAssertionCredentials (new Google_AssertionCredentials
( 'MANY-NUMBERS-LETTERS-DASHES#developer.gserviceaccount.com',
array ('https://www.googleapis.com/auth/fusiontables'),
file_get_contents ('path/to/my/privatekey.p12') ));
$client->setClientId ('NUMBERS-LETTERS-DASHES.apps.googleusercontent.com');
$client->authenticate();
// ^ Also fails equally good with and without this line.
return $client->getAccessToken();
}
A little debug output shows that $client->authenticate() returns true, but $client->getAcessToken() returns null. No exceptions are thrown. I have the feeling I'm doing something fundamentally wrong. If so, please forgive my stupidity and point me in the right direction.
You don't need the authenticate() call, but you'll need to call refreshTokenWithAssertion() to refresh the underlying access token. If you are using the client library to make signed requests, it will lazily make this call for you if underlying access token has expired.
The API requests to refresh the access_token are expensive, and have a low quota, so you'll want to cache the access_token.
// Set your client id, service account name, and the path to your private key.
// For more information about obtaining these keys, visit:
// https://developers.google.com/console/help/#service_accounts
const CLIENT_ID = 'INSERT_YOUR_CLIENT_ID';
const SERVICE_ACCOUNT_NAME = 'INSERT_YOUR_SERVICE_ACCOUNT_NAME';
// Make sure you keep your key.p12 file in a secure location, and isn't
// readable by others.
const KEY_FILE = '/super/secret/path/to/key.p12';
$client = new Google_Client();
$client->setApplicationName("Google FusionTable Sample");
// Set your cached access token. Remember to store the token in a real database instead of $_SESSION.
session_start();
if (isset($_SESSION['token'])) {
$client->setAccessToken($_SESSION['token']);
}
$key = file_get_contents(KEY_FILE);
$client->setAssertionCredentials(new Google_AssertionCredentials(
SERVICE_ACCOUNT_NAME,
array('https://www.googleapis.com/auth/fusiontables'),
$key)
);
$client->setClientId(CLIENT_ID);
if ($client->getAuth()->isAccessTokenExpired()) {
$client->getAuth()->refreshTokenWithAssertion();
}
// Get the json encoded access token.
$token = $client->getAccessToken();
I think all you did was correct, now you have two options left:
Use your $client to make a service call with something like that
$service = new Google_FusiontablesService($client);
$selectQuery = "select * from 1AwxQ46kfmPoYoq38e5CopJOWkCo_9GUU_ucD6zI";
$service->query->sql($selectQuery)
Or call the internal function refreshTokenWithAssertion() in order to get your token:
$client::$auth->refreshTokenWithAssertion();
$token = $client->getAccessToken(); //this should work now
For both cases I have examples in my GitHub Repo.