phpgoogle-apigoogle-calendar-apigoogle-oauthgoogle-api-php-client

Google Calendar PHP API


I am currently using the Google Calendar API for a web application. However, every hour, I am prompted with a link to verify quick-start access.

Details:

I have tried using OAUTH and service accounts with no luck.

Below is the code that creates Google_Client and Service 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.

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."
}'

Solution

  • 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.,