googleapis / google-api-php-client

A PHP client library for accessing Google APIs
Apache License 2.0
9.19k stars 3.52k forks source link

Client is unauthorized to retrieve access tokens using this method" w/ Service Account #2214

Open amanets opened 2 years ago

amanets commented 2 years ago

PROBLEM / CAUSE: I am using google-php-api-client to create calendar event, with service account and set the subject, subject email address is my admin account, with out subject this is working but when i am set subject this is not working and given error Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested, When i am calling throw google API this is working properly API Calling code is :

define('CALENDAR_ID', <claendar_id>); 

/*********************** Start: Create access Token***********************************/
function base64url_encode($data) {
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function getJwtAssertion($private_key_file)
{
    $json_file = file_get_contents($private_key_file);
    $info = json_decode($json_file);
    $private_key = $info->{'private_key'};

    //{Base64url encoded JSON header}
    $jwtHeader = base64url_encode(json_encode(array(
        "alg" => "RS256",
        "typ" => "JWT"
    )));
    //{Base64url encoded JSON claim set}
    $now = time();
    $jwtClaim = base64url_encode(json_encode(array(
        "iss" => <serviceaccount>,
        "sub" => <admin_email_address>,
        "scope" => "https://www.googleapis.com/auth/calendar",
        "aud" => "https://www.googleapis.com/oauth2/v4/token",
        "exp" => $now + 3600,
        "iat" => $now
    )));

    $data = $jwtHeader.".".$jwtClaim;

    // Signature
    $Sig = '';
    openssl_sign($data,$Sig,$private_key,'SHA256');
    $jwtSign = base64url_encode($Sig);
    $jwtAssertion = $data.".".$jwtSign;
    return $jwtAssertion;
}

function getGoogleAccessToken($private_key_file)
{
    $result = [
      'success' => false,
      'message' => '',
      'token' => null
    ];   
    if(!file_exists($private_key_file)){
        $result['message'] = 'Google json key file missing!';
        return $result;
    }
    $jwtAssertion = getJwtAssertion($private_key_file);
    try { 
        $payload = [
            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
            'assertion' => $jwtAssertion
        ];
        $url = 'https://oauth2.googleapis.com/token';
        $ch = curl_init();      
          curl_setopt($ch, CURLOPT_URL, $url);        
          curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);        
          curl_setopt($ch, CURLOPT_POST, 1);      
          curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
          curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);    
          $data = json_decode(curl_exec($ch), true);
        $http_code = curl_getinfo($ch,CURLINFO_HTTP_CODE);
        if($http_code != 200){
          $result['message'] = 'Error : Failed to access token';

        } else {
            $result['token'] = $data['access_token'];
            $result['success'] = true;
        }
    } catch (RequestException $e) {
          $result['message'] = $e->getMessage();
    }
    return $result;
}
$KEY_FILE_LOCATION = __DIR__.'/key_file.json';
$googleToken = getGoogleAccessToken($KEY_FILE_LOCATION);
/*********************** End Create access Token***********************************/
if(isset($googleToken['token'])){
    $access_token = $googleToken['token'];
    getCalendarDetails($access_token); 
}
function getCalendarDetails($access_token) {
    $url_events = 'https://www.googleapis.com/calendar/v3/calendars/claendar_id';
    $ch = curl_init();  
    curl_setopt($ch, CURLOPT_URL, $url_events);        
    // curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    // curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET"); 
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);   
    curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer '. $access_token, 'Content-Type: application/json')); 

    $data = curl_exec($ch);
    $http_code = curl_getinfo($ch,CURLINFO_HTTP_CODE);

    echo "<pre>";
    print_r($data);
    die();
}

But i am set the subject with Google php client is not working

My code is :

require_once 'vendor/autoload.php';
putenv('GOOGLE_APPLICATION_CREDENTIALS='.__DIR__.'/clanedar_key.json');

$calendarId = <calendar_id>;

$client = new Google_Client();

// use the application default credentials
$client->useApplicationDefaultCredentials();
$client->setSubject("admin_gmail_account");

$client->addScope([Google_Service_Calendar::CALENDAR_EVENTS, Google_Service_Calendar::CALENDAR]);

$service = new Google_Service_Calendar($client);

$optParams = array(
    'maxResults' => 10,
    'orderBy' => 'startTime',
    'singleEvents' => true,
    'timeMin' => date('c', strtotime('2022-01-28 9:00am -1 minute')),
    'timeMax' => date('c', strtotime('2022-02-28 10:00am +1 minute')),
);
$results = $service->events->listEvents($calendarId, $optParams);

