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.
Related
I am managing an application written in Node.js (restful backend) and React (frontend) and use AWS Cognito for user authentication.
In the frontend, I have a form which requests my backend to delete a user. The only information the frontend can send me is the username (email in my case) and an access token which I sent to the frontend after a successful login.
Now I want to delete a user. Until now, I tried it with the deleteUser method (see Use case 13 in the js documentation).:
const poolData = {
UserPoolId: process.env.COGNITO_USER_POOL_ID,
ClientId: process.env.CLIENT_ID
};
AWS.config.region = process.env.AWS_DEFAULT_REGION;
const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
// ...
router.post('/deleteUser', (req, res) => {
var userData = {
Username: req.body.email,
Pool: userPool
};
var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
if (cognitoUser == null) {
res.status(400).send('User not found');
return;
}
cognitoUser.deleteUser(function(err, result) {
if (err) {
res.status(400).send('Could not delete user');
} else {
res.status(200).send('Deleted user');
}
});
});
This throws an error in the error block of deleteUser section with Error: User is not authenticated. I need to firstly authenticate the user but I won't get the password from the frontend to do so. I just have the access token it can provide me.
How can I authenticate the user with the provided token? Or am I misunderstanding a concept here? If so, what am I doing wrong?
Any help will much appreciated.
I'm trying to work with the google API's for the first time, and when I attempt to make a request to the gmail API I'm getting a "precondition check failed" error. I am using a service account authorization, not Oauth2 user consent. Things I've tried:
Authorized "domain wide delegation" for the service account.
Ensured the APP is trusted in the G suite account.
Ensured service account role is "owner"
Enabled domain wide delegation for the client ID of the service account in the g suite admin panel.
This is an adapted sample from the Node client library, but the sample did not use service account auth so I wasn't able to use the sample directly.
const path = require('path');
const {google} = require('googleapis');
const gmail = google.gmail('v1');
async function runSample() {
// Obtain user credentials to use for the request
const auth = new google.auth.GoogleAuth({
keyFile: path.resolve(__dirname, 'google-key.json'),
scopes: ['https://www.googleapis.com/auth/gmail.readonly'],
});
google.options({auth});
const res = await gmail.users.messages.list({userId: 'me'}); // have tried with my gsuite email address as well
console.log(res.data);
return res.data;
}
if (module === require.main) {
runSample().catch(console.error);
}
module.exports = runSample;
Returning error with message: Error: Precondition check failed.
After searching the dark web for eternity, I found a link to a github issue that described how to authenticate as a service using JWT auth.
This is a working version of what I was trying to accomplish:
const path = require('path');
const {google} = require('googleapis');
async getMessageList(userId, qty) {
const JWT = google.auth.JWT;
const authClient = new JWT({
keyFile: path.resolve(__dirname, 'google-key.json'),
scopes: ['https://www.googleapis.com/auth/gmail.readonly'],
subject: 'admin#example.com' // google admin email address to impersonate
});
await authClient.authorize(); // once authorized, can do whatever you want
const gmail = google.gmail({
auth: authClient,
version: 'v1'
});
const response = await gmail.users.messages.list({
includeSpamTrash: false,
maxResults: qty,
q: "",
userId: userId
});
// the data object includes a "messages" array of message data
return response.data;
}
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',
});
}
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.
Goal
Use googleapis with Firebase Cloud Functions to get a list of all users in my G Suite domain.
Question
How do I Instantiate an Admin SDK Directory service object. I do not see a NodeJS example, and I'm not clear how to setup and make the request with googleapis.
Context
This code runs from Firebase Cloud Functions, and it seems to authenticate okay. Now, how do I setup the service object at //TODO in the following code:
// Firebase Admin SDK
const functions = require('firebase-functions')
const admin = require('firebase-admin')
admin.initializeApp(functions.config().firebase)
// Google APIs
const googleapis = require('googleapis')
const drive = googleapis.drive('v3')
const gsuiteAdmin = googleapis.admin('directory_v1')
// Service Account Key - JSON
let privatekey = require("./privatekey.json")
let jwtClient = new googleapis.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
['https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/admin.directory.user'])
// Firebase Cloud Functions - REST
exports.authorize = functions.https.onRequest((request, response) => {
//authenticate request
jwtClient.authorize(function (err, tokens) {
if (err) {
console.log(err)
return
} else {
console.log("Successfully connected!")
}
// TODO
// USE SERVICE OBJECT HERE??
// WHAT DOES IT LOOK LIKE?
response.send("Successfully connected!")
})
})
Order of Operations:
Create Service Account Credentials in Google Cloud Console
Add Domain-Wide Delegation to the Service Account
Authorize the API in G Suite - Security - Advanced
Go back to the Service Account and Download the .json key file
I downloaded the .json key file too soon, e.g., before authorizing the APIs in G Suite. The order, Setting up the Service Account with DwD and then authorization the API in G Suite API and then downloading the .json key file is important.
The Example
// Firebase Admin SDK
const functions = require('firebase-functions')
const admin = require('firebase-admin')
admin.initializeApp(functions.config().firebase)
// Google APIs
const googleapis = require('googleapis')
const drive = googleapis.drive('v3')
const directory = googleapis.admin('directory_v1')
// Service Account Key - JSON
let privatekey = require("./privatekey.json")
let impersonator = 'example#example.com'
let jwtClient = new googleapis.auth.JWT(
privatekey.client_email,
null, // not using path option
privatekey.private_key,
['https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.user.readonly'],
impersonator
)
// Firebase Cloud Functions - REST
exports.getUsers = functions.https.onRequest((request, response) => {
//authenticate request
jwtClient.authorize(function (err, tokens) {
if (err) {
console.log(err)
return
} else {
console.log("Successfully connected!")
}
//Google Drive API
directory.users.list ({
auth: jwtClient,
domain: 'example.com',
maxResults: 10,
orderBy: 'email',
viewType: 'domain_public'
}, function(err, res) {
if (err) {
console.log('The API returned an error: ' + err)
return;
}
var users = res.users;
if (users.length == 0) {
console.log('No users in the domain.');
} else {
console.log('Users:');
for (var i = 0; i < users.length; i++) {
var user = users[i];
console.log('%s (%s)', user.primaryEmail, user.name.fullName)
}
response.send(users)
}
})
})
})
UPDATE
The example above is not secure. A Cloud Function, especially with G Suite Domain-wide Delegation, should not respond to http requests unless they come from your application. See in this example that the Cloud Function uses admin.auth().verifyIdToken(idToken)... to validate that the request is authenticated by Firebase.
If you don't properly handle your G Suite DwD Cloud Function, you risk exposing your G Suite API to the public.