4

I am currently using the Google Calendar API for a web application. However, every hour, I am prompted with a link to verify quickstart access. Does anyone know how to fix this?

Details:

  • I have created a new gmail id: redu@gmail.com
  • redu@gmail.com has an associated calendar
  • My php based web application needs to do the following with calendar:
  • Create a new calendar for every registered user (as an additional calendar for redu@gmail.com)
  • Create an event for a logged in user and add another registered user as an invitee

I have tried using OAUTH and service accounts with no luck. Any help is greatly appreciated.

Below is the code that creates Google_Client and Srvice objects using service account's credentials

function __construct()
    {
        Service account based client creation. 
        $this->client = new Google_Client();
        $this->client->setApplicationName("Redu");
        $this->client->setAuthConfig(CREDENTIALS_PATH);
        $this->client->setScopes([SCOPES]);
        $this->client->setSubject('redu@gmail.com');
        $this->client->setAccessType('offline');

        $this->service = new Google_Service_Calendar($this->client);
     }

When I try to use the $service object to create a calendar or create an event I get an error saying that domain wide permissions are not setup. However, when I created the service account I did enable domain wide delegation.

EDIT:

Below is my code to create a Google_Client using service account key and use the client to create a new calendar for redu@gmail.com. Note that I shared redu@gmail.com's calendar with reduservice@subtle-breaker-280602.iam.gserviceaccount.com and set the permission to "Manage Changes and Manage Sharing". The error I am getting is below the code:

require (__DIR__.'/../../../vendor/autoload.php');
define('CREDENTIALS_PATH', __DIR__ . '/redu_service_account_credentials.json');
define('SCOPES', Google_Service_Calendar::CALENDAR);

function createNewCalendar($userName) {
    //Service account based client creation. 
    $client = new Google_Client();
    $client->setApplicationName("REdu");
     // path to the credentials file obtained upon creating key for service account
    $client->setAuthConfig(CREDENTIALS_PATH);
    $client->setScopes([SCOPES]);
    $client->setSubject('redu@gmail.com');
    $client->setAccessType('offline');

    $service = new Google_Service_Calendar($client);

    $calendar = new Google_Service_Calendar_Calendar();
    $calendar->setSummary($userName);
    $calendar->setTimeZone('America/Los_Angeles');

    $createdCalendar = $service->calendars->insert($calendar);

    // Make the newly created calendar public
    $rule = new Google_Service_Calendar_AclRule();
    $scope = new Google_Service_Calendar_AclRuleScope();

    $scope->setType("default");
    $scope->setValue("");
    $rule->setScope($scope);
    $rule->setRole("reader");

    // Make the calendar public
    $createdRule = $service->acl->insert($createdCalendar->getId(), $rule);
    return $createdCalendar->getId();
}

ERROR:

Fatal error: Uncaught exception 'Google_Service_Exception' with message '{
  "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."
}'
itsMeMoriarty
  • 125
  • 1
  • 1
  • 9
  • Do you have a GSuite account with your won domain or a customer account (gmail)? – ziganotschka Jun 17 '20 at 09:37
  • You may be using service account credentials with that code but thats code for Oauth2 authorization it wont work with a service account. Im actually surprised it doesn't just go boom. – Linda Lawton - DaImTo Jun 17 '20 at 11:44
  • @ziganotschka we don't have a gsuite account. redu@gmail.com is a regular (I guess consumer?) account. – itsMeMoriarty Jun 17 '20 at 16:18
  • @DaImTo Thanks for the detailed response below. I will try your suggestions and get back to you with results. Really appreciate it! For my use case should I be using service account or OAuth? – itsMeMoriarty Jun 17 '20 at 16:19
  • Updated my anwser with a response to your error – Linda Lawton - DaImTo Jun 17 '20 at 17:53
  • I you do not a GSuite account, you cannot use a service account with domain-wide delegation, see [prerequisites](https://developers.google.com/google-ads/api/docs/oauth/service-accounts#prerequisites). While you technically can the account up, it won't work as intended because con configure it, you need to be the admin of an account with a domain and have access to https://admin.google.com/. Only GSuite users have access to this feature. See also [here](https://developers.google.com/admin-sdk/directory/v1/guides/delegation) and [here](https://stackoverflow.com/a/30806648/11599789). – ziganotschka Jun 18 '20 at 07:23

2 Answers2

6

OAUTH2 vs Service accounts

Oauth2 and service accounts are two different things. You use oauth2 if you are trying to access a users data. The consent window you mentioned will prop up and ask that they grant permission for your application to access their data.

Service accounts on the other hand are dummy users who can be pre approved to access data you the developer control. You could share a calendar with a service account granting it access to that calendar it will no need to be authenticated in the same manner as a user.

A service account will never popup and request access again.

Oauth2 example with refresh token.

The issue is that your access token is expiring. If it expires then the user will need to grant your application access to their data again. To avoid this we use a refresh token and store that in a session varable and when the acces stoken expires we just request a new one.

Notice how i am requesting $client->setAccessType("offline"); this will give me a refresh token.

the session vars are now set storing this data

    $_SESSION['access_token'] = $client->getAccessToken();
    $_SESSION['refresh_token'] = $client->getRefreshToken();  

Then latter i can check if the access token is expired if so i refresh it

 if ($client->isAccessTokenExpired()) {             
            $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
            $client->setAccessToken($client->getAccessToken());   
            $_SESSION['access_token'] = $client->getAccessToken();                
        }       

oauth2callback.php

    require_once __DIR__ . '/vendor/autoload.php';
    require_once __DIR__ . '/Oauth2Authentication.php';
    
    // Start a session to persist credentials.
    session_start();
    
    // Handle authorization flow from the server.
    if (! isset($_GET['code'])) {
        $client = buildClient();
        $auth_url = $client->createAuthUrl();
        header('Location: ' . filter_var($auth_url, FILTER_SANITIZE_URL));
    } else {
        $client = buildClient();
        $client->authenticate($_GET['code']); // Exchange the authencation code for a refresh token and access token.
        // Add access token and refresh token to seession.
        $_SESSION['access_token'] = $client->getAccessToken();
        $_SESSION['refresh_token'] = $client->getRefreshToken();    
        //Redirect back to main script
        $redirect_uri = str_replace("oauth2callback.php",$_SESSION['mainScript'],$client->getRedirectUri());    
        header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL));
    }