foreach ($results->getItems() as $event) {
    $start = $event->start->dateTime;
    $start = !$start ? $event->start->date : $start;
    $end = $event->end->dateTime;
    printf($event->getSummary() . "<br>" . $start . "<br>" .$end. "<br>". $event->hangoutLink ."<br><br>");
} 

Seen Error : Fatal error: Uncaught Google\Service\Exception: { "error": "unauthorized_client", "error_description": "Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested." } in /var/www/html/google-api-php-client-main/src/Http/REST.php:128 Stack trace: #0 /var/www/html/google-api-php-client-main/src/Http/REST.php(103): Google\Http\REST::decodeHttpResponse() googleapis/google-api-dotnet-client#1 [internal function]: Google\Http\REST::doExecute() googleapis/google-api-dotnet-client#2 /var/www/html/google-api-php-client-main/src/Task/Runner.php(182): call_user_func_array() googleapis/google-api-dotnet-client#3 /var/www/html/google-api-php-client-main/src/Http/REST.php(66): Google\Task\Runner->run() googleapis/google-api-dotnet-client#4 /var/www/html/google-api-php-client-main/src/Client.php(918): Google\Http\REST::execute() googleapis/google-api-dotnet-client#5 /var/www/html/google-api-php-client-main/src/Service/Resource.php(238): Google\Client->execute() googleapis/google-api-dotnet-client#6 /var/www/html/google-api-php-client-main/vendor/google/apiclient-services/src/Calendar/Resource/Events.php(271): Google\Service\Resource->call() googleapis/google-api-dotnet-client#7 /var/www/html/google-api-php-c in /var/www/html/google-api-php-client-main/src/Http/REST.php on line 128

amanda-tarafa commented 2 years ago

I am moving this issue to the PHP repo as this is the .NET repo.

LindaLawton commented 2 years ago

Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested,

There are serval types of clients that you can create on google cloud console. Service account , web, native and some mobile ones. The code used to authorize them is different. You cant use web credentials with code designed for use with a service account. I would check that you actually have service account credentials.

