Search code examples
phpgoogle-oauthgmail-apiservice-accountsgoogle-workspace

client is unauthorized to retrieve access tokens using this method service account error


It's been such a major headache getting my service account to authenticate on the same webapp where I have users logging in via oauth2 as well.

So I'm wondering, is this even possible?

If not, should one just stick with the service account? Does one have to then authenticate the users on one's own - old school style? Haha

Thanks.

Regarding the service account, I have enabled the domain wide delegation, enabled the client key + api scope in my G suite admin console, and have gotten the php sample with the books api working. However any time I try any other api, other than books, I get the error,

client is unauthorized to retrieve access tokens using this method

UPDATE: I've tried to use @dalmto's example, and have added a few lines to test the gmail api, for example:

putenv('GOOGLE_APPLICATION_CREDENTIALS=credentials.json');

$user = '[email protected]';

function getGoogleClient() {
    return getServiceAccountClient();
}

function getServiceAccountClient() {
    try {
        // Create and configure a new client object.
        $client2 = new Google_Client();
        $client2->useApplicationDefaultCredentials();
        $client2->setScopes(array('https://www.googleapis.com/auth/userinfo.email','https://www.googleapis.com/auth/admin.directory.user.readonly','https://www.googleapis.com/auth/userinfo.profile','https://www.googleapis.com/auth/gmail.readonly','https://www.googleapis.com/auth/calendar'));
        $client2->setAccessType('offline');
        $client2->setSubject($user);
        return $client2;
    } catch (Exception $e) {
        print "An error occurred: " . $e->getMessage();
    }
}

$newGoogleClient = getGoogleClient();

$service3 = new Google_Service_Gmail($newGoogleClient);
$results3 = $service3->users_labels->listUsersLabels($user);

But am now just receiving "400: Bad Request" errors

EDIT: After some more digging there is a note: 'failedPrecondition' - any idea which precondition that could be? I've allowed the following scopes for the client in my admin console:

hxxps://www.googleapis.com/auth/gmail.metadata, hxxps://www.googleapis.com/auth/userinfo.email, hxxps://www.googleapis.com/auth/userinfo.profile, hxxps://www.googleapis.com/auth/gmail.modify, hxxps://www.googleapis.com/auth/gmail.readonly,
hxxps://www.googleapis.com/auth/gmail.labels,
hxxps://mail.google.com/

And enabled the apis and enabled the scope in the 'OAuth Consent Screen'

DWD is also enabled: Service Account Overview Screenshot

EDIT2: Okay so I found the missing precondition was the "setSubject".

Once I added that it went a step further, but still failed again at '"error": "unauthorized_client",\n "error_description": "Client is unauthorized to retrieve access tokens using this method.'

FYI: When creating the service account, I gave it the "project -> owner" role. Is that sufficient? Does one have to add more?

EDIT3: I've also just checked logger and it says that DWD is enabled.. Im at my whits end here haha

   client: {
    adminState: {
     updateTime:  "2018-11-23T00:29:44.810Z"      
    }
    assertionMatchExistingGrant:  "MATCH_GRANT_DISABLED"     
    authType:  "PUBLIC_KEY"     
    brandId:  "aaaaaaaaaaaaaa"     
    clientId:  "aaaaaaaaaaaaaaaaaa"     
    consistencyToken:  "2018-11-23T00:29:44.953175Z"     
    creationTime:  "2018-11-23T00:29:44.810Z"     
    displayName:  "Client for servicemaint1"     
    domainWideDelegation:  "DELEGATION_ENABLED"     
    projectNumber:  "aaaaaaaaaaaaaaaa"     
    threeLeggedOauth:  "DISABLED"     
    updateTime:  "2018-11-23T00:29:44.953175Z"     
   }

EDIT4: FINALLY WORKING!

