Service account with Google Workspace-authorized domain-wide delegation gets error "Not Authorized to access this resource/api" when trying to use admin SDK for scopes from a GCP cloud function that the Workspace has authorized the service account's client ID to access. Not sure what the issue is.
Have a GCP Cloud Funciton (that I am sending requests to via GCP API gateway) configured with...
Service account: my-domain-wide-delegation-enabled-serviceaccount@my-gcp-project-name.iam.gserviceaccount.com
Build service account: [email protected]
Cloud function contains a helper function like...
const SCOPES = [
'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.group',
'https://www.googleapis.com/auth/gmail.send'
//'https://www.googleapis.com/auth/drive.readonly',
//'https://www.googleapis.com/auth/documents.readonly',
//'https://www.googleapis.com/auth/iam.serviceAccounts.credentials'
];
async function getWorkspaceCredentials() {
try {
console.log("Getting workspace creds...");
const auth = new google.auth.GoogleAuth({
scopes: SCOPES
});
// Get the source credentials
console.log("Getting client...");
const client = await auth.getClient();
console.debug("Client info: ", {
email: client.email, // service account email
scopes: client.scopes // actual scopes being used
});
const email = await auth.getCredentials();
console.debug("Service account details: ", {
email: email.client_email,
project_id: email.project_id,
type: email.type
});
console.log("Setting client subject (admin user to impersonate)...")
client.subject = '[email protected]';
const token = await client.getAccessToken();
console.debug("Successfully got test access token: ", token.token.substring(0,10) + "...");
console.log("Workspace creds obtained successfully.");
return client;
} catch (error) {
console.error('Failed to get workspace credentials:', error);
throw error;
}
}
... and used in the entry-point function like...
functions.http('createNewWorkspaceAccount', async (req, res) => {
// Get Workspace credentials and create admin service
const auth = await getWorkspaceCredentials();
console.debug("auth credentials: ", auth);
const admin = google.admin({ version: 'directory_v1', auth });
console.debug("admin service from auth credentials: ", admin);
// DEBUG testing
const testList = await admin.users.list({
domain: 'mydomain.com',
maxResults: 1
});
console.debug("Test list response: ", testList.data);
console.debug("Admin-queried user data for known testing user check: ", await admin.users.get({userKey: "[email protected]"}));
});
I keep getting an error like...
Error processing request: {
error: {
code: 403,
message: 'Not Authorized to access this resource/api',
errors: [ [Object] ]
}
}
... when we get to the admin.users.list()
line. IDK what is going wrong here.
Here are some of the log messages I get when running the helper function...
Client info: {
email: undefined,
scopes: [
'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.group',
'https://www.googleapis.com/auth/gmail.send'
]
}
Service account details: {
email: 'my-domain-wide-delegation-enabled-serviceaccount@my-gcp-project-name.iam.gserviceaccount.com',
project_id: undefined,
type: undefined
}
... the logs from the...
console.debug("auth credentials: ", auth);
console.debug("admin service from auth credentials: ", admin);
...lines in the entry function are very long, so was not sure what would be helpful to post from those here, but execution does reach these lines. *EDIT (I've added this for more detail with some values redacted in case there is something informative here that I'm missing).
auth credentials: Compute {
_events: [Object: null prototype] {},
_eventsCount: 0,
_maxListeners: undefined,
transporter: DefaultTransporter {},
credentials: [Object: null prototype] {
access_token: 'qwertyqwertyqwerty',
token_type: 'Bearer',
expiry_date: 123456789101112,
refresh_token: 'compute-placeholder'
},
eagerRefreshThresholdMillis: 300000,
forceRefreshOnFailure: false,
certificateCache: {},
certificateExpiry: null,
certificateCacheFormat: 'PEM',
refreshTokenPromises: Map(0) {},
_clientId: undefined,
_clientSecret: undefined,
redirectUri: undefined,
serviceAccountEmail: 'default',
scopes: [
'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.group',
'https://www.googleapis.com/auth/gmail.send'
],
subject: '[email protected]',
.
.
.
}
admin service from auth credentials: Admin {
context: {
_options: { auth: [Compute] },
google: GoogleApis {
abusiveexperiencereport: [Function: abusiveexperiencereport],
acceleratedmobilepageurl: [Function: acceleratedmobilepageurl],
accessapproval: [Function: accessapproval],
accesscontextmanager: [Function: accesscontextmanager],
adexchangebuyer: [Function: adexchangebuyer],
adexchangebuyer2: [Function: adexchangebuyer2],
adexperiencereport: [Function: adexperiencereport],
admin: [Function: admin],
.
.
.
_discovery: [Discovery],
auth: [AuthPlus],
_options: {}
}
},
asps: Resource$Asps {
context: { _options: [Object], google: [GoogleApis] }
},
channels: Resource$Channels {
context: { _options: [Object], google: [GoogleApis] }
},
chromeosdevices: Resource$Chromeosdevices {
context: { _options: [Object], google: [GoogleApis] }
},
customer: Resource$Customer {
context: { _options: [Object], google: [GoogleApis] },
devices: Resource$Customer$Devices {
context: [Object],
chromeos: [Resource$Customer$Devices$Chromeos]
}
},
customers: Resource$Customers {
context: { _options: [Object], google: [GoogleApis] },
chrome: Resource$Customers$Chrome {
context: [Object],
printers: [Resource$Customers$Chrome$Printers]
}
},
domainAliases: Resource$Domainaliases {
context: { _options: [Object], google: [GoogleApis] }
},
domains: Resource$Domains {
context: { _options: [Object], google: [GoogleApis] }
},
groups: Resource$Groups {
context: { _options: [Object], google: [GoogleApis] },
aliases: Resource$Groups$Aliases { context: [Object] }
},
members: Resource$Members {
context: { _options: [Object], google: [GoogleApis] }
},
mobiledevices: Resource$Mobiledevices {
context: { _options: [Object], google: [GoogleApis] }
},
orgunits: Resource$Orgunits {
context: { _options: [Object], google: [GoogleApis] }
},
privileges: Resource$Privileges {
context: { _options: [Object], google: [GoogleApis] }
},
resources: Resource$Resources {
context: { _options: [Object], google: [GoogleApis] },
buildings: Resource$Resources$Buildings { context: [Object] },
calendars: Resource$Resources$Calendars { context: [Object] },
features: Resource$Resources$Features { context: [Object] }
},
roleAssignments: Resource$Roleassignments {
context: { _options: [Object], google: [GoogleApis] }
},
roles: Resource$Roles {
context: { _options: [Object], google: [GoogleApis] }
},
schemas: Resource$Schemas {
context: { _options: [Object], google: [GoogleApis] }
},
tokens: Resource$Tokens {
context: { _options: [Object], google: [GoogleApis] }
},
twoStepVerification: Resource$Twostepverification {
context: { _options: [Object], google: [GoogleApis] }
},
users: Resource$Users {
context: { _options: [Object], google: [GoogleApis] },
aliases: Resource$Users$Aliases { context: [Object] },
photos: Resource$Users$Photos { context: [Object] }
},
verificationCodes: Resource$Verificationcodes {
context: { _options: [Object], google: [GoogleApis] }
}
}
Here is the full error:
GaxiosError: Not Authorized to access this resource/api
at Gaxios._request (/workspace/node_modules/googleapis-common/node_modules/gaxios/build/src/gaxios.js:129:23)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async Compute.requestAsync (/workspace/node_modules/googleapis-common/node_modules/google-auth-library/build/src/auth/oauth2client.js:368:18)
at async /workspace/index.js:236:22 {
response: {
config: {
url: 'https://admin.googleapis.com/admin/directory/v1/users?domain=mydomain.com&maxResults=1',
method: 'GET',
userAgentDirectives: [Array],
paramsSerializer: [Function (anonymous)],
headers: [Object],
params: [Object],
validateStatus: [Function (anonymous)],
retry: true,
responseType: 'json',
retryConfig: [Object]
},
data: { error: [Object] },
headers: {
'alt-svc': 'h3=":443"; ma=2592000,h3-29=":443"; ma=2592000',
'content-encoding': 'gzip',
'content-type': 'application/json; charset=UTF-8',
date: 'Tue, 14 Jan 2025 21:28:50 GMT',
server: 'ESF',
'transfer-encoding': 'chunked',
vary: 'Origin, X-Origin, Referer',
'x-content-type-options': 'nosniff',
'x-frame-options': 'SAMEORIGIN',
'x-xss-protection': '0'
},
status: 403,
statusText: 'Forbidden',
request: {
responseURL: 'https://admin.googleapis.com/admin/directory/v1/users?domain=mydomain.com&maxResults=1'
}
},
config: {
url: 'https://admin.googleapis.com/admin/directory/v1/users?domain=mydomain.com&maxResults=1',
method: 'GET',
userAgentDirectives: [ [Object] ],
paramsSerializer: [Function (anonymous)],
headers: {
'x-goog-api-client': 'gdcl/5.1.0 gl-node/20.18.1 auth/7.14.1',
'Accept-Encoding': 'gzip',
'User-Agent': 'google-api-nodejs-client/5.1.0 (gzip)',
Authorization: 'Bearer qwertyqwertyqwerty',
Accept: 'application/json'
},
params: { domain: 'mydomain.com', maxResults: 1 },
validateStatus: [Function (anonymous)],
retry: true,
responseType: 'json',
retryConfig: {
currentRetryAttempt: 0,
retry: 3,
httpMethodsToRetry: [Array],
noResponseRetries: 2,
statusCodesToRetry: [Array]
}
},
code: 403,
errors: [
{
message: 'Not Authorized to access this resource/api',
domain: 'global',
reason: 'forbidden'
}
]
}
I've also double-checked that the OAuth 2 Client ID in the GCP project for the my-domain-wide-delegation-enabled-serviceaccount@my-gcp-project-name.iam.gserviceaccount.com
service account at IAM & Admin > Service Accounts does indeed match the Client ID in the Google Workspace's Security > API Controls > Domain-wide Delegation UI, the scopes enabled there for that client ID are...
https://www.googleapis.com/auth/admin.directory.user
https://www.googleapis.com/auth/admin.directory.group
https://www.googleapis.com/auth/gmail.send
Note that the only role that this service account has in the GCP project's IAM & Admin > IAM UI is "Secret Manager Secret Accessor" (IDK if this is good enough or not, but there is logic before the code snippet of the entry function I've shown that runs fine with just these role permissions, so didn't think it should be an issue).
I have Admin SDK enable for the project, but do I need to add that as a role for the service account? What is that role called? (I wouldn't normally think this is the issue as I usually get a different kind of error message when a service account is trying to use an API it does not have role permissions for, but I'm stuck on what else could be going on here).
The testadminaccount
is indeed an admin account (I can see their properties in Workspace and see that they are in fact have super admin role). I can sign into Chrome as that user and go to our Google Workspace UI and browse the user directory, edit their info, and create new users, etc. *EDIT: IRL the testadminaccount
I'm testing with is my own super admin account that I myself use to admin the Google Workspace.
I've tried removing the service account client ID from Workspace's DwD list and re-adding it; this is not help.
I've even tried setting the service account to have owner role in the GCP project and this did not change anything.
Anyone with more experience have any idea what the issue could be here?
Thanks.
Per the comments by @DazWilkin, I simply gave up on the specific google.auth.GoogleAuth({})
and getClient()
method I was using and just used a keyfile json of the DWD-enabled service account, copied as a secret in the GCP project's Secrets Manager, and then accessing that secret keyfile data from the function via google.auth.JWT()
(see https://cloud.google.com/nodejs/docs/reference/google-auth-library/latest/google-auth-library/jwt) in order to have that DWD-enabled service account impersonate a specified admin user.
Relevant code included below:
async function getWorkspaceCredentials() {
try {
// Get the service account key from Secret Manager
console.debug("Accessing service account keyfile info...")
const secretManager = new SecretManagerServiceClient();
const name = WORKSPACE_DWD_SERVACCT_KEYFILE_SECRET_URI;
const [version] = await secretManager.accessSecretVersion({ name });
const serviceAccountKey = JSON.parse(version.payload.data.toString());
console.debug("Service account private key ID: ", serviceAccountKey.private_key_id);
// Create JWT client with the service account key
console.log("Getting workspace creds...");
const auth = new google.auth.JWT( // https://cloud.google.com/nodejs/docs/reference/google-auth-library/latest/google-auth-library/jwt
serviceAccountKey.client_email,
null,
serviceAccountKey.private_key,
SCOPES,
ADMIN_IMPERSONATION_ACCOUNT
);
// Authorize the client
await auth.authorize();
console.debug("JWT client info: ", {
email: auth.email,
subject: auth.subject,
scopes: auth.scopes
});
console.log("Workspace creds obtained successfully.");
return auth;
} catch (error) {
console.error('Failed to get workspace credentials:', error);
throw error;
}
}
Then in the entry function...
functions.http('myEntryFunction', async (req, res) => {
do.stuff();
// Get Workspace credentials and create admin service
const auth = await getWorkspaceCredentials();
console.debug("auth credentials: ", auth);
const admin = google.admin({ version: 'directory_v1', auth });
console.debug("Admin service from auth credentials: ", admin);
console.debug("Testing admin credentials...")
// DEBUG testing
console.debug("Admin-queried user data for known testing user check: ", await admin.users.get({userKey: "[email protected]"}));
console.debug("Admin credentials testing verified.")
do.otherStuff();
});