Not able to watch Admin Users Directory using `google-admin-sdk` - node.js

I am trying to connect to the G-Suite's User directory using the google-admin-sdk. I am using an API Key for authorization and I am not able to reach a successful execution.
Here is the code snippet that I'm using:
import { google } from 'googleapis';
import uuid from 'uuid/v4';
const API_KEY = 'my api key goes here';
google.admin({
version: 'directory_v1',
auth: API_KEY
}).users.list({
customer: 'my_customer',
maxResults: 10,
orderBy: 'email',
}, (err, res: any) => {
if (err) { return console.error('The API returned an error:', err.message); }
const users = res.data.users;
if (users.length) {
console.log('Users:');
users.forEach((user: any) => {
console.log(`${user.primaryEmail} (${user.name.fullName})`);
});
} else {
console.log('No users found.');
}
});
Output:
Login Required
Can someone tell me what I am doing wrong here?
Also, how do I proceed further for listening to the events emitted by the Google API?
---UPDATE---
Here is the snippet that works for me now:
import { JWT } from 'google-auth-library';
import { google } from 'googleapis';
// Importing the serivce account credentials
import { credentials } from './credentials';
const scopes = ['https://www.googleapis.com/auth/admin.directory.user'];
const adminEmail = 'admin_account_email_address_goes_here';
const myDomain = 'domain_name_goes_here';
async function main () {
const client = new JWT(
credentials.client_email,
undefined,
credentials.private_key,
scopes,
adminEmail
);
await client.authorize();
const service = google.admin('directory_v1');
const res = await service.users.list({
domain: myDomain,
auth: client
});
console.log(res);
}
main().catch(console.error);
--- Bonus Tip ---
If you face any Parse Errors while using other methods of the directory, remember to JSON.stringify the request body. For example, on the admin.users.watch method:
// Watch Request
const channelID = 'channel_id_goes_here';
const address = 'https://your-domain.goes/here/notifications';
const ttl = 3600; // Or any other TTL that you can think of
const domain = 'https://your-domain.goes';
const body = {
id: channelID,
type: 'web_hook',
address,
params: {
ttl,
},
};
// Remember to put this in an async function
const res = await service.users.watch({
domain,
customer: 'my_customer',
auth: client, // get the auth-client from above
event: 'add'
}, {
headers: {
'Content-Type': 'application/json'
},
// This is the important part
body: JSON.stringify(body),
});

As you can see in the official documentation, every request sent "to the Directory API must include an authorization token". In order to authorize your request, you have to use OAuth 2.0.
You are providing an API key instead, which is not appropriate for this process. API keys are usually used for accessing public data, not users' private data as in your current situation.
You should follow the steps provided in the Node.js Quickstart instead:
First, obtain client credentials from the Google API Console.
Second, authorize the client: obtain an access token after setting the user credentials and the appropriate scopes (a process accomplish in functions authorize and getNewToken in the Quickstart).
Finally, once the client is authorized, call the API (function listUsers).
Update:
If you want to use a Service Account for this, you will have to follow these steps:
Grant domain-wide delegation to the Service Account by following the steps specified here.
In the Cloud console, create a private key for the Service Account and download the corresponding JSON file. Copy it to your directory.
Use the Service Account to impersonate a user who has access to this resource (an Admin account). This is achieved by indicating the user's email address when creating the JWT auth client, as indicated in the sample below.
The code could be something along the following lines:
const {google} = require('googleapis');
const key = require('./credentials.json'); // The name of the JSON you downloaded
const jwtClient = new google.auth.JWT(
key.client_email,
null,
key.private_key,
['https://www.googleapis.com/auth/admin.directory.user'],
"admin#domain" // Please change this accordingly
);
// Create the Directory service.
const service = google.admin({version: 'directory_v1', auth: jwtClient});
service.users.list({
customer: 'my_customer',
maxResults: 10,
orderBy: 'email',
}, (err, res) => {
if (err) return console.error('The API returned an error:', err.message);
const users = res.data.users;
if (users.length) {
console.log('Users:');
users.forEach((user) => {
console.log(`${user.primaryEmail} (${user.name.fullName})`);
});
} else {
console.log('No users found.');
}
});
Reference:
Directory API: Authorize Requests
Directory API: Node.js Quickstart
Delegate domain-wide authority to your service account
Google Auth Library for Node.js
I hope this is of any help.

