I'm trying to create a Laravel API to interact with Gmail via the Google API client library for PHP.
However, I encounter issues with the endpoint to get mails.
So far, I could do authorization and get the access token in my DB. This endpoint return JSON of this format:
{
"user_id": 1,
"token": {
"access_token": "ya29.XXXXXXXXXXXX-K_P9Z0Rd0nU5WzSvU3TMlC0TZQRbMLkDHE1XI3j29mbIRP13dt_NGMb4d9trTECGKiwbjM45Ijk7fbhpLzU2JL7w-6w_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"expires_in": 3599,
"scope": "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/gmail.readonly",
"token_type": "Bearer",
"id_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-yK6GHPRATk64GaPcYCuIVyqTYNNvfvr8gCVuTCKr3RMtUz5J97ROZkJRN5w_AQvyJFL369MNTnHQAiqJoPIN2Wes0OAbeq1urpDRMRaAfF6Nuqun-pmewI8g5OCjkUMqekZTKlzCqRl7Xkm4qQRrwk66SxwGdC78Iy5Z_3VG1OIj681zoY18dQu9_ZqGMnwVuHmffmzNInuAmqHcQ7CLM_I_vJbWV3-UKVRF2UtjHvmUydCdo4PrEpL371i8exwPme5IK_xMcywxdfdjxm4duyv6X_ky2MCYwANNBSyBLMYh214FZPowL8choHmMIms-biJlg",
"created": 1647922921
},
"name": "gmail",
"updated_at": "2022-03-22T04:22:01.000000Z",
"created_at": "2022-03-22T04:22:01.000000Z",
"id": 10
}
My second endpoint is to get the mails by using the access token.
My implementation looks like this.
// Services
public function getEMails($user_id, $access_token){
$this->client->setAccessToken($access_token);
$service = new Gmail($this->client);
$messages = $service->users_messages->listUsersMessages($user_id);
return $messages;
}
// Controller
public function getMails(WebService $web_service, GmailServices $gmail_services){
$access_token = $web_service->token['access_token'];
$user_id = 'me';
if (isset($access_token)){
$mails = $gmail_services->getEMails($user_id, $access_token);
}
return $mails;
}
// api.php (In route middleware)
Route::get('/web-service/{mails}', [WebServiceController::class, 'getMails'])->name('web-service.getMails');
Hitting this endpoint, I get the below error.
"message": "Trying to access array offset on value of type null",
"exception": "ErrorException"
which corresponds to this line
$access_token = $web_service->token['access_token'];
I did some minor research, but I couldn't resolve it. Unfortunately, I'm not advanced in Laravel development, and I'll appreciate any help and feedback.
Furthermore, I equally want to add that my implementation is largely based on Bitfumes test-driven API development on YouTube. Here is the link to the repo https://github.com/bitfumes/laravel-test-driven-api
Thanks!
After trying the above, I was expecting to get the mails and equally work on pagination or querying via LIMIT so that the backend will not over-stress to get all mails at once.
Everything you do should be going though the Google_Client object. Your code is a little confusing to me you have two getEMails methods and neither appear to be using assigning things properly though the client.
When properly initialized it is able to handle everything for you.
$client = new Google_Client();
$client->setApplicationName('Gmail API PHP Quickstart');
$client->setScopes(Google_Service_Gmail::GMAIL_READONLY);
$client->setAuthConfig('credentials.json');
$client->setAccessType('offline');
$client->setPrompt('select_account consent');
You should be testing if the access token has expired. If it is expired then have the Google_Client request a new one.
if ($client->isAccessTokenExpired()) {
// Refresh the token if possible, else fetch a new one.
if ($client->getRefreshToken()) {
$client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
} else {
// request access of the user if it has not been granted.
}
// note after refresh remember to store the new refresh token.
}
Related
For my online platform, I'm setting up a basic google sign-in button whereby users can create an account or log-in using their gmail email address.
I created my credentials: OAuth 2.0 Client IDs (got both ID and secret) and Service Accounts (now removed - not needed when asking access approval from user).
For the oauth consent screen, I've added my non-sensitive scopes:
./auth/userinfo.email
./auth/userinfo.profile
openid
I'm using my own gmail address for testing and API url used is https://www.googleapis.com/oauth2/v2/userinfo? with query on the email field.
EDIT: I've removed the service account so ignore this:
I have setup my jwt sign too with the service account email (XXX-googleconnect#genial-analyzer-341XXX...) and the (pretty damn long) generated key.
Could you please tell me how to debug the error "message":"Undefined index: jwt" when I run the google API?
EDIT: I've amended my code to reflect the removal of the service account. So no more jwt token: "jwt_bearer": "false",
<?php
$exports = <<<'JSON'
{
"name": "oauth",
"module": "oauth",
"action": "provider",
"options": {
"jwt_bearer": "false",
"service": "google",
"client_id": "307540412021-test06test06test06test06test06.apps.googleusercontent.com",
"client_secret": "XXXXX-X0RMDgT1nEgxAKrOmpcjK4sXY7Qq",
"tokenHandling": "self"
},
"meta": [
{
"name": "access_token",
"type": "text"
}
],
"outputType": "object"
}
JSON;
?>
But now I get "message":"Undefined index: false",
Disclaimer: I'm a no-coder, with basic php knowledge, pls if you could possibly use as easy to understand a language, that would be highly appreciated (and helpful for other no-coders too I bet)
Thanks in advance.
What you need to understand is that there is a big diffrence between Service account authoirzation, Oauth2, and signin.
If you run oauth2 authorization and request access of the using the email and profile scopes your going to have access to the users profile information.
if you try to run service account authentication and request the email and profile scopes your either going to get an error or your going to get the service accounts profile information. As a service account is a dummy user.
"message":"Undefined index: jwt"
Implies to me that you are using the wrong code for the wrong client.
service account client credentils
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();
}
}
Oauth2 client
function getGoogleClient() {
$client = getOauth2Client();
// Refresh the token if it's expired.
if ($client->isAccessTokenExpired()) {
$client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
file_put_contents($credentialsPath, json_encode($client->getAccessToken()));
}
return $client;
}
/**
* Builds the Google client object.
* Documentation: https://developers.google.com/identity/protocols/OAuth2
* Scopes will need to be changed depending upon the API's being accessed.
* Example: 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 buildClient(){
$client = new Google_Client();
$client->setAccessType("offline"); // offline access. Will result in a refresh token
$client->setIncludeGrantedScopes(true); // incremental auth
$client->setAuthConfig(__DIR__ . '/client_secrets.json');
$client->addScope([YOUR SCOPES HERE]);
$client->setRedirectUri(getRedirectUri());
return $client;
}
As you can see the code is completely different for each type of authorization.
My main issue is why are you even trying to mix service account authorization into this if you are signing users into your app?
Should you be using a Google service account?
I'm building an application that has a simple email-pass login. I want to use the Microsoft Graph to return the profile information of a user that has logged in (I'm not using office365 OAuth right now).
I've set up an action that tries to fetch this user data (i.e given name or profile photo) from an organization I'm part of. I got this error:
"line":113,
"message":"Client error: GET https://graph.microsoft.com/v1.0/users/{user}
resulted in a 401 Unauthorized response:
{
"error": {
"code": "Authorization_IdentityNotFound",
"message": "The identity of the calling application (truncated...)"
}
}
I've set up the application as specified on the Microsoft Graph tutorial (step 2) and this the code I've written, using the repository readme:
class MsGraphService implements iAction
{
protected $accessToken;
public function __construct()
{
$guzzle = new \GuzzleHttp\Client();
$url = 'https://login.microsoftonline.com/'.ENV('MSGRAPH_TENAND_ID').'/oauth2/token?api-version=1.0';
$token = json_decode($guzzle->post($url, [
'form_params' => [
'client_id' => ENV('MSGRAPH_CLIENT_ID'),
'client_secret' => ENV('MSGRAHP_CLIENT_SECRET'),
'resource' => 'https://graph.microsoft.com/',
'grant_type' => 'client_credentials',
],
])->getBody()->getContents());
$this->accessToken = $token->access_token;
}
public function execute($data)
{
$graph = new Graph();
$graph->setAccessToken($this->accessToken);
$user = $graph->createRequest("GET", sprintf("/users/%s",$data['email']))
->setReturnType(Model\User::class)
->execute();
return $user->getGivenName();
}
}
The accesstoken attribute was caught, so I think the app configuration is ok.
Maybe is an issue about API permissions, specifically: Application permissions?
I've also asked about this on GitHub.
You are probably running into problems because you are trying to use the AzureAD V1 authentication endpoint. It is much easier to get the AzureAD v2 endpoint to work with Microsoft Graph. There are details on how to do it here https://learn.microsoft.com/en-us/graph/tutorials/php?tutorial-step=3
The main different between V1 and V2 is that you no longer use a resource parameter and you use scopes instead to say want you want access to.
When trying to fetch user related information using the API (no signed-in user) you should ask an administrator for permissions to read that information. In order to set the correct permissions to your application you should:
Go to https://portal.azure.com and login
Click on Azure Active Directory
Go to App Registrations
Select your existing appliaction and click on View API permissions
Select + Add a permission / Microsoft Graph / Application Permissions / User / User.Read.All
Ask an administrator for consent
With that, you will be able to fetch users data from your application without a signed-in user.
I have a problem with google api and oauth2 token.
There is an app which allows to synchronize contacts / calendar with your google account by oauth2 token.
When first time user wants to connect with his google account, he needs to grant access , then app is receiving code/token which is saved and will be used for offline synchronization later.
function getClient($app)
{
$client = new Google_Client();
$client->setAuthConfig("path_to_secret.json");
switch($app)
{
case 'contacts':
$client->addScope(Google_Service_Script::WWW_GOOGLE_COM_M8_FEEDS);
$client->addScope(Google_Service_People::USERINFO_EMAIL);
break;
case 'calendar':
$client->addScope(Google_Service_Calendar::CALENDAR);
break;
default:
throw new Exception('API Callback not defined in setup');
}
$client->setAccessType('offline'); // offline access
$client->setIncludeGrantedScopes(true); // incremental auth
$client->setRedirectUri(GOOGLE_APP_URL . $app.'/callback.php');
return $client;
}
(there are different tokens for contacts and calendar)
The synchronization script:
...
try
{
$client = getClient('calendar');
$client->setAccessToken(unserialize($accessToken));
$http = $client->authorize();
$service = new Google_Service_Calendar($client);
...
}
$accessToken is a serialized string like:
a:5:{s:12:"access_token";s:131:"******token_here********";s:10:"token_type";s:6:"Bearer";s:10:"expires_in";i:3598;s:8:"id_token";s:902:"***id_token****";s:7:"created";i:1505178047;}
This is working for first time and couple more times but after some time(hours) there is an error:
Error: {"error": { "errors": [ { "domain": "global", "reason": "authError", "message": "Invalid Credentials", "locationType": "header", "location": "Authorization" } ], "code": 401, "message": "Invalid Credentials" }}
What I am doing wrong?
What is interesting that for contacts synchronization works fine all the time (access token has the same attributes as in calendar synchronization )
Ok, propably solved - refresh_token is provided for the first time only, so when I was testing it more times then I didn't get refresh token.
When I revoked access in https://myaccount.google.com/u/0/permissions and connected again then I received also refresh token. I assume now it will work properly
I am trying to create a web-app with a simple dashboard with Analytics data for the accounts who logged in with Google. I am using Laravel with Socialite package, and I can log the users in with Google currently. I have my developer client-key and client-secret. I set scopes for Analytics read-only, and offline access, and I store customer name, email, Google ID, access token and refresh token in my database. I can log the user in without any problem.
What I want to do is for now, just access the profiles an Analytics account currently has. I followed Analytics API documentation examples but could not get it working. Since I am storing an access token and a refresh token, I think I should be able to authenticate the current user and get their Analytics data, but I could not find any simple methods from the Client and Analytics libraries. I will need to access their Analytics data offline, and this is why I think I should be able to authorize my requests with access token and refresh token, but I do not get any Analytics specific data from user login process. I am completely lost now, how do I authorize my requests to Anayltics API? I have been using AdWords API for more than 8 months, and everything is crystal clear in AdWords API documentation, but I could not get anything working with Analytics API.
These are my user login methods:
public function redirectToProvider()
{
$parameters = ['access_type' => 'offline'];
return Socialite::driver('google')
->scopes(['https://www.googleapis.com/auth/analytics.readonly'])
->with($parameters)
->redirect();
}
/**
* Obtain the user information from Google.
*
* #return Response
*/
public function handleProviderCallback()
{
$outsiderLogin = Socialite::driver('google')->stateless()->user();
$user = User::where('googleID', $outsiderLogin->id)->first();
// Register the user if there is no user with that id.
if (!$user) {
$user = new User;
$user->name = $outsiderLogin->name;
$user->googleID = $outsiderLogin->id;
$user->email = $outsiderLogin->email;
$user->token = $outsiderLogin->token;
$user->refreshToken = $outsiderLogin->refreshToken;
$user->save();
}
// Log the user in.
Auth::login($user);
return redirect('/home');
}
Thank you very much.
I have found the solution for now. At first, I figured that I needed the code that returns with authentication URL from Google, and when I inspect the Socialite package, I have found a protected method getCode() in \vendor\laravel\socialite\src\Two\AbstractProvider.php, which returns the code from the URL. I edited the source file of the package and changed the method type from protected to public, and that made it possible to use that method outside of the class, which allowed me to access the code from the URL, then store it in DB for further authentication requirements. But there were issues with this setup, first of all, I should find a way to keep that package without any update, since any update will rollback the changes I made to the source file. The second problem I faced was the way I store tokens. By default, Google Client API returns an array which contains the fields access_token, refresh_token, expires_in, id and created, and with these fields, it authenticates the requests to Analytics server. In my scenario, there were no standard array returning from the basic Socialite login process. There were access_token, refresh_token and expires variables and I stored them all in my database as well. This caused an issue with Google library, it asked for a structured array and I did not even have the variables expires_in and created, this is why I setup a fake array which tells Google to refresh token with every request, and this was not a good practice either.
At the end, I could not understand how to use any package online and I wrote my own simple authentication, and I do not know if it has any vulnerabilities, but it works for me, it may also work for those who needs it.
Here are my routes lines:
Route::get('auth/google', [
'as' => 'googleLogin',
'uses' => 'Auth\AuthController#redirectToProvider'
]);
Route::get('auth/google/callback', [
'as' => 'googleLoginCallback',
'uses' => 'Auth\AuthController#handleProviderCallback'
]);
And these are the AuthController methods:
/**
* Redirect the user to the Google authentication
*/
public function redirectToProvider()
{
// Create the client object and set the authorization configuration from JSON file.
$client = new Google_Client();
$client->setAuthConfig('/home/vagrant/Analytics/client_secret.json');
$client->setRedirectUri('http://' . $_SERVER['HTTP_HOST'] . '/auth/google/callback');
$client->addScope(Google_Service_Analytics::ANALYTICS_READONLY);
$client->addScope("email");
$client->addScope("profile");
$client->setAccessType("offline");
$auth_url = $client->createAuthUrl();
return redirect($auth_url);
}
/**
* Obtain the user information from Google.
*
* #return redirect to the app.
*/
public function handleProviderCallback()
{
// Handle authorization flow from the server.
if (! isset($_GET['code'])) {
return redirect('auth/google');
} else {
// Authenticate the client, and get required informations.
$client = new Google_Client();
$client->setAuthConfig('/home/vagrant/Analytics/client_secret.json');
$client->authenticate($_GET['code']);
// Store the tokens in the session.
Session::put('token', $client->getAccessToken());
$service = new Google_Service_Oauth2($client);
$userInfo = $service->userinfo->get();
$user = User::where('googleID', $userInfo->id)->first();
// If no match, register the user.
if(!$user) {
$user = new User;
$user->name = $userInfo->name;
$user->googleID = $userInfo->id;
$user->email = $userInfo->email;
$user->refreshToken = $client->getRefreshToken();
$user->code = $_GET['code'];
$user->save();
}
Auth::login($user);
return redirect('/home');
}
}
I have placed the client_secret.json file I have downloaded from Google API Console into the specified folder, this may be different for you. I have also modified the migration file in order to match the required segemnts. After these steps, I am able treat that user as it is a simple user that registered with the basic Laravel auth.
Now I can query, say, the accounts in the user's Google Analytics account like this:
/**
* #var $client to be authorized by Google.
*/
private $client;
/**
* #var $analytics Analytics object to be used.
*/
private $analytics;
public function __construct()
{
$this->client = $this->AuthenticateCurrentClient();
$this->analytics = new Google_Service_Analytics($this->client);
}
private function AuthenticateCurrentClient(){
$user = Auth::user();
$token = Session::get('token');
// Authenticate the client.
$client = new Google_Client();
$client->setAccessToken($token);
$client->authenticate($user->code);
return $client;
}
public function GetAccounts(){
try {
$accountsObject = $this->analytics->management_accounts->listManagementAccounts();
$accounts = $accountsObject->getItems();
return $accounts;
} catch (apiServiceException $e) {
print 'There was an Analytics API service error '
. $e->getCode() . ':' . $e->getMessage();
} catch (apiException $e) {
print 'There was a general API error '
. $e->getCode() . ':' . $e->getMessage();
}
}
There were thousands of times Stack Overflow has helped me, I hope this helps someone to get things working.
You're not really going to find what you're looking for with the Socialite package shipped with Laravel (which is more used for logins and that's about it).
You can however find many Google Analytic packages (along with many other Laravel bundles) here which should help you make API calls:
http://packalyst.com/s/google%20analytics
More specifically, this package: https://github.com/spatie/laravel-analytics
That, or run your own Guzzle and cURL scripts. I use Guzzle when I need something quick without building a full blown API.
However, there's an interesting post here about using Socialite to access GA data. But you're quite limited. If you're creating user driven dashboards, I'd opt for a separate package.
https://laracasts.com/discuss/channels/tips/how-i-made-google-analytics-work-with-socialite
I'am also trying to do the same thing. By far, I've user authentication at place with oAuth 2.0 and Socialite package. I need the list of sites to be fetched from GA. And am totally stuck there. It would really be great if you can guide me as on how should I move on further..
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.