function initializeCalendar()
{

  // Use the developers console and download your service account
  // credentials in JSON format. Place them in this directory or
  // change the key file location if necessary.
  $KEY_FILE_LOCATION = __DIR__ . '/service-account-credentials.json';

  // Create and configure a new client object.
  $client = new Google_Client();
  $client->setApplicationName("Hello Calendar");
  $client->setAuthConfig($KEY_FILE_LOCATION);
  $client->setScopes(['https://www.googleapis.com/auth/Calendar]);
  $client->setSubject("account1@example.com");  // workspace user to deligate
  $service = new Google_Service_Calendar($client);

  return $servie;
}
paseto commented 1 year ago

Any updates on this? Really makes no sense, I use cloud console(with normal Gmail user) and create a service account. Then I cannot use this service account without have a Workspace Admin User Account to use the API.

LindaLawton commented 1 year ago

@paseto service accounts don't work with standard Gmail user accounts for either the google calendar api or the gmail api. You need to have a google workspace domain account and configure domain wide delegation to a user on the domain.

There's no update for that its working as intended.

paseto commented 1 year ago

Thanks @LindaLawton, I'll try oauth2 with refresh token since I only need to access a calendar from a single gmail user account.

LindaLawton commented 1 year ago

Just remember to set your app to production when your ready. Apps in testing have their refresh tokens expired after seven days.

UVLabs commented 5 months ago

@paseto service accounts don't work with standard Gmail user accounts for either the google calendar api or the gmail api. You need to have a google workspace domain account and configure domain wide delegation to a user on the domain.

There's no update for that its working as intended.

This one comment saved me so much time after hours trying to debug why the heck I couldn't get this to work.

I watched a tutorial and in it a service account was created and it worked with the service account. I even downloaded the zip that the video creator uploaded and tested it and it worked. But when I tried my code the same way...it didn't.

I assume this is due to some changes in the Google API client library where the latest ones do not allow service accounts to access and edit calendars without "domain wide delegation" which you need a google workspace account to get!

I resorted to using OAuth and it worked just fine.

// Include the Composer autoload file
require_once 'vendor/autoload.php';

// Your Google Cloud Platform project's client ID and client secret
$clientID = '454....apps.googleusercontent.com';
$clientSecret = 'GOCS...';

// Create a new Google client
$client = new Client();
$client->setApplicationName('Your Application Name');
$client->setClientId($clientID);
$client->setClientSecret($clientSecret);
$client->setAccessType('offline');  // Request offline access to get a refresh token
$client->setRedirectUri('https://example.com/callback'); // Where to redirect to after permission is granted
$client->setPrompt('consent');
$client->setScopes([
    Calendar::CALENDAR, // Scope for accessing the calendar
    Calendar::CALENDAR_EVENTS, // Scope for accessing the calendar events
]);

$authUrl = $client->createAuthUrl();
echo $authUrl; // Echo just for testing...ideally you would handle displaying the authurl differently.

// You should refactor this to get code from URL once, get access token and then save.
$code = $_GET['code'] ?? '';
if( $code ){
    $client->fetchAccessTokenWithAuthCode($code);
    $accessToken = $client->getAccessToken(); // Now you have your access token json...store it securely.
}

$client->setAccessToken($accessToken); 

// If the access token has expired, refresh it
if ($client->isAccessTokenExpired()) { // You can use a cron job instead to check this and refresh access token and update the stored value before hand.
    $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken()); // Refresh and set new access token.
}

// Create a service object for making requests to the Calendar API
$service = new Calendar($client);

// Define the event details
$event = new Event([
    'summary' => 'Sample Event',
    'start' => ['dateTime' => '2024-01-21T10:00:00', 'timeZone' => 'America/Los_Angeles'],
    'end' => ['dateTime' => '2024-01-21T12:00:00', 'timeZone' => 'America/Los_Angeles']
]);

// Insert the event
$calendarId = 'primary';  // Use 'primary' for the user's primary calendar
$event = $service->events->insert($calendarId, $event);

@paseto Are you aware of the rate limits for test accounts added to an OAuth app? The ones added here:

image

I tried finding a definite answer but couldn't find any. I would be the only one using the app to create calendar events in my own calendar, so don't need any crazy limit but would still like to know...probably about 100 scheduled calendar events a day

codev-agency commented 3 months ago

@amanets did you could fixed it? I have same error

mohsin889 commented 2 months ago

@paseto service accounts don't work with standard Gmail user accounts for either the google calendar api or the gmail api. You need to have a google workspace domain account and configure domain wide delegation to a user on the domain.

There's no update for that its working as intended.

I have a service account with domain wide deligation enabled and when i impersonate the user and add an attendee to the event it gives me the error

{ "error": "unauthorized_client", "error_description": "Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested."}

Code that I'm using to create an event and add attendee.


<?php

namespace App\Http\Services;

use Carbon\Carbon;

class GoogleService
{
    private $client, $calendar_service;
    public function __construct()
    {
        $this->client = new \Google_Client();
        $this->client->setApplicationName("laravel-5-6-google-calendar");
        $this->client->setScopes([\Google_Service_Calendar::CALENDAR_EVENTS, \Google_Service_Calendar::CALENDAR_READONLY]);
        $this->client->setAuthConfig(storage_path().'/google_service_account.json');
        $this->client->setSubject("john@companyname.net");
        $this->calendar_service = new \Google_Service_Calendar($this->client);
    }

    public function getGoogleCalendarEvent($appraisal_id, $address, $date, array $emails = array())
    {
        $_cfg = [
            "summary" => "Inspection appointment for ({$appraisal_id}/{$address})",
            "location" => $address,
            "start" => [
                "dateTime" => Carbon::parse($date,'utc')->setTimezone("America/Denver")->toIso8601String(),
                "timeZone" => "America/Denver"
            ],
            "end" => [
                "dateTime" => Carbon::parse($date,'utc')->setTimezone("America/Denver")->addDay()->toIso8601String(),
                "timeZone" => "America/Denver"
            ],
            'visibility' => 'public'
        ];

        if(!empty($emails)) {
            $_cfg["attendees"] = $this->preProcessAttendee($emails);
        }

        $event = new \Google_Service_Calendar_Event($_cfg);
        $calendar_id = "test _calendar_id";
        $event = $this->calendar_service->events->insert($calendar_id, $event);
        return $event->htmlLink;
    }

    private function getCalenders()
    {
        $calendarList = $this->calendar_service->calendarList->listCalendarList();
        $calendars = [];
        foreach ($calendarList->getItems() as $calendar) {
            $calendars[] = [
                'id' => $calendar->getId(),
                'summary' => $calendar->getSummary(),
            ];
        }
        return $calendars;
    }

    private function preProcessAttendee(array $emails)
    {
        return array_map(function ($e) {
            return [
                "email" => $e['email'],
                "displayName" => $e['name'],
                "responseStatus" => "needsAction"
            ];
        },$emails);
    }
}