User Impersonation with Google Service Account
Hi. I'm trying to implement user impersonation with a google service account and have been having problems for a while. After adding the user to be impersonated as the subject, a token gets created but when calling an API like Google Calendar I get a 401 Invalid Credentials error as if the token that was just created has expired or is invalid.
Do you have any samples of user impersonation? I don't see any in the samples or using Node in Google's documentation. Here is their documentation on the subject: https://developers.google.com/identity/protocols/oauth2/service-account#delegate-domain-wide-authority
Thanks a lot.
i am unable to impersonate successfully as well.
Error, code 401
unauthorized_client: Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested.
const googleService = {};
const authClient = google.auth.fromJSON(serviceJson);
authClient.scopes = [
'https://www.googleapis.com/auth/calendar',
'https://www.googleapis.com/auth/calendar.events',
];
authClient.subject = '[email protected]';
googleService.authClient = authClient;
async function addGuestAndSendEmail(eventId, calendarId, newGuest) {
const {
data: { attendees = [] },
} = await googleService.event.get(eventId, calendarId);
attendees.push({ email: newGuest });
return calendar.events.patch({
calendarId,
eventId,
auth: googleService.authClient,
requestBody: {
sendUpdates: 'all',
attendees,
},
});
}
service account where I got serviceJson
Enable G Suite Domain-wide Delegation - check
service account granted access on google admin Calendar (Read-Write) https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events
Badly need help :((
without authClient.subject, i get error code 403
Service accounts cannot invite attendees without Domain-Wide Delegation of Authority.
also tried
const authClient = new google.auth.JWT({
email: serviceJson.client_email,
key: serviceJson.private_key,
scopes: [
'https://www.googleapis.com/auth/calendar',
'https://www.googleapis.com/auth/calendar.events',
'https://www.googleapis.com/auth/cloud-platform',
],
subject: '[email protected]',
});
googleService.authClient = authClient;
with error
unauthorized_client: Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested
I was able to do impersonation with official python client library, so I suspect there's something wrong with this NodeJS client
There's an outstanding PR, #779, to add support for impersonation. Perhaps try this as a starting point, and see if the approach would work for you?
Hey @bcoe thanks for pointing out the PR, I wish to have seen this a few days ago 😄 We've finished the feature in the python client so I think I'll be delayed in testing the PR
i tried to impersonate a user under the domain but it didn't work. I got error
Error: Error: Unable to refresh sourceCredential: Error: Error: Unable to impersonate: Error: Requested entity was not found.
sample code:
const saclient = new JWT(
serviceJson.client_email,
null,
serviceJson.private_key,
scopes,
);
// Use that to impersonate the targetPrincipal
const targetClient = new Impersonated({
sourceClient: saclient,
targetPrincipal: '[email protected]',
lifetime: 30,
delegates: [],
targetScopes: scopes,
});
const authHeaders = await targetClient.getRequestHeaders();
console.log('authHeaders', authHeaders);
I guess this impersonation applies only to a service account, not to domain users
There's an outstanding PR, #779, to add support for impersonation. Perhaps try this as a starting point, and see if the approach would work for you?
https://stackoverflow.com/a/61571003/3539640
any service accounts made after March 2, 2020 will no longer be able to invite guests to events without using impersonation.
@bcoe Has impersonation been implemented in this package?
https://stackoverflow.com/questions/61473708/creating-events-using-the-google-calendar-api-and-service-account
Is this working fine
maybe this could help
const auth = new google.auth.GoogleAuth({
keyFile: './service.json',
scopes: [
'https://www.googleapis.com/auth/contacts',...
],
clientOptions: {
subject: '[email protected]'
},
});
@dr-aiuta you save my life!! After hours of searching... Not sure why Google don't put this on docs...
@fabiomig is @dr-aiuta's example working for you? It sounds like we definitely should document this approach.
@bcoe, @dr-aiuta's approach worked for me. Also working for me is the following (Calendar API specific example):
const { google } = require('googleapis');
const moment = require('moment');
const googleKey = require('../service-account.json');
const SUBJECT = '[email protected]'
const auth = new google.auth.JWT({
email: googleKey.client_email,
key: googleKey.private_key,
scopes: ['https://www.googleapis.com/auth/calendar'],
subject: SUBJECT
});
const calendar = google.calendar({ version: 'v3', auth });
calendar.events.insert({
calendarId: SUBJECT,
sendUpdates: 'all',
requestBody: {
summary: 'This is a summary',
description: 'This is a description',
start: { dateTime: moment().add(1, 'day'), timeZone: 'PST' },
end: { dateTime: moment().add(1, 'day').add(45, 'minutes'), timeZone: 'PST' },
attendees: [{ email: SUBJECT }, { email: '[email protected]' }]
}
}, (err, res) => {
if (err) return console.log('The API returned an error: ' + err);
// handle res
});
Bear in mind it impersonates a user, not a separate Service Account. I was only able to find this info on SO/here/a couple of blogs. It would be helpful if it were documented if it isn't already.
maybe this could help
const auth = new google.auth.GoogleAuth({ keyFile: './service.json', scopes: [ 'https://www.googleapis.com/auth/contacts',... ], clientOptions: { subject: '[email protected]' }, });
Thank you much.
can this "clientOptions" parameter be put on one of the Quickstarts pages as one of the examples? I spent hours trying to find a solution and I'm so relieved to find this thread. This should be standard in the Directory API docs.
Hi .. I am trying to use the service account using Google Cloud Functions to access the Workspace Directory API. I am trying to use Application Default Credentials approach. Since the documentation doesn't mention any additional steps to be done in the Google Cloud Console side, I assume that Application Default Credentials (ADC) to the function is automatically passed to the cloud function from Google Cloud's Metadata server. The following code works perfectly in my local, emulated environment where I have set GOOGLE_APPLICATION_CREDENTIALS environment variable to point to the JSON credential file. However when I deploy the function to Google Cloud, I am getting the error "Not Authorized to access this resource/api". I have been searching and trying for days without any success. As an aside, before I stumbled upon this thread and recommendation from @dr-aiuta, I was using getCredentials() method of GoogleAuth to get the private_key to create JWT auth (for the "subject" property) and passing that auth to admin API call. Which again worked perfectly in my local environment but fails in the cloud environment because getCredentials() private_key is null, which is probably expected behavior. Any help is deeply appreciated. If this request needs to be posted somewhere else please advise as well.
export const listUsers = functions.https.onCall((data, context) => {
return new Promise(async (resolve, reject) => {
const envAuth = new GoogleAuth({
scopes: ["https://www.googleapis.com/auth/admin.directory.user.readonly"],
clientOptions: {
subject: "[email protected]",
},
});
const client = await envAuth.getClient();
const service = google.admin({version: "directory_v1", auth: client});
try {
const response = await service.users.list({
customer: "MYCUSTOMER_ID",
});
resolve(response);
} catch (err) {
reject(err);
}
});
});
I am having the same issue as @increos and would like to replicate the solution provided for Python: https://github.com/GoogleCloudPlatform/professional-services/blob/master/examples/gce-to-adminsdk/main.py
@jmkrimm My conclusion through trial and error (correctly or incorrectly ) is the ADC strategy works for "newer?" Google Cloud Platform service e.g. Cloud Storage, Secrets Manager etc. But if you are reaching beyond to other (legacy?) Google Products like Workspace then you need other approaches. I got it to work by using using the Secret Manager product. Storing my keys there and reading those in and explicitly using the JWT token to get access to the Google Admin Directory API.
I am pasting excerpts of my code below (in typescript) if it helps in anyway :
import {SecretManagerServiceClient} from '@google-cloud/secret-manager';
import {JWT} from 'google-auth-library';
... ...
const client = new SecretManagerServiceClient();
const name = 'projects/<project id >/secrets/private_key/versions/1';
export function googleAuthorize(scopes: Array<string>, subject: string): Promise<JWT> {
return new Promise(async (resolve) => {
const [version] = await client.accessSecretVersion({
name: name,
});
const secret = JSON.parse(version.payload?.data?.toString() as string);
const jwt = new google.auth.JWT({
scopes: scopes,
subject: subject,
email: secret.client_email,
key: secret.private_key,
});
resolve(jwt);
});
}
and the finally
etc. etc.
const scopes = ['https://www.googleapis.com/auth/admin.directory.user.readonly'];
const jwtAuth = await googleAuthorize(scopes, ADMIN_EMAIL);
const service = google.admin({version: 'directory_v1', auth: jwtAuth});
try {
const response = await service.users.list({
customer: "MYCUSTOMER_ID",
});
resolve(response);
} catch (err) {
reject(err);
}
etc etc
@increos sorry but that solution will not work because I am trying not to create a KEY file at all. When code runs on GCP and the default service account has the necessary authorization, it should just work. Similar to authenticaing to GCP resources like Cloud Storage. The issue is not with the Google API but the nodejs library. Google Workspace API in most cases expects impersonation of a Google Workspace account and the client library does not support this without providing a key file. It is very frustrating for enterprise Google Workspace customers like myself.
maybe this could help
const auth = new google.auth.GoogleAuth({ keyFile: './service.json', scopes: [ 'https://www.googleapis.com/auth/contacts',... ], clientOptions: { subject: '[email protected]' }, });
Wow great, thank you so much 😄 , I've been looking for this all day long
for me the solution with JWT worked:
const auth = new google.auth.JWT({
keyFile: 'path-to-service-account.json',
scopes: [
'https://www.googleapis.com/auth/calendar',
'https://www.googleapis.com/auth/calendar.events',
],
subject: '[email protected]',
})
maybe this could help
const auth = new google.auth.GoogleAuth({ keyFile: './service.json', scopes: [ 'https://www.googleapis.com/auth/contacts',... ], clientOptions: { subject: '[email protected]' }, });
THANK YOU!
@dr-aiuta
maybe this could help
const auth = new google.auth.GoogleAuth({ keyFile: './service.json', scopes: [ 'https://www.googleapis.com/auth/contacts',... ], clientOptions: { subject: '[email protected]' }, });
Thanks! This solved everything! ❤️
maybe this could help
const auth = new google.auth.GoogleAuth({ keyFile: './service.json', scopes: [ 'https://www.googleapis.com/auth/contacts',... ], clientOptions: { subject: '[email protected]' }, });Wow great, thank you so much 😄 , I've been looking for this all day long
OMG THANK YOU SO MUCH
also tried
const authClient = new google.auth.JWT({ email: serviceJson.client_email, key: serviceJson.private_key, scopes: [ 'https://www.googleapis.com/auth/calendar', 'https://www.googleapis.com/auth/calendar.events', 'https://www.googleapis.com/auth/cloud-platform', ], subject: '[email protected]', }); googleService.authClient = authClient;with error
unauthorized_client: Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested
For those of you who still encounter this problem and hasn't found light, try redownloading json credentials. If you add scope or domain-wide delegation to the service account, it seems the private key will also change, and thus you need to download a new one
@increos sorry but that solution will not work because I am trying not to create a KEY file at all. When code runs on GCP and the default service account has the necessary authorization, it should just work. Similar to authenticaing to GCP resources like Cloud Storage. The issue is not with the Google API but the nodejs library. Google Workspace API in most cases expects impersonation of a Google Workspace account and the client library does not support this without providing a key file. It is very frustrating for enterprise Google Workspace customers like myself.
I'm trying too to use the Application Default Credentials to create a JWT token to impersonate the users. This will be very useful to deploy both in production and in test enviroment without a cumbersone json file to every time manage
Wow, so to this date, the Node.js library has no way of just using the ADC? Instead, we have to explicitly create a key for the Service Account (which is a bad practice as per Google's documentation)
and using that generated JSON to manually create a JWT with
subject: "impersonated_user@domain"????
@bcoe @danielbankhead can you please shed some light on this. I believe both Python and Java libraries have this functionality already as expressed in this tutorial of yours and it seems crazy that I have to increase security risks at my organisation due to a missing feature...
Edit: FYI manually creating a new Impersonated auth as per this guide doesn't work either as that's only meant for impersonating other SAs.
Tests
const auth = new GoogleAuth({
clientOptions: { // these seem to be ignored altogether
subject: hostEmail,
},
scopes: [ // added just in case, but I've tried without this and same thing
'https://www.googleapis.com/auth/calendar',
'https://www.googleapis.com/auth/calendar.events',
],
});
const targetClient = new Impersonated({
sourceClient: authClient,
targetPrincipal: targetUserToImpersonateWithServiceAccount,
lifetime: 30,
delegates: [],
targetScopes: [
'https://www.googleapis.com/auth/calendar',
'https://www.googleapis.com/auth/calendar.events',
],
});
const authHeaders = await targetClient.getRequestHeaders();
results in a GaxiosError: Could not refresh access token: PERMISSION_DENIED: unable to impersonate: Request had insufficient authentication scopes. even with DWD set up.
Doing a Frankenstein-worthy workaround:
const authClient: { sourceClient: AuthClient } = (await auth.getClient()) as unknown as {
sourceClient: AuthClient;
};
const targetClient = new Impersonated({
sourceClient: authClient.sourceClient,
// ...
I get a GaxiosError: Could not refresh access token: NOT_FOUND: unable to impersonate: Not found; Gaia id not found for email email@domain
There's no way to use an impersonated Service Account to impersonate a Workspace user via ADC for Node as far as I can tell, and the maintainers seem not to mind it. I really want to be wrong, but nothing points me to think otherwise.
I guess it's either the JWT way or logging in directly as the SA, without impersonation (is that even possible?) :/
After combining knowledge from this thread, and countless other sources online (Google and not), I was finally able to get NodeJS to do Service Account impersonation using my Google Account ADC, without having to manage service account keys.
- optional: destroy your
~/.config/gclouddirectory to ensure no other pre-existing login credentials conflict with the authentication:rm -rf ~/.config/gcloud - provide ADC to gcloud:
gcloud auth application-default loginand follow the login flow in the browser, logging in with your Google Account. - provide your ADC Google Account the Service Account Token Creator Role on the Service Account you are trying to impersonate.
- The IAM Service Account Credentials API must be enable in your GCP Project. I thought this was the same as the IAM API. It is not. Goto GCP -> APIs & Services -> Enable APIs & Services -> search for it, and Enable it.
- install the necessary dependencies, and run the code below (example uses BigQuery):
const { BigQuery } = require('@google-cloud/bigquery');
const { GoogleAuth, Impersonated } = require('google-auth-library');
const projectId = 'your-project';
const serviceAccountEmail = `your-sa@${projectId}.iam.gserviceaccount.com`;
const scopes = ['https://www.googleapis.com/auth/cloud-platform'];
const delegates = [];
const lifetime = 3600;
(async () => {
const auth = new GoogleAuth({ scopes });
const sourceClient = await auth.getClient();
const authClient = new Impersonated({
sourceClient,
targetPrincipal: serviceAccountEmail,
lifetime,
delegates,
targetScopes: scopes
});
const bigquery = new BigQuery({ projectId, authClient });
const response = await bigquery.query('select 1 as x ');
/* should print [ [ { x: 1 } ] ] */
console.log(response);
})()
While this works, it leaves much to be desired. I would prefer if all of this impersonation & authclient code could be stored locally and referenced as ADC by the Node SDK... so, I found: gcloud auth application-default login --impersonate-service-account SERVICE_ACCT_EMAIL. But, it fails for reasons I'm still not sure. Substituting (2) above with this command and cleaning up the BigQuery constructor to just simply const bigquery = new BigQuery({ projectId }); fails with:
Error: The incoming JSON object does not contain a client_email field
I guess it has something to do with the credential file that is stored in ~/.config/gcloud/application_default_credentials.json. The schemas are not aligned when running the command with or without the --impersonate-service-account option.
I will go ahead with what I have now, but would appreciate if anyone could shed light on it.
@jars you're doing the opposite of what we're trying to achieve, which we know works ;).
This issue is about using a Service Account to impersonate a user, not the other way around. For example, I want to be able to send a calendar invitation or an email on behalf of colleagues in my Domain (with their prior consent, obviously) via Domain-Wide Delegation (DWD). Hope that clears it up!
Note: Mind you, this is already possible in other client libraries, but not for the Node one for some reason