I am trying to integrate several Google API calls into a custom Drupal 8 module.
I am basically trying to first get my custom class to get an access token from Google via OAuth before I try do anything else. I am doing this by using a class function with everything simply in one place. The function is as follows:
public function testTokenRequest(): void
{
// Setup Google Client Config within context of initialized class
$this->googleClient->setClientId($this->googleClientID);
$this->googleClient->setClientSecret($this->googleClientSecret);
$this->googleClient->setDeveloperKey($this->googleApiKey);
// Add Google MyBusiness scope
$this->googleClient->setScopes(array('https://www.googleapis.com/auth/plus.business.manage'));
try {
$accessToken = $this->googleClient->getAccessToken(); // null returned where breakpoint hit
$this->googleAccessToken = $accessToken; // Put xdebug breakpoint here
} catch (Exception $exception) {
echo $exception->getMessage();
}
}
Currently all I get is a null returned for the $accessToken = $this->googleClient->getAccessToken(); call.
Unsure where I am going wrong, possibly the AddScopes call because the vendor documentation for the apiclient does this slightly differently, i.e. $client->addScope(Google_Service_Plus::PLUS_ME); but I couldn't find the correct class to use for the MyBusinessAPI scope so used the OAuth playground string instead https://www.googleapis.com/auth/plus.business.manage
I get an OAuth Playground AccessToken returned when I use that but end up with a Permission Denied error instead even though I have the GMB API added to my whitelist under credentials.
Google MyBusiness is Oauth2 based. The access token is not received until the user has approved your app, it is normal that the function returns null if the user has not approved the app yet.
Here is an example on how you create a link where to send the user to start authentication and authorization for your app.
$client = new \Google_Client();
$client->setAuthConfig(getcwd() . '/../client_secret.apps.googleusercontent.com.json');
$client->setAccessType("offline"); // offline access
$client->setIncludeGrantedScopes(true); // incremental auth
$client->addScope(
array(
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/plus.business.manage'
)
);
$client->setRedirectUri('http://server.com/code');
$client->setApprovalPrompt('force');
return new Response(
'<html><body>Authenticate here : <a href="' .
$auth_url = filter_var($client->createAuthUrl(), FILTER_SANITIZE_URL)
. '">HERE</a></body></html>'
);
The example above assumes your server will also implement a /code endpoint where the user is redirected to with the authorization token, then you need to call the api to exchange the token with the access and refresh code.
This document will help you understand further
https://developers.google.com/api-client-library/php/auth/web-app
Related
As detailed in my question to the the Google API team, I would like to work out a way to avoid redirects.
In theory this should be possible as an Authentication Code from one client (JavaScript) should be agnostic of the client and thus it should work if passed to the PHP client to fetch the access and refresh tokens.
Steps in theory:
Client gets an authorization code
Client exchanges the authorization code for the access and refresh tokens
How am I attempting this?
Run the JavaScript client to get the Authentication token
GoogleAuth = gapi.auth2.getAuthInstance()
GoogleAuth.grantOfflineAccess({
scope: 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/plus.business.manage https://www.googleapis.com/auth/plus.me openid email profile'
}).then(function (resp) {
var auth_code = resp.code;
console.log("AuthCode:" + auth_code)
})
At this point i get a code, not sure if this is an authorization code or access token but i cannot see any other function in the Javascript library that is Authorization token explicit/specific.
Use the Authentication Token in the PHP API Library
$error = $request->get('error');
$code = $request->get('code');
if($error){
throw new Exception('Error from authenticating ' . $error);
}
$client = new \Google_Client();
$client->setAuthConfig(getcwd() . '/../client_secret.apps.googleusercontent.com.json');
$client->setAccessType("offline"); // offline access
$client->setIncludeGrantedScopes(true); // incremental auth
$client->addScope(
array(
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/plus.business.manage'
)
);
$client->setRedirectUri('http://myserver.com/code');
$client->setApprovalPrompt('force');
$client->fetchAccessTokenWithAuthCode($code);
$accessToken = $client->getAccessToken();
return new Response(
"<html><body>Authenticated with code : " . $code . "<br/>\n\n".
" Access Token is : ". var_export($accessToken, true) . "</body></html>"
);
Access token is still null. the line $accessToken = $client->getAccessToken(); returns null or false.
This works in the full PHP based version but the PHP version is based on creating a link that the user needs to follow, the user is then on the Google server and can approve the app, when approved the user is redirected back to the app. Then the app receives the Authentication code.
I just would like to avoid redirects due to the architecture of single pages apps or just preference. The only alternative I can think of is to open a popup and notify the original window when access and refresh codes are returned so that the PHP client can go on querying the API, but it is an ugly solution IMHO.
Is there another way to get an authorization code that works on PHP but obtained from JavaScript?
The initial task was as simple as this: get the latest post from a Google+ page.
It took 3 days now to find out that all examples on the Internet seem to be either outdated or wrong or not valid. Google developer docs also don't provide much help, complicating things more and more with every new confusing documentation page. So, guys, I'm giving up.
First I tried to implement OAuth 2.0 procedure which was documented in their docs (https://developers.google.com/identity/protocols/OAuth2WebServer). As its title implies it is exactly about connecting from a server app. I followed it, and at first glance, it worked: I got the back call, successfully authenticated, fetched access token and stored it and made a simple call to fetch the posts.
// Initialization
$this->googleClient = new Google_Client();
$this->googleClient->setAuthConfig(Json::decode($config->get('client_json')));
$this->googleClient->setAccessType('offline');
$this->googleClient->setIncludeGrantedScopes(TRUE);
$this->googleClient->addScope('https://www.googleapis.com/auth/plus.me');
$this->googleClient->setRedirectUri(Url::fromRoute('mymodule.gplus.callback')->setAbsolute()->toString());
// The callback
$client->authenticate($code);
$accessToken = $client->getAccessToken();
(The only thing which seemed silly here - is the scope. I had no idea what scope should I claim for if I need to just read a public post from a public page, so I just picked the first random entry which looked related.)
As I said I got the token and could fetch my posts:
// Using Google_Service_Plus
$this->client()->setAccessToken($access_token);
$this->googleServicePlus = new Google_Service_Plus($this->client($reset));
$this->googleServicePlus->activities->listActivities($endpoint, 'public', ['maxResults' => 1]);
But after 1 hour it just stopped working claiming that the token is outdated or something and it needs to be refreshed. And here comes the showstopper: I found no way to refresh the token. $response from authenticate() doesn't return refresh token anymore (although it's been mentioned many times in other answers) so I don't even have a way to refresh it.
I tried digging in the library (from my composer.json: "google/apiclient": "^2.0") and figured out that authenticate() method is actually deprecated there are few other methods which seem to play with tokens. I tried \Google_Client::fetchAccessTokenWithAssertion() which asked for some Application Default Credentials... which leads us to completely different topic and way of authentication described here: https://developers.google.com/identity/protocols/OAuth2ServiceAccount
So should I abandon everything which I did and now implement something new? How could I just do this simple task of fetching news?
Sorry for the long question.
The process you are following is good. The problem you are having is refreshing the token. Although the official documentation states:
If you use a Google API Client Library, the client object refreshes the access token as needed as long as you configure that object for offline access.
It does not explain how to do it using the PHP Client Library. This was a problem for me too so this is the approach I'm taking and hopefully it can help you.
// 1. Build the client object
$client = new Google_Client();
$client->setRedirectUri('http://' . $_SERVER['HTTP_HOST'] . '/index.php');
$client->setAuthConfig("client_secret.json");
$client->addScope($scopes);
$client->setAccessType("offline");
I normally save the Access Token to the session, therefore before proceeding, I check if the access token is already saved to the session. If it is, then I proceed to check if the access token is already expired. If it is, then I proceed to refresh the access token, and then I proceed to make the API Call.
// 2. Check if the access token is already saved to session
if( isset($_SESSION["access_token"]) && ($_SESSION["access_token"]) ) {
//set access token before checking if already expired
$client->setAccessToken($_SESSION["access_token"]);
//check if access token is already expired and refresh if so
if ($client->isAccessTokenExpired()) {
$refreshToken = $_COOKIE["refresh_token"]; //get refresh token
$client->refreshToken($refreshToken); // refresh the access token
}
//get new access token and save it to session
$_SESSION['access_token'] = $client->getAccessToken();
// set access token after checking if already expired
$client->setAccessToken($_SESSION["access_token"]);
$plusService = new Google_Service_Plus($client);
$optParams = array(
"maxResults" => 5,
"pageToken" => null
);
$activitiesList = $plusService->activities->listActivities("+cnn", "public", $optParams);
$activities = $activitiesList->getItems();
foreach ($activities as $activity ) {
print_r($activity);
print "<br>**********************<br>";
}
}
If the access token is not saved to the session, this means that the authentication and authorization has not taken place, so I proceed to authenticate the user.
// 3. Authenticate user since access token is not saved to session
else {
if( !isset($_GET["code"]) ){ //get authorization code
$authUrl = $client->createAuthUrl();
header('Location: ' . filter_var($authUrl, FILTER_SANITIZE_URL));
} else { //exchange authorization code for access token
$client->authenticate($_GET['code']); //authenticate client
//get access token and save it to session
$_SESSION['access_token'] = $client->getAccessToken();
//save refresh token to a Cookie
$refreshToken = $_SESSION["access_token"]["refresh_token"];
setcookie("refresh_token", $refreshToken, time() + (86400 * 30), "/");
$redirect_uri = 'http://' . $_SERVER['HTTP_HOST'] . '/index.php';
header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL));
}
}
Please note: For demonstration purposes, I'm saving the refresh token to a cookie in this example; However, it is well known that you should not save this info to a cookie but instead to a secure database. Also, the authenticate() method is not deprecated, it's just an alias for the method fetchAccessTokenWithAuthCode(). Another thing, the scope you are using is not silly, since you are fetching info from a public page, according to the documentation here and here, I intuited that I should only allow access to Know who you are on Google https://www.googleapis.com/auth/plus.me.
I created a Google API Console project and client ID with web application type.Then Using OAuth 2.0 Playground - Google Developers I authorized to drive, sheet and calendar scopes using my client id.
Also, Service account client id and scopes added and authorized in G Suite.
I tried to list files in a folder in the drive using the below sample
index.php
<?php
require_once 'vendor/autoload.php';
require_once 'vendor/google/apiclient/examples/templates/base.php';
$service = get_service_document();
$folderid='FOLDER_ID';
try {
$children1 = $service->files->listFiles(array(
'q' => "'$folderid' in parents "));
$filearray1 = $children1;
}
catch(Exception $e){
echo $e->getMessage();
}
print_r($children1);
exit;
function buildServiceDrive($userEmail,$service_id,$scope,$service_filename) {
$client = new Google_Client();
putenv("GOOGLE_APPLICATION_CREDENTIALS=".$service_filename);
if ($credentials_file = checkServiceAccountCredentialsFile()) {
// set the location manually
$client->setAuthConfig($credentials_file);
}
elseif (getenv('GOOGLE_APPLICATION_CREDENTIALS')) {
// use the application default credentials
$client->useApplicationDefaultCredentials();
}
else {
echo missingServiceAccountDetailsWarning();
return;
}
$client->setApplicationName("DRIVE");
$client->setScopes('https://www.googleapis.com/auth/drive');
$client->setSubject($userEmail);
return new Google_Service_Drive($client);
}
//COMMON FUNCTION TO CREATE CALENDAR ID
function get_service_document(){
$userstamp='user#domain.com';
$driveService =buildServiceDrive($userstamp,'','','project-id-451a5f6b12ce.json';
return $driveService;
}
But I got this issue
{
"error": "unauthorized_client",
"error_description": "Client is unauthorized to retrieve access tokens using this method."
}
I m getting this issues newly created Google API Console project only
Please help me to solve this.
Thanks in advance
This is a common error when running an API call with a service account but not properly completing the domain-wide delegation (DWD) or because the authorization in the admin console has not propagated yet.
This article explains in details the process of DWD. If you have done that, wait 24 hours and it should work. If it doesn't work after that, then it must be something else but as far as I can say right now, the DWD process is the issue.
PLEASE NOTE: DWD is available only to G Suite customers. If you are using a consumer gmail.com account, you won't be able to do this. Instead, you'll have to go through the user consent OAuth flow.
This error could also occur if API client only have write permissions and in scope you specify that you only need readonly access.
{
"error": "unauthorized_client",
"error_description": "Client is unauthorized to retrieve access tokens using this method."
}
Short version: What would be the appropriate way to send the JWT generated from Facebook login (laravel/socialite) to the angularjs front end without using session.
Long Version
I am making an app that has angularjs front end and laravel 5.2 backend. I am using tymondesigns/jwt-auth for authentication instead of session.
I am also using laravel/socialite for social Facebook authentication. For that I am using the stateless feature of socialite so that I don't need session in any ways.
The basic authentication works perfectly. But, when I try to use Facebook login, I follow these steps
User clicks on a button on the angular side that redirects to the provider login page of the back end.
public function redirectToProvider() {
return Socialite::with('facebook')->stateless()->redirect();
}
2. User gives his login information. After logging in he is redirected to my handlecallback function.
try {
$provider = Socialite::with('facebook');
if ($request->has('code')) {
$user = $provider->stateless()->user();
}
} catch (Exception $e) {
return redirect('auth/facebook');
}
return $this->findOrCreateUser($user);
Next I use the findorcreate function to determine whether the user exists or not. If not than I just create a new user and create JWT from that.
$user = User::where('social_id', '=', $facebookUser->id)->first();
if (is_object($user)) {
$token = JWTAuth::fromUser($user);
return redirect()->to('http://localhost:9000/#/profile?' . 'token=' . $token);#angular
} else {
$result = array();
$result['name'] = $facebookUser->user['first_name']
$result['email'] = $facebookUser->user['email'];
$result['social_id'] = $facebookUser->id;
$result['avatar'] = $facebookUser->avatar;
$result['gender'] = $facebookUser->user['gender'];
$result['status'] = 'active';
$result['login_type'] = 'facebook';
$result['user_type'] = 'free_user';
try {
$user = User::create($result);
} catch (Exception $e) {
return response()->json(['error' => 'User already exists.'], HttpResponse::HTTP_CONFLICT);
}
$token = JWTAuth::fromUser($user);
return redirect()->to('http://localhost:9000/#/profile?' . 'token=' . $token);#angular
}
My problem is, in the last block of code I am having to send the jwt to my frontend via url. Which isn't secure at all. What would be the right way to send the generated JWT to the frontend without using session. Thank you
The official documentation of Laravel Socialite says:
Stateless Authentication
The stateless method may be used to disable session state verification. This is useful when adding social authentication to an API:
return Socialite::driver('google')->stateless()->user();
Then, you can authenticate using the jwt-auth method:
JWTAuth::fromUser($user)
If you're using $http on the Angular side, try returning the token as a JSON response from Laravel:
return response()->json(compact('token'));
Then store the token in localStorage or sessionStorage or what have you.
If you're generating your Angular page from within Laravel (i.e. not using Laravel as an API, but showing your Angular page from /public/index.php, for instance) you could load the view with the token in the data for the view.
As long as you're using HTTPS either of these two scenarios are better than passing the token in the redirect URL.
You can store token and use client side redirect without storing to browser history to redirect user to profile page without token in URL:
document.location.replace({profile-url})
I am working with this package Analytics-Laravel 4 for google analytics and I have follower all of the steps correctly. When I try to get the site id for example, I face this error:
Error refreshing the OAuth2 token, message: '{ "error" : "invalid_grant" }'
I have double checked all of the configurations, client id, service_account and private key but the error still occurs.
Anything else I should try to check that maybe would solve this issue?!
I didn't use this package before, I'm using google-api-php-client, but anyways, this error occurs if you don't set the refresh token.
You should know that you need to have the access token ONLY once. You also need to set the access type to be offline, which will provide you with a refresh token that you will use to automatically get a new access token without getting a new code every time your access token expires.
In google's console, I created a Client ID for web application. Make sure you set the redirect URI to your web page where you will receive the code and will extract the access token using that code.
Here is a code example using google-api-php-client, I hope it will help:
You need to run the following code only once, and retrieve and store the access token.
<?php
require_once('google-api-php-client-master/src/Google/Client.php');
session_start();
$client = new Google_Client();
$client->setApplicationName('APP_NAME');
$client->setClientId(YOUR_CLIENT_ID);
$client->setClientSecret('YOUR_CLIENT_SECRET');
$client->setRedirectUri('YOUR_REDIRECT_URI');
$client->setDeveloperKey('YOUR_DEV_KEY');
$client->setScopes(array('https://www.googleapis.com/auth/analytics.readonly'));
$client->setAccessType("offline");
// Step 1: Create an auth url
if (isset($_GET['ref']) && $_GET['ref'] == "1") {
$authUrl = $client->createAuthUrl();
return Redirect::to($authUrl);
}
// Step 2: The user accepted your access now you need to exchange it.
if (isset($_GET['code'])) {
$client->authenticate($_SESSION['code']); //Authenticate the client
$token = $client->getAccessToken(); //Get the access token
var_dump($token); //Store the token in your DB or config file
die();
}
?>
After getting your access token from the code above (which should contain a refresh token), store it in your DB or a config file.
Now the following code should authenticate the client and refresh the access token when it expires via the getAccessToken function
<?php
require_once('google-api-php-client-master/src/Google/Client.php');
require_once('google-api-php-client-master/src/Google/Service/Analytics.php');
$client = new Google_Client();
$client->setApplicationName('APP_NAME');
$client->setClientId(YOUR_CLIENT_ID);
$client->setClientSecret('YOUR_CLIENT_SECRET');
$client->setRedirectUri('YOUR_REDIRECT_URI');
$client->setDeveloperKey('YOUR_DEV_KEY');
$client->setScopes(array('https://www.googleapis.com/auth/analytics.readonly'));
$client->setAccessType("offline"); //Make sure the access type is offline to get a refresh token
$config = CoreConfig::find(1); //Getting the first record from the config table
$client->setAccessToken($config->google_access_token); //Retrieve the access token that you stored and set it to the client object
//Check this the token is expired
if($client->isAccessTokenExpired()) {
$token = json_decode($config->google_access_token, true); //Get the token stored, and convert JSON to array
$client->refreshToken($token['refresh_token']); //Set the refresh token
$newtoken = $client->getAccessToken(); //Call the getAccessToken() function to get a new access token for you
$config->update(array('google_access_token' => $newtoken)); //Store the new token in your DB
}
if ($client->getAccessToken()) {
$analytics = new Google_Service_Analytics($client);
//Do something with the $analytics object
}
?>
It could be the server time. If the local time on your server is out of sync with google's oAuth server even by a few seconds you'll get that error message.
You can check the time by running "date" in the console.
Running "sudo ntpdate ntp.ubuntu.com" solved it for us.