Related

Not Authorized To Access This Resource/API (GCP)

I set up a service account with domain-wide delegation and I passed the client email, private key, scopes, and a user email the JWT method to impersonate a G-Suite user. I then get specific user info from the Admin API and use it to create an email signature and push it to the Gmail API. It works great if the user email I pass to the JWT method is a super admin but if I try to pass any other user I get an error response, "Not Authorized to access this resource/api". Any ideas on how I can get it to work with a regular user account within my domain?
Here is the code. (genSignature.js)
const { google } = require('googleapis');
const privatekey = require('../private-key.json');
const scopes = [
'https://www.googleapis.com/auth/gmail.settings.basic',
'https://www.googleapis.com/auth/gmail.settings.sharing',
'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.user.readonly'
];
const auth = async (user) => {
try {
const jwtClient = new google.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
scopes,
user // User who will be impersonated using the JWT client.
);
await jwtClient.authorize();
return jwtClient;
} catch (err) {
console.log(err.message);
};
};
function genSig(e) {
auth(e).then((jwtClient) => {
// Authenticate with the gmail API.
const gmail = google.gmail({
version: 'v1',
auth: jwtClient
});
// Authenticate with the admin API.
const dir = google.admin({
version: 'directory_v1',
auth: jwtClient
});
// Get users contact and job data from the directory. This data will be used as variables in their email signature.
dir.users.get({ userKey: e }, (err, response) => {
if (err) {
console.log(err.message);
} else {
let phones = response.data.phones;
let workPhone = '';
if (phones) {
for (i = 0; i < phones.length; i++) {
if (phones[i].type == 'work') {
workPhone = phones[i].value;
};
};
};
function getUserData() {
let userData = {
name: response.data.name.fullName,
email: response.data.primaryEmail,
phone: workPhone,
avatar: response.data.thumbnailPhotoUrl,
department: response.data.organizations[0].department,
title: response.data.organizations[0].title
};
return userData;
};
let requestBody = {
signature: 'Test'
};
// Update the users email signature for their primary email.
gmail.users.settings.sendAs.update({ userId: e, sendAsEmail: e, requestBody }, (err, response) => {
if (err) {
console.log(err.message);
} else {
console.log(response.data);
};
});
};
});
});
}
module.exports = genSig;
(signatures.js)
const express = require('express');
const router = express.Router();
const genSig = require('../../functions/genSignature');
// Get webhooks from Google.
router.post('/', (req, res) => {
let email = req.body.email;
let emailStr = email.toString();
console.log(emailStr);
genSig(emailStr);
res.status(200).json({
"msg": "data recieved..."
});
});
module.exports = router;
(index.js)
const express = require('express');
const app = express();
app.use(express.json());
app.use('/email-signature', require('./routes/api/signatures'));
const PORT = process.env.PORT || 6000;
app.listen(PORT, () => console.log(`Server is running on port ${PORT}`));
Here are some screenshots.
API configuration on G-Suite
Service Account Setup
Successful request vs unsuccessful request
You need to impersonate an admin:
Only accounts with User Management privileges (like a Super Admin, or a User Management Admin) can access Users: get. You have to take into account that this is part of Admin SDK, which is to be used by admin accounts.
You can also check this is not possible if you try calling this via Try this API on the reference docs (you'll get the same message: Not Authorized to access this resource/api).
It doesn't matter that you're using a Service Account with domain-wide authority: when the service account is impersonating another user, it can only access the resources this user can access.
Solution:
In this case, the impersonated account should have user management privileges if you want to retrieve user data from Admin SDK.
But since these privileges are not necessary for calling the Gmail API method, you could impersonate an admin account when calling Users: get, and a regular one when calling users.settings.sendAs.update.
Reference:
Admin privileges definitions
Pre-built administrator roles
this is not a new post. However, I faced it and found a solution.
You can use a service account by assigning a role. See "Assign a role to a service account" in Assign specific admin role. There are details in updates blog post.
At first, you need to create a custom admin role at Google Workspace Admin Console. And you can assign service accounts to the custom admin role with email address.
It worked on Google Cloud Functions in my environment.

NodeJS Googleapis Service Account authentication

I'm trying to perform authentication on GoogleAPIs using a Service Account. I have a service account set up, with its credentials located at credentials.json. I try to access a private sheet, to which I added the E-Mail address of the service account with editing rights.
Here the code I am using:
const {
google
} = require('googleapis');
const fs = require('fs');
let scopes = ['https://www.googleapis.com/auth/spreadsheets'];
let credentials = require("./credentials.json");
const authClient = new google.auth.JWT(
credentials.client_email,
null,
credentials.private_key,
scopes);
authClient.authorize(function(err, tokens) {
if (err) {
console.log(err);
return;
} else {
authClient.setCredentials(tokens);
}
});
const sheets = google.sheets({
version: 'v4',
authClient
});
let spreadsheetId = //...
let range = //...
const request = {
spreadsheetId: spreadsheetId,
range: range
};
sheets.spreadsheets.values.get(request, function(err, response) {
if (err) {
console.log('The API returned an error: ' + err);
} else {
console.log('Result: ' + response);
}
});
I guess the API changed over time, since many guides showed different approaches, and in the end none worked for me.
The error is as follows:
The API returned an error: Error: The request is missing a valid API key.
To my understanding, a simple API key should only be necessary for unauthenticated access on public sheets, so I don't get why it is even requiring that. If I add such an API key I get the error
The API returned an error: Error: The caller does not have permission
Using
$ npm list googleapis
`-- googleapis#52.1.0
Any help would be greatly appreciated.
For who still facing googleapis problems within NodeJS Runtime in 2022.
Firstly, redirect into Google-IAM-Admin/ServiceAccount to pick the current working project.
Secondly, click to jump into Service Account that has the following format project#sub-name-id.iam.gserviceaccount.com.
Thirdly, between [Details, Permissions, Keys, Metrics, Logs]. Jump into Keys then Add Key -> Create new Key -> Key type::JSON and save JSON file to your computer.
Here within NodeJS Runtime, I use the following Semantic Version
googleapis#100.0.0
You can create JWT Client and inject into google default auth at google.options({auth: client}); or provide auth-client to specific Service as google.chat({version: 'v1', auth: client});
However, in the following example. I create a GoogleAuth instance and then make an AuthClient after. Which resulted the same behaviour to the JWT Method.
/** Import Node Native Dependencies !*/
import * as path from "path";
/** Import ES6 Default Dependencies !*/
import {google} from "googleapis";
const {client_email, private_key} = require('$/keys/credentials.json');
/**
** #description - Google [[Service Account]] Authenticator.
**/
const auth = new google.auth.GoogleAuth({
keyFile: path.resolve('keys/credentials.json'),
/** Scopes can be specified either as an array or as a single, space-delimited string; ~!*/
scopes: [
"https://www.googleapis.com/auth/chat.bot",
],
});
const client = new google.auth.JWT({
email: client_email,
key: private_key,
/** Scopes can be specified either as an array or as a single, space-delimited string; ~!*/
scopes: [
"https://www.googleapis.com/auth/chat.bot",
],
});
(async () => {
/** #description - Either [[Get Client]] from [Google Auth] or Use directly from [JWT Client] ~!*/
const client = await auth.getClient();
/** #description - Use this Authorized Client as Default Authenticated to fallback from [Non-Authenticated Services] ~!*/
google.options({auth: client});
const chat = google.chat({
version: 'v1',
/** #description - Provide [Authenticated Services] to [Google Chat Service] Instance ~!*/
auth: client,
});
const response = await chat.spaces.members.get({
// Required. Resource name of the attachment, in the form "spaces/x/messages/x/attachments/x".
name: 'spaces',
});
console.log('response', response.data);
return void 0;
})();

Google Directory API: Unable to access User/Group endpoints using Service Account (403)

I am trying to verify members of groups using the Google Directory API and cannot get past a 403 error every time I make the request.
I am using a service account, which I have enabled the "Enable G Suite Domain-wide Delegation" option for. I have also added the "https://www.googleapis.com/auth/admin.directory.user, https://www.googleapis.com/auth/admin.directory.group" Scopes using the Client ID within Suite under, "Manage API Client Access"
Code wise, I am using Node for this, and the google supplied googleapis package from NPM.
The external JSON file is the JSON credentials file downloaded when I created the service user.
Here's the code of me trying to get the request.
import { google } from 'googleapis';
async function getGroupUsers(){
const auth = await google.auth.getClient({
keyFile: './src/jwt.keys.json',
scopes: [
'https://www.googleapis.com/auth/admin.directory.group',
'https://www.googleapis.com/auth/admin.directory.group.member',
],
});
const admin = google.admin({
version: 'directory_v1',
auth,
});
const res = await admin.groups.get({
groupKey: 'redacted#domain.redacted',
});
console.log(res)
}
I can't see any obvious reason this isn't working, as I can't see how the user doesn't have permission to the resource?
Obviously missing something obvious here, as the google documentation for this is all over the shop sadly.
Help greatly appreciated!
Thanks
Gareth
Ok after much banging of head and googling I finally for there with this, final working code is as follows, not the inclusion of the client.subject value, which has to be an administrator for the domain in question.
async function validateToken(idToken) {
const keys = JSON.parse(GOOGLE_CREDS);
const client = auth.fromJSON(keys);
client.scopes = [
'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.group',
];
client.subject = 'admin#gsuite.domain';
const admin = google.admin({
version: 'directory_v1',
// auth,
auth: client,
});
const res = await admin.groups.list({
domain: 'redacted',
userKey: email,
});
const { groups } = res.data;
let role = '';
// Check for user role
if (containsGroup(USER_GROUP, groups)) {
role = USER_GROUP;
}
// Check for admin role
if (containsGroup(ADMIN_GROUP, groups)) {
role = ADMIN_GROUP;
}
// Not an admin or user so return unathenticated
if (role === '') {
return authResponse();
}
return successResponse({
'X-Hasura-User-Id': userid,
'X-Hasura-Email': email,
'X-Hasura-Role': role,
'X-Hasura-Groups': groups.map(group => group.id),
'Cache-Control': 'max-age=600',
});
}

Google Suite - Google API access - Client is unauthorized to retrieve access tokens using this method

I am struggling for days with the set up in trying to access GMail Google API from a node.js script using googleapis lib. I succeeded once but I cannot remember how I did it , I tried to reset a project, service-account and G-Suite Domain wide delegation following the Google doc ..
Here is what I did :
In my GCP Console console,
1. Existing organisation : lechorodescharentes.org
2. In this organisation , I created a project : choro-dev
3. In this project I created a service account : choro-dev-postoffice
with choro-dev-postoffice with role TokenGenerator
and enabled the Google Apps Domain-wid Delegation
downloaded the new private key ( JSON file )
4. I enabled the GMail API ( from Libray menu)
In my G-Suite domain's admin console,
5. I added the following copes for this service account's ClientID
"https://www.googleapis.com/auth/admin.directory.user",
"https://www.googleapis.com/auth/admin.directory.group"
Node.js client
I am trying to access the GMail API with the following Firebase function code using the node.js googleapis library
with server -server authentication using service account
see node.js client code
In this code, I have 2 authentication functions
connect() : to a JSON Web Token
authorize() : to request an access token from the Google OAuth 2.0 Authorization Server
Deployed the Firebase function
Run the function
Got the JWT client displayed
Function ended with error :
{"infos":"unauthorized_client: Client is unauthorized to retrieve access tokens using this method."}
node.js client code
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const {google} = require('googleapis');
const nodemailer = require('nodemailer')
const _ = require('lodash');
const KEY = require('./service-key.json');
function connect () {
return new Promise((resolve, reject) => {
const jwtClient = new google.auth.JWT(
KEY.client_email,
null,
KEY.private_key,
_.values(KEY.scopes), // scopes as authorized in G-Suite admin
KEY.admin_email . // impersonated user
);
jwtClient.authorize((err) => {
if(err) {
reject(err);
} else {
resolve(jwtClient); // returns client
}
});
});
}
// Send a message to the contact user
function sendMessage (client, sender, msg) {
return new Promise((resolve, reject) => {
var transporter = nodemailer.createTransport({
host: 'smtp.gmail.com',
port: 465,
secure: true,
auth: {
type: 'OAuth2',
user: KEY.admin_email,
serviceClient: KEY.client_id,
privateKey: KEY.private_key,
accessToken: client.access_token,
refreshToken: client.refresh_token,
expires: client.expiry_date
}
});
const mailOptions = {
from: 'SITE CONTACT<' + sender + '>',
to: KEY.contact_email,
subject: 'Message',
text: 'From: ' + sender + '\n\n' + msg,
html: '<h1>Message</h1><p>From: ' + sender + '</p><p>' + msg + '</p>'
};
transporter.sendMail(mailOptions, (err, response) => {
if (err) {
reject(err);
return;
}
resolve(response);
});
});
}
function newContactMessage (from, msg) {
return connect()
.then(client => {
return sendMessage(client, from, msg);
});
}
exports.sendContactMessage = functions.https.onRequest((req, res) => {
const sender_email = 'dufourisabelle#orange.fr';
const sender_msg = 'just a test message to contact the site owner.'
newContactMessage(sender_email, sender_msg).then(() => {
return {status: 200};
}, error => {
return {status: error.status, infos: error.message};
}).then(response => {
return res.send(response);
}).catch(console.error);
});
What could I add to it ? I'll try to re-initiate the all process and pray ... ??

google-auth-library v.1.0 how to use just for identity?

I,ve been using google-auth-library#0.10.0 nodejs just for verifying user identity in my api services, now it changed to 1.0 and everything is broken.
I previously used example from here:
https://developers.google.com/identity/sign-in/web/backend-auth
now I cannot figure out how to verify identity using the new library.
Examples here: https://github.com/google/google-auth-library-nodejs
explains how to get access to google apis, I just need to verify identity.
this is my code:
const GoogleAuth = require('google-auth-library');
const auth = new GoogleAuth.GoogleAuth();
const google = require('googleapis');
const authData = {
'googleAuth': {
'clientID': 'xxxxxxxxxxx-aaaaaaaaaaaaaaaaaaaaaa.apps.googleusercontent.com',
'clientSecret': 'sssssssssssssssssssssssss',
'callbackURL': 'http://localhost:121212/auth/'
}
};
const CLIENT_ID = authData.googleAuth.clientID;
function verifyToken(token) {
let tokenPromise = new Promise(function(resolve, reject) {
client.verifyIdToken(
token,
CLIENT_ID,
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3],
function(e, login) {
if (e) {
return reject(e);
} else {
var payload = login.getPayload();
var userid = payload['sub'];
//console.log(payload, userid, e, login);
return resolve(login);
// If request specified a G Suite domain:
//var domain = payload['hd'];
}
});
});
return tokenPromise;
};
it was working fine, just to get googleUserId.
now I've this error:
Error: This method accepts an options object as the first parameter, which includes the idToken, audience, and maxExpiry.
I understand I'm not passing properly parameters and maybe its not the method to use..but how can I know which method and how it accepts parameters??? I cannot find any documentation about google-auth-library#1.0...some one is using it in production??
The verifyIdToken changed. Here's a working example.
import { OAuth2Client } from 'google-auth-library';
const client = new OAuth2Client(
'xxxxxxxxxxx-aaaaaaaaaaaaaaaaaaaaaa.apps.googleusercontent.com,
'sssssssssssssssssssssssss',
'http://localhost:121212/auth/',
);
client.verifyIdToken({
idToken: TOKEN,
audience: CLIENT_ID_1 // If you have multiple [CLIENT_ID_1, CLIENT_ID_2, ...]
}, (err, login) => {
console.log(login);
});
Just tested this recently.
Google needs to update their documentation.
Please note that you can specify a maxAge to the verify id token options as well.
Here's the interface copied from the source code
export interface VerifyIdTokenOptions {
idToken: string;
audience: string|string[];
maxExpiry?: number;
}
As you can see the idToken and audience are required, the maxExpiry is optional.
Source can be found here

Resources