Getting Error: unauthorized_client when trying to authorize script - node.js

I've created service account with domain wide delegation and its scopes (in Admin console and Developer console) as described in documentation. I've been trying this for a week now and I am stuck. This is my code:
const google = require('googleapis');
const gmail = google.gmail('v1');
const directory = google.admin('directory_v1');
const scopes = [
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/admin.directory.user.readonly'
];
const key = require('./service_key.json');
var authClient = new google.auth.JWT(
key.client_email,
key,
key.private_key,
scopes,
"kruno#example.com"
);
authClient.authorize(function(err, tokens){
if (err) {
console.log(err);
return;
}
// API call methods here...
});
I get this error:
Error: unauthorized_client
I am unable to understand:
Is this proper technique for calling Google API methods from server-side scripts without any user interaction? (under domain only)
How do service account and actual user account communicate this way?
I heard about callback URI, am I missing it?

I think you are missing the final step which is giving access to your application in the control panel of your domain.
You can follow doc properly to activate it with your application
https://developers.google.com/+/domains/authentication/delegation
Also you can start with your first call step here
https://developers.google.com/adwords/api/docs/guides/first-api-call

Related

How do I call Google Analytics Admin API (for GA4) using an OAuth2 client in node.js?

I've noticed that all the node.js code samples for Google Analytics Admin and Google Analytics Data assume a service account and either a JSON file or a GOOGLE_APPLICATION_CREDENTIALS environment variable.
e.g.
const analyticsAdmin = require('#google-analytics/admin');
async function main() {
// Instantiates a client using default credentials.
// TODO(developer): uncomment and use the following line in order to
// manually set the path to the service account JSON file instead of
// using the value from the GOOGLE_APPLICATION_CREDENTIALS environment
// variable.
// const analyticsAdminClient = new analyticsAdmin.AnalyticsAdminServiceClient(
// {keyFilename: "your_key_json_file_path"});
const analyticsAdminClient = new analyticsAdmin.AnalyticsAdminServiceClient();
const [accounts] = await analyticsAdminClient.listAccounts();
console.log('Accounts:');
accounts.forEach(account => {
console.log(account);
});
}
I am building a service which allows users to use their own account to access their own data, so using a service account is not appropriate.
I initially thought I might be able to use the google-api-node-client -- Auth would be handled by building a URL to redirect and do the oauth dance...
Using google-api-nodejs-client:
const {google} = require('googleapis');
const oauth2Client = new google.auth.OAuth2(
YOUR_CLIENT_ID,
YOUR_CLIENT_SECRET,
YOUR_REDIRECT_URL
);
// generate a url that asks permissions for Google Analytics scopes
const scopes = [
"https://www.googleapis.com/auth/analytics", // View and manage your Google Analytics data
"https://www.googleapis.com/auth/analytics.readonly", // View your Google Analytics data
];
const url = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: scopes
});
// redirect to `url` in a popup for the oauth dance
After auth, Google redirects to GET /oauthcallback?code={authorizationCode}, so we collect the code and get the token to perform subsequent OAuth2 enabled calls:
// This will provide an object with the access_token and refresh_token.
// Save these somewhere safe so they can be used at a later time.
const {tokens} = await oauth2Client.getToken(code)
oauth2Client.setCredentials(tokens);
// of course we need to handle the refresh token too
This all works fine, but is it possible to plug the OAuth2 client from the google-api-node-client code into the google-analytics-admin code?
👉 It looks like I need to somehow call analyticsAdmin.AnalyticsAdminServiceClient() with the access token I've already retrieved - but how?
The simple answer here is don't bother with the Node.js libraries for Google Analytics Admin & Google Analytics Data.
Cut out the middleman and build a very simple wrapper yourself which queries the REST APIs directly. Then you will have visibility on the whole of the process, and any errors made will be your own.
Provided you handle the refresh token correctly, this is likely all you need:
const getResponse = async (url, accessToken, options = {}) => {
const response = await fetch(url, {
...options,
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return response;
};
I use Python but the method could be similar. You should create a Credentials object based on the obtained token:
credentials = google.auth.credentials.Credentials(token=YOUR_TOKEN)
Then use it to create the client:
from google.analytics.admin import AnalyticsAdminServiceClient
client = AnalyticsAdminServiceClient(credentials=credentials)
client.list_account_summaries()

How to use googleapis google.auth.GoogleAuth() for google API service account in Twilio serverless function?

How to use googleapis google.auth.GoogleAuth() for google API service account in Twilio serverless function, since there is no FS path to provide as a keyFile value?
Based on the example here ( https://www.section.io/engineering-education/google-sheets-api-in-nodejs/ ) and here ( Google api node.js client documentation ) my code is based on the example here ( Receive an inbound SMS ) and looks like...
const {google} = require('googleapis')
const fs = require('fs')
exports.handler = async function(context, event, callback) {
const twiml = new Twilio.twiml.MessagingResponse()
// console.log(Runtime.getAssets()["/gservicecreds.private.json"].path)
console.log('Opening google API creds for examination...')
const creds = JSON.parse(
fs.readFileSync(Runtime.getAssets()["/gservicecreds.private.json"].path, "utf8")
)
console.log(creds)
// connect to google sheet
console.log("Getting googleapis connection...")
const auth = new google.auth.GoogleAuth({
keyFile: Runtime.getAssets()["/gservicecreds.private.json"].path,
scopes: "https://www.googleapis.com/auth/spreadsheets",
})
const authClientObj = await auth.getClient()
const sheets = google.sheets({version: 'v4', auth: authClientObj})
const spreadsheetId = "myspreadsheetID"
console.log("Processing message...")
if (String(event.Body).trim().toLowerCase() == 'KEYWORD') {
console.log('DO SOMETHING...')
try {
// see https://developers.google.com/sheets/api/guides/values#reading_a_single_range
let response = await sheets.spreadsheets.values.get({
spreadsheetId: spreadsheetId,
range: "'My Sheet'!B2B1000"
})
console.log("Got data...")
console.log(response)
console.log(response.result)
console.log(response.result.values)
} catch (error) {
console.log('An error occurred...')
console.log(error)
console.log(error.response)
console.log(error.errors)
}
}
// Return the TwiML as the second argument to `callback`
// This will render the response as XML in reply to the webhook request
return callback(null, twiml)
...where the Asset referenced in the code is for a JSON generated from creating a key pair for a Google APIs Service Account and manually copy/pasting the JSON data as an Asset in the serverless function editor web UI.
I see error messages like...
An error occurred...
{ response: '[Object]', config: '[Object]', code: 403, errors: '[Object]' }
{ config: '[Object]', data: '[Object]', headers: '[Object]', status: 403, statusText: 'Forbidden', request: '[Object]' }
[ { message: 'The caller does not have permission', domain: 'global', reason: 'forbidden' } ]
I am assuming that this is due to the keyFile not being read in right at the auth const declaration (IDK how to do it since all the example I see assume a local filepath as the value, but IDK how to do have the function access that file for a serverless function (my attempt in the code block is really just a shot in the dark)).
FYI, I can see that the service account has an Editor role in the google APIs console (though I notice the "Resources this service account can access" has the error
"Could not fund an ancestor of the selected project where you have access to view a policy report on at least one ancestor"
(I really have no idea what that means or implies at all, very new to this)). Eg...
Can anyone help with what could be going wrong here?
(BTW if there is something really dumb/obvious that I am missing (eg. a typo) just LMK in a comment so can delete this post (as it would then not serve any future value of others))
The caller does not have permission', domain: 'global', reason: 'forbidden
This actually means that the currently authenticated user (the service account) does ot have access to do what you are asking it to do.
You are trying to access a spread sheet.
Is this sheet on the service accounts google drive account? If not did you share the sheet with the service account?
The service account is just like any other user if it doesn't have access to something it cant access it. Go to the google drive web application and share the sheet with the service account like you would share it with any other user just use the service account email address i think its called client id its the one with an # in it.
delegate to user on your domain
If you set up delegation properly then you can have the service account act as a user on your domain that does have access to the file.
delegated_credentials = credentials.with_subject('userWithAccess#YourDomain.org')

Google Sign-In idToken with createSessionCookie causing error - there is no user record corresponding to the provided identifier

Stack:
Google Sign-in (Vanilla JS - client side),
Firebase Functions (ExpressJS)
Client-Side:
My Firebase function express app uses vanilla javascript on the client side. To authenticate I am making use of Firebase's Google SignIn feature client-side javascript web apps, found here.
// Firebase setup
var firebaseConfig = {
apiKey: "AIza...",
authDomain: "....firebaseapp.com",
databaseURL: "https://...-default-rtdb.firebaseio.com",
...
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE);
function postIdTokenToSessionLogin(idToken, csrfToken) {
return axios({
url: "/user/sessionLogin", < ----- endpoint code portion found below
method: "POST",
data: {
idToken: idToken,
csrfToken: csrfToken,
},
});
}
// ...
// On sign-in click
var provider = new firebase.auth.GoogleAuthProvider();
firebase.auth()
.signInWithPopup(provider)
.then(async value => {
const idToken = value.credential.idToken;
const csrfToken = getCookie('_csrf');
return postIdTokenToSessionLogin(idToken, csrfToken);
}).then(value => {
window.location.assign("/user/dashboard")
}).catch((error) => {
alert(error.message);
});
Note I am using value.credential.idToken (most sources imply to use this, but haven't found an example saying use this specifically)
Directly after calling signInWithPopup, a new account is created in my Firebase Console Authentication matching the gmail account that was just signed in.
Server-side:
Once I authenticate, I create an axios request passing in the {user}.credential.idToken and following the server-side setup here (ignoring the CSRF - this just doesn't want to work).
In creating the session, I use the following code in my firebase functions express app, the endpoint which is router.post('/sessionLogin', (req, res) => (part of /user route prefix):
// Set session expiration to 5 days.
const expiresIn = 60 * 60 * 24 * 5 * 1000;
const idToken = req.body.idToken.toString(); // eyJhbGciOiJSUzI1NiIsImt...[936]
admin
.auth()
.createSessionCookie(idToken, {expiresIn}) < ----------- Problem line
.then((sessionCookie) => {
// Set cookie policy for session cookie.
const options = {maxAge: expiresIn, httpOnly: true, secure: true};
res.cookie('session', sessionCookie, options);
res.end(JSON.stringify({status: 'success'}));
}).catch((error) => {
console.error(error);
res.status(401).send('UNAUTHORIZED REQUEST!');
});
On the createSessionCookie call, I get the following error & stack trace:
Error: There is no user record corresponding to the provided identifier.
at FirebaseAuthError.FirebaseError [as constructor] (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:44:28)
at FirebaseAuthError.PrefixedFirebaseError [as constructor] (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:90:28)
at new FirebaseAuthError (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:149:16)
at Function.FirebaseAuthError.fromServerError (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:188:16)
at C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\auth\auth-api-request.js:1570:49
at processTicksAndRejections (internal/process/task_queues.js:93:5)
This is part of the sign-in flow with a existing Gmail account.
What is causing this?
After many hours of searching, Googling - I have seen the light.
For some additional context, this error featured heavily in my struggle "Firebase ID token has invalid signature." - I will get to that in a second.
Further, another issue I also faced was using a local auth emulator for web client-side (javascript), see this for setup.
TL;DR to solve the immediate problem
Client-side remained largely the same, however the documentation provided by Firebase was inaccurate/misleading - thanks to this post, I found the solution. Thus, it follows...
Which is the ID Token? (Client-side):
The examples from here (to allow signInWithPopup), the response (if successful) results in
...
.signInWithPopup(provider)
.then((result) => {
/** #type {firebase.auth.OAuthCredential} */
var credential = result.credential;
// This gives you a Google Access Token. You can use it to access the Google API.
var token = credential.accessToken;
// The signed-in user info.
var user = result.user;
// ...
})
Looking for an idToken, I found one using result.credential.idToken but no where on the internet on if this was infact the correct token to use.
I ran into this error using the provided idToken above:
Firebase ID token has incorrect "aud" (audience) claim. Expected
"[insert your **projectId**]" but got
"59895519979-2l78aklb7cdqlth0eob751mdm67kt301.apps.googleusercontent.com".
Make sure the ID token comes from the same Firebase project as the
service account used to authenticate this SDK.
Trying other tokens like result.credential.accessToken responded with various verification errors - what to do?
Mention earlier, this solution on Github suggested to use firebase.auth().currentUser.getIdToken() AFTER you have signed in. An example (building on my previous code) is to do the following:
...
.signInWithPopup(provider)
.then((result) => {
// current user is now valid and not null
firebase.auth().currentUser.getIdToken().then(idToken => {
// send this ID token to your server
const csrfToken = getCookie('_csrf');
return postIdTokenToSessionLogin(idToken, csrfToken);
})
})
At this point, you can verify your token and createSessionCookies to your heart's desire.
BUT, a secondary issue I unknowingly created for myself using the Authentication Emulator.
To setup for client-side use:
var auth = firebase.auth();
auth.useEmulator("http://localhost:9099");
To setup for hosting your firebase functions app (assuming you are using this with e.g. nodejs + express, see this for setup, ask in comments, can provide more details if needed)
Using Authentication Emulator caused the following errors AFTER using the above mentioned "fix". Thus, DO NOT RUN the local authentication emulator (with Google sign-in of a valid Google account) as you will consistently get.
Firebase ID token has invalid signature. See
https://firebase.google.com/docs/auth/admin/verify-id-tokens for
details on how to retrieve an ID token
You can use all your local emulators, but (so far in my experience) you will need to use an online authenticator.

Google Calendar API and Service Account permission error

I'm trying to integrate the Google Calendar API in my app.
So far i've managed to do this:
Created a new project on Cloud Platform
Enabled Calendar API
Added a new service account with role: Owner
Generated jwt.json
Granted domain-wide for that service account
Shared a calendar with that service account (modify rights)
Enabled in the GSuite the option for everyone out of the organisation to modify the events
Now, my code on node.js looks like this:
const { JWT } = require('google-auth-library');
const client = new JWT(
keys.client_email,
null,
keys.private_key,
['https://www.googleapis.com/auth/calendar']
);
const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`;
const rest = await client.request({url});
console.log(rest);
The error I get is:
Sending 500 ("Server Error") response:
Error: Insufficient Permission
Anyone has any ideea? This gets frustrating.
How about this modification?
I think that in your script, the endpoint and/or scope might be not correct.
Pattern 1:
In this pattern, your endpoint of https://dns.googleapis.com/dns/v1/projects/${keys.project_id} is used.
Modified script:
const { JWT } = require("google-auth-library");
const keys = require("###"); // Please set the filename of credential file of the service account.
async function main() {
const calendarId = "ip15lduoirvpitbgc4ppm777ag#group.calendar.google.com";
const client = new JWT(keys.client_email, null, keys.private_key, [
'https://www.googleapis.com/auth/cloud-platform' // <--- Modified
]);
const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`;
const res = await client.request({ url });
console.log(res.data);
}
main().catch(console.error);
In this case, it is required to enable Cloud DNS API at API console. And it is required to pay. Please be careful with this.
I thought that the reason of your error message of Insufficient Permission might be this.
Pattern 2:
In this pattern, as a sample situation, the event list is retrieved from the calendar shared with the service account. If the calendar can be used with the service account, the event list is returned. By this, I think that you can confirm whether the script works.
Modified script:
const { JWT } = require("google-auth-library");
const keys = require("###"); // Please set the filename of credential file of the service account.
async function main() {
const calendarId = "###"; // Please set the calendar ID.
const client = new JWT(keys.client_email, null, keys.private_key, [
"https://www.googleapis.com/auth/calendar"
]);
const url = `https://www.googleapis.com/calendar/v3/calendars/${calendarId}/events`; // <--- Modified
const res = await client.request({ url });
console.log(res.data);
}
main().catch(console.error);
Note:
This modified script supposes that you are using google-auth-library-nodejs of the latest version.
Reference:
JSON Web Tokens in google-auth-library-nodejs

Impersonate user with google calendar API oauth2 service account (node.js)

I am trying to use a server for server authentication through the google calendar API. In the documentation they mention you can impersonate a user. I have added owner permissions to the account I want to impersonate and made sure the domain-wide delegation is enabled. From what I have read, the 'user#example' should specify the impersonator, but it does not work. I have all the functions of creating events etc working, but I can't get it to be from an email other than the randomly generated google one.
Here's my code:
var google = require('googleapis');
var calendar = google.calendar('v3');
var scopes = ['https://www.googleapis.com/auth/calendar'];
var key = require ('./xxx.json'); // private json
console.log("Calendar Service connected");
var jwtClient = new google.auth.JWT(
key.client_email,
null,
key.private_key,
scopes,
'user#example.com'
);
jwtClient.authorize(function(err, token) {
if(err) {
//console.log(err);
}
console.log('token',token);
//listCalendars(jwtClient);
});
module.exports = {};
I got it to work after:
Enabling domain-wide delegation
Adding user to the service account as the owner
Most importantly: Going onto google admin and giving api access to the service account

Resources