Authentication.php

require_once __DIR__ . '/vendor/autoload.php';
/**
 * Gets the Google client refreshing auth if needed.
 * Documentation: https://developers.google.com/identity/protocols/OAuth2
 * Initializes a client object.
 * @return A google client object.
 */
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;
}

/**
 * Builds the redirect uri.
 * Documentation: https://developers.google.com/api-client-library/python/auth/installed-app#choosingredirecturi
 * Hostname and current server path are needed to redirect to oauth2callback.php
 * @return A redirect uri.
 */
function getRedirectUri(){

    //Building Redirect URI
    $url = $_SERVER['REQUEST_URI'];                    //returns the current URL
    if(strrpos($url, '?') > 0)
        $url = substr($url, 0, strrpos($url, '?') );  // Removing any parameters.
    $folder = substr($url, 0, strrpos($url, '/') );   // Removeing current file.
    return (isset($_SERVER['HTTPS']) ? "https" : "http") . '://' . $_SERVER['HTTP_HOST'] . $folder. '/oauth2callback.php';
}


/**
 * Authenticating to Google using Oauth2
 * Documentation:  https://developers.google.com/identity/protocols/OAuth2
 * Returns a Google client with refresh token and access tokens set. 
 *  If not authencated then we will redirect to request authencation.
 * @return A google client object.
 */
function getOauth2Client() {
    try {
        
        $client = buildClient();
        
        // Set the refresh token on the client. 
        if (isset($_SESSION['refresh_token']) && $_SESSION['refresh_token']) {
            $client->refreshToken($_SESSION['refresh_token']);
        }
        
        // If the user has already authorized this app then get an access token
        // else redirect to ask the user to authorize access to Google Analytics.
        if (isset($_SESSION['access_token']) && $_SESSION['access_token']) {
            
            // Set the access token on the client.
            $client->setAccessToken($_SESSION['access_token']);                 
            
            // Refresh the access token if it's expired.
            if ($client->isAccessTokenExpired()) {              
                $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
                $client->setAccessToken($client->getAccessToken()); 
                $_SESSION['access_token'] = $client->getAccessToken();              
            }           
            return $client; 
        } else {
            // We do not have access request access.
            header('Location: ' . filter_var( $client->getRedirectUri(), FILTER_SANITIZE_URL));
        }
    } catch (Exception $e) {
        print "An error occurred: " . $e->getMessage();
    }
}
?>

code for service account

The credential files are different dont mix them up.

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();
}

}

Error

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

There are two types of clients Oauth2 clients and Service account clients. The .json file you download is diffrent for each client. As is the code you will use for each client. You cant interchange this code.

The error you are getting stats that the client you are using cant be used for the code you are using. Try to download the client secret .json for the service account again.,

Community
  • 1
  • 1
Linda Lawton - DaImTo
  • 106,405
  • 32
  • 180
  • 449
  • 1
    Ok Thanks! I am able to use the service account to list events on the calendar that is shared with the service account's email address. – itsMeMoriarty Jun 18 '20 at 18:44
  • 2
    It should be noted that your environment variable GOOGLE_APPLICATION_CREDENTIALS should be set to the filepath of your .JSON file you downloaded from the Service Account. putenv('GOOGLE_APPLICATION_CREDENTIALS=credentials-169991-ac1f887b4f8f.json'); – tlorens Sep 16 '20 at 15:37
  • very helpful thank you, where do you specify the json file for the service account? useApplicationDefaultCredentials() needs to know the path to that file somehow right? thank you – Robert Sinclair Dec 10 '20 at 17:32
1

Here's a working example that generates the authentication object using the Service Account's JSON file

$client = new Google\Client();
$client->setApplicationName(APP_NAME);
$client->setAuthConfig(PATH_TO_JSON_FILE);
$client->setScopes(['YOUR_SCOPE1','YOUR_SCOPE2']);
$client->setSubject(EMAIL_OF_PERSON_YOURE_IMPERSONATING);
$client->setAccessType('offline');

$service = new Google_Service_Drive($client);
// Do stuff with the $service object
  1. Generate Service Account in Google API Console
  2. Delegate domain wide authority to that Service Account's Client ID in Google workspace and define the scopes that the Service Account will have access to
  3. Use the code above and make sure to include one more more relevant scopes
Robert Sinclair
  • 4,550
  • 2
  • 44
  • 46