Search code examples
google-apps-scriptgoogle-cloud-platformoauth-2.0service-accounts

Using a Service Account for Admin API with OAuth2 library in Google Apps Script to Add A User to a Google Group



EDIT:

The URL needs to be https://admin.googleapis.com/admin/directory/v1/groups/{groupKey}/members and not https://admin.googleapis.com/admin/directory/v1/groups/{groupKey}/members/insert (without the insert, even though it's the insert method)


I am trying to build a script with Google Apps Script that would be able to add users to a Google Group with a service account. Now, using the AdminDirectory service is straightforward, so this code works:

const addUser = email => {
  const member = AdminDirectory.Members.insert(
    { email, role: 'MEMBER' },
    '[email protected]'
  );
};

However this service requires super-user privileges. Hence my decision was to use a service account.

I created the service account and went through the whole checklist:

  1. I generated the JSON key
  2. Enabled domain-wide delegation
  3. In the Admin Console > Security > API Controls > Domain-wide Delegation I added the following scopes:
  4. Then I built this solution that uses the OAuth2 library:
const addUserWithServiceAccount = email => {
  const serviceAccount = {
    type: 'service_account',
    project_id: 'my-tools-324016',
    private_key_id: '021623b...',
    private_key: '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----\n',
    client_email: 'SERVICE-ACCOUNT-EMAIL',
    client_id: '1059879019...',
    auth_uri: 'https://accounts.google.com/o/oauth2/auth',
    token_uri: 'https://oauth2.googleapis.com/token',
    auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
    client_x509_cert_url:
      'https://www.googleapis.com/robot/v1/metadata/x509/...',
  };

  const _getAdminService = serviceAccount => {
    return OAuth2.createService('admin-sdk-test')
      .setTokenUrl('https://accounts.google.com/o/oauth2/token')
      .setPrivateKey(serviceAccount.private_key)
      .setIssuer(serviceAccount.client_email)
      .setPropertyStore(PropertiesService.getScriptProperties())
      .setCache(CacheService.getUserCache())
      .setLock(LockService.getUserLock())
      .setScope([
        'https://www.googleapis.com/auth/admin.directory.group',
        'https://www.googleapis.com/auth/admin.directory.group.member',
        'https://www.googleapis.com/auth/admin.directory.user',
      ]);
  };

  const _executeWithAuth2 = (groupKey, email) => {
    console.log('_executeWithAuth2()');
    const adminService = _getAdminService(serviceAccount);
    if (!adminService.hasAccess()) {
      Logger.log('ERROR n' + adminService.getLastError());
      return;
    }

    const url = `https://admin.googleapis.com/admin/directory/v1/groups/${groupKey}/members/insert?memberKey=${email}`;

    const headers = {
      Authorization: `Bearer ${adminService.getAccessToken()}`,
      // 'Content-Type': 'application/json',
    };
    const options = {
      method: 'put',
      headers,
      contentType: 'application/json',
      payload: JSON.stringify({
        email,
        role: 'MEMBER',
        kind: 'admin#directory#member',
        type: 'USER',
      }),
      muteHttpExceptions: true,
    };

    console.log(`fetch(${url}, ${JSON.stringify(options, null, 2)})`);

    const result = UrlFetchApp.fetch(url, options).getContentText();
    console.log(result);
    return result;
  };

  return _executeWithAuth2('test-group@domain', email);
};

Now when I run it, I get the following error in response:

{
  "error": {
    "code": 403,
    "message": "Not Authorized to access this resource/api",
    "errors": [
      {
        "message": "Not Authorized to access this resource/api",
        "domain": "global",
        "reason": "forbidden"
      }
    ]
  }
}

And when I switch PUT to POST I get:

The requested URL /admin/directory/v1/groups/[email protected]/members/[email protected] was not found on this server.

However this approach with the Service Account works very well with BigQuery. And even though I wasn't able to find an Admin Directory-specific solution, I was expecting it to work in the same way.

What am I missing here?


Solution

  • A service account does not have super-user privileges neither

    This is why you are getting an Not Authorized error.

    • The service account needs to impersonate the super-user with the respective privelleges.
    • For this, in addition to enabling domain-wide delegation, you need to specify the impersonated user programmatically by seetting him as subject with .setSubject("EMAIL OF SUPER USER").

    Sample:

    const _getAdminService = serviceAccount => {
        return OAuth2.createService('admin-sdk-test')
          .setSubject("EMAIL OF SUPER USER")
          .setTokenUrl('https://accounts.google.com/o/oauth2/token')
          .setPrivateKey(serviceAccount.private_key)
          .setIssuer(serviceAccount.client_email)
          .setPropertyStore(PropertiesService.getScriptProperties())
          .setCache(CacheService.getUserCache())
          .setLock(LockService.getUserLock())
          .setScope([
            'https://www.googleapis.com/auth/admin.directory.group',
            'https://www.googleapis.com/auth/admin.directory.group.member',
            'https://www.googleapis.com/auth/admin.directory.user',
          ]);
      };