Open hsw48 opened 4 years ago
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@domain.com.au';
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@domain.com.au',
});
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: 'someuser@domain.com',
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?
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@email.com'
},
});
@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.to.impersonate@email.com'
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: 'other.attendee.email@email.com' }]
}
}, (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@email.com' }, });
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: "admin@mydomain.com",
},
});
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@email.com' }, });
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: 'impersonated@email.com',
})
maybe this could help
const auth = new google.auth.GoogleAuth({ keyFile: './service.json', scopes: [ 'https://www.googleapis.com/auth/contacts',... ], clientOptions: { subject: 'email@email.com' }, });
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@email.com' }, });
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@email.com' }, });
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@domain.com.au', }); 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.
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.
1) optional: destroy your ~/.config/gcloud
directory to ensure no other pre-existing login credentials conflict with the authentication: rm -rf ~/.config/gcloud
2) provide ADC to gcloud: gcloud auth application-default login
and follow the login flow in the browser, logging in with your Google Account.
3) provide your ADC Google Account the Service Account Token Creator Role on the Service Account you are trying to impersonate.
4) 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.
5) 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
Ahh -- Got it, @BoscoDomingo. I don't want to derail convo. Will search for a more suitable place online to post my findings. Update: I used the Hide function above and found a SO Question to answer instead.
@charlesjacobsonrarebirds
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
Can you please direct me how to do this in python client . Also does this method need Domain wide delegation? I came from here https://stackoverflow.com/questions/67396417/error-invalid-conference-type-value-while-creating-event-with-google-meet-link
Its difficult for us to enable domain wide delegation because I dont need to invite users or access their calenders. the only thing I need is to create google meeting where everybody can join
@akshaychopra5207, Did you ever figure this out? I am also trying to created a google meeting where everyone can join and getting this error in python?
Just FYI this is still an issue, 4 years later. Python and Java already have this functionality, so it's a matter of translating code unless I'm missing something (I haven't been able to look into the code)
Hello,
I am not sure this is helpful or not for people who like us wants to use WIF with temporary access credentials generated from the client library config file and impersonation with SA for domain wide delegation.
We rebuilt the functionality used in this Github Action (https://github.com/google-github-actions/auth) so all credit to that source for figuring out the boilerplate. We reuse this in Lambdas and other types of AWS Services.
Full disclaimer I don't consider myself to be a node / typescript coder so don't take the code here for granted. There are probably a lot of things that could be neater
import axios from 'axios'; // we need this to build the DWD
import { ExternalAccountClient, OAuth2Client } from 'google-auth-library';
import { SecretService } from './secret'; // deals with fetching secrets from aws secretsmanager
import { SignedJWT } from '../domain/google';
/*
export interface SignedJWT {
kid: string;
signedJwt: string;
}
*/
import { GOOGLE_API_SCOPES } from '../constants'; // Just definition of the API Scopes requested
import { logger } from '../logging'; // This is a lambda powertools configuration
const {
ENVIRONMENT,
GCP_PROJECT_ID,
SERVICE_ACCOUNT_EMAIL
} = process.env;
// userAgent is the default user agent.
const userAgent = `someuser-agent-you-want-to-use`;
export async function generateOauth2Client(secretService: SecretService, impersonationEmail: string): Promise<OAuth2Client> {
const creds = await secretService.getGoogleSecret(); // get SA config for WIF
const auth_client = ExternalAccountClient.fromJSON(creds);
if (!auth_client) { throw Error('no client created') }
auth_client.scopes = ['https://www.googleapis.com/auth/cloud-platform'];
if (!GCP_PROJECT_ID || !SERVICE_ACCOUNT_EMAIL) { throw Error('missing required gcp env vars') }
if (!auth_client) { throw Error('no client created') }
logger.debug("requesting dwd credentials");
const unsignedJwt = buildDomainWideDelegationJWT(
SERVICE_ACCOUNT_EMAIL,
impersonationEmail,
GOOGLE_API_SCOPES,
3600, // 1 hour, longer duration is unsupported with DWD
);
// build dwd request
const url = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${SERVICE_ACCOUNT_EMAIL}:signJwt`;
const headers = await auth_client.getRequestHeaders();
const body = {
payload: unsignedJwt
}
const signJwtResponse = (await auth_client.request({
url: url,
body: JSON.stringify(body),
headers: headers,
method: 'POST'
})).data as SignedJWT;
if (!signJwtResponse.signedJwt) {
throw new Error("No data in response")
}
const access_token = await generateDomainWideDelegationAccessToken(signJwtResponse.signedJwt);
// We get an OAuth2 access token, we need to convert this into a client to be able to initiate the other google api clients such as admin or gmail
const oauth2Client = new OAuth2Client();
oauth2Client.setCredentials({ access_token: access_token });
return oauth2Client
function buildDomainWideDelegationJWT(
serviceAccount: string,
subject: string | undefined | null,
scopes: Array<string> | undefined | null,
lifetime: number,
): string {
const now = Math.floor(new Date().getTime() / 1000);
const body: Record<string, string | number> = {
iss: serviceAccount,
aud: 'https://oauth2.googleapis.com/token',
iat: now,
exp: now + lifetime,
};
if (subject && subject.trim().length > 0) {
body.sub = subject;
}
if (scopes && scopes.length > 0) {
// Yes, this is a space delimited list.
// Not a typo, the API expects the field to be "scope" (singular).
body.scope = scopes.join(' ');
}
return JSON.stringify(body);
}
/*
We need to send this request with axios (or other http client) because we want to use a JWT (JSON Web Token) for Domain-Wide Delegation of Authority.
The Google Auth Library for Node.js does not provide a built-in method for this specific use case as far as we have found
*/
async function generateDomainWideDelegationAccessToken(
signedJwt: string,
): Promise<string> {
const url = 'https://oauth2.googleapis.com/token';
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': userAgent
};
const body = new URLSearchParams();
body.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
body.append('assertion', signedJwt);
try {
const resp = await axios.post(url, body.toString(), { headers });
if (resp.status < 200 || resp.status > 299) {
throw new Error(`Failed to call ${url}: HTTP ${resp.status}: ${resp.data || '[no body]'}`);
}
logger.debug("Got oauth2 access token")
return resp.data.access_token;
} catch (err) {
throw new Error(`Failed to generate Google Cloud Domain Wide Delegation OAuth 2.0 Access Token: ${err}`);
}
}
}
Apart from this the service account needs to have service account token creator permission.
You can then start other clients such as admin or gmail clients ie.
import { gmail_v1 } from '@googleapis/gmail';
const oauth2Client = await generateOauth2Client(secretService, impersonationEmail);
gmailClient = new gmail_v1.Gmail({ auth: oauth2Client });
I want to build in top of @tcvall86 answer, but using the application default credentials, as the original OP requested (and I also needed), so here is the improved version (moved back to vanilla JS):
const axios = require("axios"); // we need this to build the DWD
const { GoogleAuth } = require("google-auth-library");
const { OAuth2Client } = require("google-auth-library");
logger = console;
// userAgent is the default user agent.
const userAgent = `someuser-agent-you-want-to-use`;
async function generateOauth2Client(impersonationEmail, gcpProjectId, serviceAccountEmail, googleApiScopes) {
const auth_client = new GoogleAuth({
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
});
if (!auth_client) {
throw Error("no client created");
}
if (!gcpProjectId || !serviceAccountEmail) {
throw Error("missing required gcp env vars");
}
if (!auth_client) {
throw Error("no client created");
}
logger.debug("requesting dwd credentials");
const unsignedJwt = buildDomainWideDelegationJWT(
serviceAccountEmail,
impersonationEmail,
googleApiScopes,
3600 // 1 hour, longer duration is unsupported with DWD
);
// build dwd request
const url = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccountEmail}:signJwt`;
const headers = await auth_client.getRequestHeaders();
const body = {
payload: unsignedJwt,
};
const signJwtResponse = (
await auth_client.request({
url: url,
body: JSON.stringify(body),
headers: headers,
method: "POST",
})
).data;
if (!signJwtResponse.signedJwt) {
throw new Error("No data in response");
}
const access_token = await generateDomainWideDelegationAccessToken(
signJwtResponse.signedJwt
);
// We get an OAuth2 access token, we need to convert this into a client to be able to initiate the other google api clients such as admin or gmail
const oauth2Client = new OAuth2Client();
oauth2Client.setCredentials({ access_token: access_token });
return oauth2Client;
function buildDomainWideDelegationJWT(
serviceAccount,
subject,
scopes,
lifetime
) {
const now = Math.floor(new Date().getTime() / 1000);
const body = {
iss: serviceAccount,
aud: "https://oauth2.googleapis.com/token",
iat: now,
exp: now + lifetime,
};
if (subject && subject.trim().length > 0) {
body.sub = subject;
}
if (scopes && scopes.length > 0) {
// Yes, this is a space delimited list.
// Not a typo, the API expects the field to be "scope" (singular).
body.scope = scopes.join(" ");
}
return JSON.stringify(body);
}
/*
We need to send this request with axios (or other http client) because we want to use a JWT (JSON Web Token) for Domain-Wide Delegation of Authority.
The Google Auth Library for Node.js does not provide a built-in method for this specific use case as far as we have found
*/
async function generateDomainWideDelegationAccessToken(signedJwt) {
const url = "https://oauth2.googleapis.com/token";
const headers = {
Accept: "application/json",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": userAgent,
};
const body = new URLSearchParams();
body.append("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
body.append("assertion", signedJwt);
try {
const resp = await axios.post(url, body.toString(), { headers });
if (resp.status < 200 || resp.status > 299) {
throw new Error(
`Failed to call ${url}: HTTP ${resp.status}: ${
resp.data || "[no body]"
}`
);
}
logger.debug("Got oauth2 access token");
return resp.data.access_token;
} catch (err) {
throw new Error(
`Failed to generate Google Cloud Domain Wide Delegation OAuth 2.0 Access Token: ${err}`
);
}
}
}
module.exports = {
generateOauth2Client
};
And this is the way that it is used:
[...]
const { generateOauth2Client } = require("./impersonation.js");
[...]
const auth = await generateOauth2Client(
"<USE-OWN-IMPERSONATED-USER>",
"<USE-YOUR-OWN-PROJECT>",
"<USE-YOUR-OWN-IMPERSONATING-SA",
[
"https://www.googleapis.com/auth/drive.readonly",
]
);
const service = google.drive({ version: "v3", auth });
resAbout = await service.about.get({
fields: "*",
});
console.log(resAbout.data.user);
The result of said code is printing the information about the impersonated user, meaning that we have managed to really impersonate it!
@lopezvit Nice one! I will give it a go whenever I have time for it, and let you know if it worked for me too. Can't promise it'll be soon though (it's for work and we've more pressing matters atm)
In case anyone else is interested in the TypeScript version of @tcvall86 and @lopezvit snippets:
import { GoogleAuth } from 'google-auth-library';
const GOOGLE_OAUTH2_TOKEN_API_URL = 'https://oauth2.googleapis.com/token';
/**
* This function generates an OAuth2 Access Token with scopes obtained via domain-wide delegation
* without requiring a JSON key file from a Service Account.
*
* Resources:
* - https://github.com/googleapis/google-auth-library-nodejs/issues/916
* - https://cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys?hl=es-419#domain-wide-delegation
*
* @param impersonationEmail Email address of the Google account that has granted domain-wide scopes to the service account
* @param serviceAccountEmail Service account which received the scopes using domain-wide delegation
* @param googleApiScopes Scopes to request for the generated Access Token
* @param lifetime Lifetime, in seconds, of the generated access token. It can't be greater than 1h
* @returns the generated access token
*/
export async function getDomainWideDelegationAccessToken(
impersonationEmail: string,
serviceAccountEmail: string,
googleApiScopes: string[],
lifetime: number,
): Promise<string> {
if (!impersonationEmail) {
throw Error('impersonationEmail is required');
}
if (!serviceAccountEmail) {
throw Error('serviceAccountEmail is required');
}
if (lifetime > 3600) {
throw Error('lifetime cannot be greater than 3600 seconds (1 hour)');
}
// Build client using Application Default Credentials
const auth_client = new GoogleAuth({
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
});
// Build JWT token for domain-wide delegation
const unsignedJwt = buildDomainWideDelegationJWT(serviceAccountEmail, impersonationEmail, googleApiScopes, lifetime);
// Sign JWT token using a system-managed private key of the given service account
const signJwtResponse = (
await auth_client.request({
url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccountEmail}:signJwt`,
body: JSON.stringify({
payload: unsignedJwt,
}),
headers: await auth_client.getRequestHeaders(),
method: 'POST',
})
).data;
if (!signJwtResponse.signedJwt) {
throw new Error('Failed to sign JWT token using the Service Account key');
}
return await generateDomainWideDelegationAccessToken(signJwtResponse.signedJwt);
}
/**
* Builds the payload to request a JWT token using domain-wide delegation.
* See: https://cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys?hl=es-419#domain-wide-delegation
*/
function buildDomainWideDelegationJWT(
serviceAccount: string,
subject: string,
scopes: string[],
lifetime: number,
): string {
const now = Math.floor(new Date().getTime() / 1000);
const body: Record<string, string | number | undefined> = {
iss: serviceAccount,
aud: GOOGLE_OAUTH2_TOKEN_API_URL,
iat: now,
exp: now + lifetime,
sub: subject ?? undefined,
// Yes, this is a space delimited list.
// Not a typo, the API expects the field to be "scope" (singular).
scope: scopes && scopes.length > 0 ? scopes.join(' ') : undefined,
};
return JSON.stringify(body);
}
/**
* We need to send this request using an alternative http client because we want to use a JWT (JSON Web Token) for Domain-Wide Delegation of Authority.
* The Google Auth Library for Node.js does not provide a built-in method for this specific use case.
* See: https://cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys?hl=es-419#domain-wide-delegation
*/
async function generateDomainWideDelegationAccessToken(signedJwt: string): Promise<string> {
const url = GOOGLE_OAUTH2_TOKEN_API_URL;
const headers = {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
};
const body = new URLSearchParams();
body.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
body.append('assertion', signedJwt);
try {
const resp = await fetch(url, {
method: 'POST',
body: body.toString(),
headers,
});
if (resp.status < 200 || resp.status > 299) {
throw new Error(`Failed to call ${url}: HTTP ${resp.status}: ${await resp.text()}`);
}
const data = (await resp.json()) as { access_token: string };
return data.access_token;
} catch (err) {
throw new Error(`Failed to generate Google Cloud Domain Wide Delegation OAuth 2.0 Access Token: ${err}`);
}
}
This can be used as follows:
// Generate access token using Domain-wide delegation
const accessToken = await getDomainWideDelegationAccessToken(
'impersonated-user@example.com',
'impersonating-service-account@example.com',
['scope1', 'scope2'], // Requested scopes
15 * 60, // Lifetime of the access token, it cannot be greater than 1h
)
const oauth2Client = new OAuth2Client();
oauth2Client.setCredentials({ access_token: accessToken });
I would also really like this feature. Here is what I am currently using - I adjusted the above code a little bit to make use of the signJwt
method that Google exposes in their IAMCredentialsClient, here it is:
import { IAMCredentialsClient } from '@google-cloud/iam-credentials'
const GOOGLE_OAUTH2_TOKEN_API_URL = 'https://oauth2.googleapis.com/token';
const buildUnsignedJwt = (serviceAccountEmail: string, impersonatedWorkspaceUserEmail: string, scopes: string[]): string => {
const now = Math.floor(new Date().getTime() / 1000);
const body: Record<string, string | number | undefined> = {
iss: serviceAccountEmail,
aud: GOOGLE_OAUTH2_TOKEN_API_URL,
iat: now,
exp: now + 3600,
sub: impersonatedWorkspaceUserEmail,
scope: scopes.join(' '),
}
return JSON.stringify(body)
}
const generateDomainWideDelegationAccessToken = async (signedJwt: string): Promise<string> => {
const headers = {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
};
const body = new URLSearchParams();
body.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
body.append('assertion', signedJwt);
try {
const resp = await fetch(GOOGLE_OAUTH2_TOKEN_API_URL, {
method: 'POST',
body: body.toString(),
headers,
});
if (resp.status < 200 || resp.status > 299) {
throw new Error(`Failed to call ${GOOGLE_OAUTH2_TOKEN_API_URL}: HTTP ${resp.status}: ${await resp.text()}`);
}
const data = (await resp.json()) as { access_token: string };
return data.access_token;
} catch (err) {
throw new Error(`Failed to generate Google Cloud Domain Wide Delegation OAuth 2.0 Access Token: ${err}`);
}
}
export const getDomainWideDelegationAccessToken = async (impersonatedWorkspaceUserEmail: string, serviceAccountEmail: string, scopes: string[]): Promise<string> => {
if (!impersonatedWorkspaceUserEmail) throw Error('impersonationEmail is required');
if (!serviceAccountEmail) throw Error('serviceAccountEmail is required');
if (scopes.length === 0) throw Error('No scopes were provided');
// Build unsigned JWT
const unsignedJwt = buildUnsignedJwt(serviceAccountEmail, impersonatedWorkspaceUserEmail, scopes);
// Sign JWT token using a system-managed private key of the given service account
const [signedJwtResponse] = await new IAMCredentialsClient().signJwt({
name: `projects/-/serviceAccounts/${serviceAccountEmail}`,
payload: unsignedJwt,
})
const { signedJwt } = signedJwtResponse
if (!signedJwt) throw new Error('Failed to sign JWT token using the Service Account key');
return await generateDomainWideDelegationAccessToken(signedJwt);
}
Thanks for your code contributions @tcvall86 @lopezvit @alitto @antnat96 ! So helpful! For anyone else trying out one of those solutions, make sure to add the Service Account Token Creator role to your service account in IAM. (I needed it with @alitto 's version.) It's so annoying this is still a limitation in the library, but this workaround is great!
method that Google exposes in their IAMCredentialsClient
Let me add that googleapis
package has this as well. This is how I call it:
// Sign JWT token using a system-managed private key of the given service account
await google.iamcredentials('v1').projects.serviceAccounts.signJwt({
name: `projects/-/serviceAccounts/${serviceAccountEmail}`,
requestBody: {
// Build JWT token for domain-wide delegation
payload: unsignedJWT,
},
auth,
})
I am not sure if this helps others but I was able to provide a service account using credentials
:
export function getGoogleAuth(serviceAccount: string): Auth.GoogleAuth {
const SCOPES = [...];
const impersonationEmail = "email@domain.com";
return new google.auth.GoogleAuth({
scopes: SCOPES,
clientOptions: {subject: impersonationEmail},
credentials: JSON.parse(serviceAccount),
});
}
Thank god i found this thread because for me it was totally unclear how to deal with the " Service accounts cannot invite attendees without Domain-Wide Delegation of Authority." error even though i did the DWD setup correctly.
Problem is, you see pretty good tutotials on the web for other programming languages, like this one for C# (https://medium.com/iceapple-tech-talks/integration-with-google-calendar-api-using-service-account-1471e6e102c8) and then you realize that the c# client obviously behaves differently than my target language JS/TS and the node lib.
What i still dont get, that while i cant add attendees to a calendar event without clientOptions->subject, the mentioned subject has nothing to do with my attendees emails or the service user i am using. I just added one of my admin email address from my workspace users. I dont get the idea of "subject" at all. I always thought the service account is the one which does the calls.
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.