So I had been trying this in a new project I created for testing all morning / last night. But my oauth2 user authenticating was running through a different project (where I also couldn't get the service account working all of yesterday morning / afternoon).

So anyway, I noticed in: https://myaccount.google.com/permissions "Apps with Access to your account" - only my old project / app was authorized. So I switched back to my first project, created a new service account client ID .json file and it finallyyy worked to authenticate both! :)

I must have that authorized that somewhere extra along the line which I had not done with the second project.

Thanks again.

EDIT5: One more quick question - is this the correct way to do this on stackoverflow? With constantly going back to edit?

Also for others stumbling upon this later, here's my total authentication block (sorry its a bit long):

putenv('GOOGLE_APPLICATION_CREDENTIALS=maintenanceapp.json');

$user = '[email protected]';

function getGoogleClient() {
    return getServiceAccountClient();
}

function getServiceAccountClient() {
  $user = '[email protected]';
    try {
        // Create and configure a new client object.
        $client2 = new Google_Client();
        $client2->useApplicationDefaultCredentials();
        $client2->setScopes(['https://www.googleapis.com/auth/gmail.metadata','https://www.googleapis.com/auth/userinfo.email','https://www.googleapis.com/auth/userinfo.profile','https://www.googleapis.com/auth/gmail.modify','https://www.googleapis.com/auth/gmail.readonly','https://www.googleapis.com/auth/gmail.labels']);
        //$client2->setAccessType('offline');
        $client2->setSubject($user);
        return $client2;
    } catch (Exception $e) {
        echo "An error occurred: " . $e->getMessage();
    }
}

$newGoogleClient = getGoogleClient();

$service3 = new Google_Service_Gmail($newGoogleClient);
$results3 = $service3->users_labels->listUsersLabels($user);


 /*************************************************
  * Ensure you've downloaded your oauth credentials
  ************************************************/
 if (!$oauth_credentials = getOAuthCredentialsFile()) {
   echo missingOAuth2CredentialsWarning();
   return;
 }
 /************************************************
  * NOTICE:
  * The redirect URI is to the current page, e.g:
  * http://localhost:8080/idtoken.php
  ************************************************/
 $redirect_uri = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'];
 $client = new Google_Client();
 // USER AUTH
 $client->setAuthConfig($oauth_credentials);
 $client->setRedirectUri($redirect_uri);
 $client->setScopes(array('https://www.googleapis.com/auth/userinfo.email','https://www.googleapis.com/auth/userinfo.profile','https://www.googleapis.com/auth/gmail.readonly','https://www.googleapis.com/auth/calendar'));
 $client->setApprovalPrompt('auto');
 $client->setAccessType('offline');
 $plus = new Google_Service_Plus($client);

 /************************************************
  * If we're logging out we just need to clear our
  * local access token in this case
  ************************************************/
 if (isset($_REQUEST['logout'])) {
   unset($_SESSION['id_token_token']);
 }

 /************************************************
  * If we have a code back from the OAuth 2.0 flow,
  * we need to exchange that with the
  * Google_Client::fetchAccessTokenWithAuthCode()
  * function. We store the resultant access token
  * bundle in the session, and redirect to ourself.
  ************************************************/
 if (isset($_GET['code'])) {
   $token = $client->fetchAccessTokenWithAuthCode($_GET['code']);
   // store in the session also
   $_SESSION['id_token_token'] = $token;
   // redirect back to the example
   header('Location: https://abc.de/index.php');
   // return;
 }
 /************************************************
   If we have an access token, we can make
   requests, else we generate an authentication URL.
  ************************************************/
 if (
   !empty($_SESSION['id_token_token'])
   && isset($_SESSION['id_token_token']['id_token'])
 ) {
   $client->setAccessToken($_SESSION['id_token_token']);
 } else {
   $authUrl = $client->createAuthUrl();
   //header('Location: ' . $authUrl);
 }
 /************************************************
   If we're signed in we can go ahead and retrieve
   the ID token, which is part of the bundle of
   data that is exchange in the authenticate step
   - we only need to do a network call if we have
   to retrieve the Google certificate to verify it,
   and that can be cached.
  ************************************************/
 if ($client->getAccessToken()) {
   $token_data = $client->verifyIdToken();
 }

Solution

  • In google developer console when you create your project and the credentials you must choose which type of client you are going to create for which type of application.

    There are several different ways to authenticate to google.

    • OAuth2 native
    • OAuth2 web
    • Mobile
    • Service account

    The code to use these clients is also different. You cant create a web OAuth2 client and use it for the code meant to be calling a service account.

    "client is unauthorized to retrieve access tokens using this method".

    Means exactly that. The client you have set up on Google developer console is either not a service account client or the code you are using is not meant for a service account client.

    This is my serviceaccount.php sample. If your code needs to look something like this and you need to make sure that the client you created on the google developer console is a service account client.

    require_once __DIR__ . '/vendor/autoload.php';
    // Use the developers console and download your service account
    // credentials in JSON format. Place the file in this directory or
    // change the key file location if necessary.
    putenv('GOOGLE_APPLICATION_CREDENTIALS='.__DIR__.'/service-account.json');
    /**
     * Gets the Google client refreshing auth if needed.
     * Documentation: https://developers.google.com/identity/protocols/OAuth2ServiceAccount
     * Initializes a client object.
     * @return A google client object.
     */
    function getGoogleClient() {
        return getServiceAccountClient();
    }
    /**
     * Builds the Google client object.
     * Documentation: https://developers.google.com/api-client-library/php/auth/service-accounts
     * Scopes will need to be changed depending upon the API's being accessed. 
     * 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 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();
        }
    }
    

    Developer console

    Under clients check that the client you are using is one that can be found under service account keys. If not then it is the wrong client type and will not work with your code. Create a new service account client and set up domain wide delegation with that client id.

    enter image description here