Google Chat API (G Suite): Request contains an invalid argument (Node JS) - google-api

const { google } = require('googleapis')
const privatekey = require('./a.json')
const scopes = ['https://www.googleapis.com/auth/chat.bot'];
const a = async () => {
try {
const jwtClient = new google.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
scopes,
'adminEmail#org.com'
);
await jwtClient.authorize();
const chat = google.chat({ version: 'v1', auth: jwtClient });
const res = await chat.spaces.messages.get({name:'spaces/XXX/messages/XX.XX'})
console.log(res)
}
catch(e) {
console.log(e)
}
}
a()
Error: Request contains an invalid argument
I am unable to find the invalid argument
Thanks in advance

Many Hangouts API request require the usage of a service account
You can consult in the documentation which type of requests are affected
For the requests requiring the usage of a service account - it is meant that the service account acts on its own behalf
Impersonation means that the service account acts on behalf of another user
Thus, impersonation is not allowed for requests that need to be carried out by a service account
Also mind that https://www.googleapis.com/auth/chat.bot is the scope to be used by the service account without domain-wide delegation
Users or impersonated service accounts need to use the scope https://www.googleapis.com/auth/chat instead - see also here
Last but not least, chat bots are not allowed to delete messages of other users

Related

How to use google reseller api using service account

I want to access Google reseller api to get customers and subscriptions using google service account key but not able to do it. Below is my code snippet:
async function runSample() {
const auth = new google.auth.GoogleAuth({
keyFile: "../server/credentials/serviceAccountKey.json",
scopes: ["https://www.googleapis.com/auth/apps.order",
"https://www.googleapis.com/auth/apps.order.readonly"
],
});
// Acquire an auth client, and bind it to all future calls
const authClient = await auth.getClient();
google.options({ auth: authClient });
// Do the magic
const res = await reseller.subscriptions.list();
console.log(res.data);
}
runSample().catch(console.error);
Here I want to get list of the subscription from google reseller console. I referenced above code from google documentation. Here I am getting the error 'Authenticated user is not authorized to perform this action.' and reason given is 'Insufficient permissions'.
errors: [
{
message: 'Authenticated user is not authorized to perform this action.',
domain: 'global',
reason: 'insufficientPermissions'
}
]
If I try to access cloud channel service api I can using the same service account key but it is giving error for reseller api.
I have given service account the owner, cloud workstation admin and service account admin role access.
I have also added scopes in domain wide delegation(dwd).
What else permission do I need?
In order to use a service account it must first be configured though your google workspace account Create a service account
You must also denote in your code the name of the user who your service account has been configured to impersonate.
const auth = new google.auth.GoogleAuth({
keyFile: "../server/credentials/serviceAccountKey.json",
clientOptions: {
subject: 'user#yourdomain.com'
},
scopes: ["https://www.googleapis.com/auth/apps.order"
],
});

Pass variables between different server-less functions

I am building an application using the Twitter API and Netlify (aws lambda functions)
This API requires these steps:
When the user goes to my /auth function, a link to the Twitter authentication is created
Once the user clicks that link, he is redirected to Twitter where a pop-up asks to allow my app to connect.
Once the user approves, he is redirected to my /auth function again but this time the authCode is set to a number rather than being undefined. This authCode is used to instantiate the twitter client class and authorize it.
A new instance of the Twitter client is created and authorized. This instance allows to query the tweets
1, 2 and 3 works. However, the authorized instance only lives inside the /auth function. How can I pass it to different functions without losing its instantiation?
How can I pass this instance to different server-less functions?
client = new Client(OAuthClient) this is what I want to pass around.
I tried with a Middleware with little success. It seems the twitter client class gets re-instantiated (so without authorization) for every server-less function
https://playful-salmiakki-67d50e.netlify.app/.netlify/functions/auth
import Client from 'twitter-api-sdk';
let client: Client;
const auth = async (event, context, callback) => {
const authCode = event.queryStringParameters ? event.queryStringParameters.code : undefined;
const authUrl = OAuthClient.generateAuthURL({
state: 'STATE',
code_challenge: 'challenge',
});
console.log('HERE LINK:');
console.log(authUrl);
if (authCode) {
await OAuthClient.requestAccessToken(authCode as string);
client = new Client(OAuthClient); <-- THIS IS WHAT I WANT TO PASS TO DIFFERENT FUNCTIONS
}
return {
statusCode: 200,
body: JSON.stringify({ message: 'Auth, go to the url displayed terminal'}),
myClient: client
};
};
exports.handler = middy().use(myMiddleware()).handler(auth);

Accessing Google API from aws lambda : Invalid OAuth scope

I am still struggling with Google's terminology of apis and services but my goal is to have automated functions via aws lambda which act on a G Suite Account (domain?) or more specific on users of this domain.
For now I just want to list all users of that domain. I run this code locally for testing.
What I have done:
I created a service account
I downloaded the json key file which contains the private key, private key id and so on
I enabled G Suite Domain-wide Delegation.
I delegated domain-wide authority to the service account from the GSuite Account
I added the following scopes for the client in the GSuite Admin Console:
https://www.googleapis.com/auth/admin.directory.group
https://www.googleapis.com/auth/admin.directory.user
This is the implementation:
const { google } = require("googleapis");
const auth = new google.auth.GoogleAuth({
keyFile: "credentials.json",
scopes:
"https://www.googleapis.com/auth/drive.readonly,https://www.googleapis.com/admin/directory/v1, https://www.googleapis.com/auth/admin.directory.group, https://www.googleapis.com/auth/admin.directory.user",
});
const service = google.admin({ version: "directory_v1", auth });
service.users.list(
{
domain: "my.domain.com",
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.");
}
}
);
I am not sure why I have to add the scopes in the GoogleAuth object but I took this from the google documentation.
When I run this I get the following error:
The API returned an error: invalid_scope: Invalid OAuth scope or ID token audience provided.
The Directory API can only be used by admins
A Service account is not an admin
If the service account shall act on behalf on the admin, you need to
enable G Suite Domain-wide Delegation (as you already did)
impersonate the service account as the admin by setting the user to be impersonated
In general, when you are using a service account you need to build the authentication flow, as explained in the documentation, that is you need to create JSON Web Token (JWT) specifying the user to impersonate.
A sample code snippet for Javascript:
const jwtClient = new google.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
scopes,
user // User who will be impersonated (needs to be an admin)
);
await jwtClient.authorize();
return jwtClient;

Invalid signature while validating Azure ad access token, but id token works

I am getting invalid signature while using jwt.io to validate my azure ad access token. My id token, however, validates just fine!
I have seen and tried the solutions suggested in
Invalid signature while validating Azure ad access token
and
https://nicksnettravels.builttoroam.com/post/2017/01/24/Verifying-Azure-Active-Directory-JWT-Tokens.aspx
but neither works for my access token.
The access and Id token is generated via Adal.js:
var endpoints = {
"https://graph.windows.net": "https://graph.windows.net"
};
var configOptions = {
tenant: "<ad>.onmicrosoft.com", // Optional by default, it sends common
clientId: "<app ID from azure portal>",
postLogoutRedirectUri: window.location.origin,
endpoints: endpoints,
}
window.authContext = new AuthenticationContext(configOptions);
Why can I validate my ID token, but not my access token?
Please refer to thread : https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/609
but if look at the Jwt.Header you will see a 'nonce'. This means you need special processing. Normal processing will fail.
So if nonce includes in access token , validate signature with JWT.io or JwtSecurityToken won't success .
If anyone else has invalid signature errors, you should check this comment : https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/521#issuecomment-577400515
Solved the issue for my configuration.
Essentially, if you are getting access tokens to access your own resource server and not the Graph API, your scopes parameter should be [CLIENT_ID]/.default (and if you are using the access token to access the Graph API, you don't need to validate the token yourself)
Thanks to Nan Yu I managed to get token that can be validated by any public jwt validator like jwt.io
(couldn't put my comment in the comments section under Nan Yu's answer because its too long).
So as I understand the point from the discussion mentioned by Nan Yu that by default Azure AD generates tokens for Microsoft Graph and these tokens use special signing mechanism so that it is not possible to validate signature using public validators (except jwt.ms Microsoft's validator which most probably knows what mysterious special handling means :) ).
To get access token not for Microsoft Graph that can be validated using public validators I had to:
Remove any Microsoft Graph related scopes (by default I had only one scope configured User.Read so removed it in appConfig > API permissions)
create a custom scope for your application (appConfig > Expose an API > Add scope ...) this scope will look like api://{application-id}/scope-name
add just created scope in the application API permissions (appConfig > API permissions > Add api permission > My APIs > select your application > Delegated Permissions > Check your scope > Add permission)
then use this scope in your openid client scopes, in my case I have: openid offline_access {application-id}/scope-name
Note that in the openid client config newly created scope is used without api:// prefix (offline_access I have to enable refresh_token can be ignored if refresh token mechanism is not used)
Well thanks to #Antoine I fix my code. Here I will let my personal vue.js plugin that is working for everybody else reference:
import { PublicClientApplication } from '#azure/msal-browser'
import { Notify } from 'quasar'
export class MsalService {
_msal = null
_store = null
_loginRequest = null
constructor (appConfig, store) {
this._store = store
this._msal = new PublicClientApplication(
{
auth: {
clientId: appConfig.auth.clientId,
authority: appConfig.auth.authority
},
cache: {
cacheLocation: 'localStorage'
}
})
this._loginRequest = {
scopes: [`${appConfig.auth.clientId}/.default`]
}
}
async handleResponse (response) {
await this._store.dispatch('auth/setResponse', response)
const accounts = this._msal.getAllAccounts()
await this._store.dispatch('auth/setAccounts', accounts)
if (accounts.length > 0) {
this._msal.setActiveAccount(accounts[0])
this._msal.acquireTokenSilent(this._loginRequest).then(async (accessTokenResponse) => {
// Acquire token silent success
// Call API with token
// let accessToken = accessTokenResponse.accessToken;
await this._store.dispatch('auth/setResponse', accessTokenResponse)
}).catch((error) => {
Notify.create({
message: JSON.stringify(error),
color: 'red'
})
// Acquire token silent failure, and send an interactive request
if (error.errorMessage.indexOf('interaction_required') !== -1) {
this._msal.acquireTokenPopup(this._loginRequest).then(async (accessTokenResponse) => {
// Acquire token interactive success
await this._store.dispatch('auth/setResponse', accessTokenResponse)
}).catch((error) => {
// Acquire token interactive failure
Notify.create({
message: JSON.stringify(error),
color: 'red'
})
})
}
})
}
}
async login () {
// this._msal.handleRedirectPromise().then((res) => this.handleResponse(res))
// await this._msal.loginRedirect(this._loginRequest)
await this._msal.loginPopup(this._loginRequest).then((resp) => this.handleResponse(resp))
}
async logout () {
await this._store.dispatch('auth/setAccounts', [])
await this._msal.logout()
}
}
// "async" is optional;
// more info on params: https://quasar.dev/quasar-cli/boot-files
export default ({
app,
store,
Vue
}) => {
const msalInstance = new MsalService(
app.appConfig, store
)
Vue.prototype.$msal = msalInstance
app.msal = msalInstance
}
PD: using quasar framework
If you are using msal.js library with react, add this to your auth configuration.
scopes: [`${clientId}/.default`]
Editing scopes fixed issue for me

Restrict Login Email with Google OAuth2.0 to Specific Domain Name

I can't seem to find any documentation on how to restrict the login to my web application (which uses OAuth2.0 and Google APIs) to only accept authentication requests from users with an email on a specific domain name or set of domain names. I would like to whitelist as opposed to blacklist.
Does anyone have suggestions on how to do this, documentation on the officially accepted method of doing so, or an easy, secure work around?
For the record, I do not know any info about the user until they attempt to log in through Google's OAuth authentication. All I receive back is the basic user info and email.
So I've got an answer for you. In the OAuth request you can add hd=example.com and it will restrict authentication to users from that domain (I don't know if you can do multiple domains). You can find hd parameter documented here
I'm using the Google API libraries from here: http://code.google.com/p/google-api-php-client/wiki/OAuth2 so I had to manually edit the /auth/apiOAuth2.php file to this:
public function createAuthUrl($scope) {
$params = array(
'response_type=code',
'redirect_uri=' . urlencode($this->redirectUri),
'client_id=' . urlencode($this->clientId),
'scope=' . urlencode($scope),
'access_type=' . urlencode($this->accessType),
'approval_prompt=' . urlencode($this->approvalPrompt),
'hd=example.com'
);
if (isset($this->state)) {
$params[] = 'state=' . urlencode($this->state);
}
$params = implode('&', $params);
return self::OAUTH2_AUTH_URL . "?$params";
}
I'm still working on this app and found this, which may be the more correct answer to this question. https://developers.google.com/google-apps/profiles/
Client Side:
Using the auth2 init function, you can pass the hosted_domain parameter to restrict the accounts listed on the signin popup to those matching your hosted_domain. You can see this in the documentation here: https://developers.google.com/identity/sign-in/web/reference
Server Side:
Even with a restricted client-side list you will need to verify that the id_token matches the hosted domain you specified. For some implementations this means checking the hd attribute you receive from Google after verifying the token.
Full Stack Example:
Web Code:
gapi.load('auth2', function () {
// init auth2 with your hosted_domain
// only matching accounts will show up in the list or be accepted
var auth2 = gapi.auth2.init({
client_id: "your-client-id.apps.googleusercontent.com",
hosted_domain: 'your-special-domain.example'
});
// setup your signin button
auth2.attachClickHandler(yourButtonElement, {});
// when the current user changes
auth2.currentUser.listen(function (user) {
// if the user is signed in
if (user && user.isSignedIn()) {
// validate the token on your server,
// your server will need to double check that the
// `hd` matches your specified `hosted_domain`;
validateTokenOnYourServer(user.getAuthResponse().id_token)
.then(function () {
console.log('yay');
})
.catch(function (err) {
auth2.then(function() { auth2.signOut(); });
});
}
});
});
Server Code (using googles Node.js library):
If you're not using Node.js you can view other examples here: https://developers.google.com/identity/sign-in/web/backend-auth
const GoogleAuth = require('google-auth-library');
const Auth = new GoogleAuth();
const authData = JSON.parse(fs.readFileSync(your_auth_creds_json_file));
const oauth = new Auth.OAuth2(authData.web.client_id, authData.web.client_secret);
const acceptableISSs = new Set(
['accounts.google.com', 'https://accounts.google.com']
);
const validateToken = (token) => {
return new Promise((resolve, reject) => {
if (!token) {
reject();
}
oauth.verifyIdToken(token, null, (err, ticket) => {
if (err) {
return reject(err);
}
const payload = ticket.getPayload();
const tokenIsOK = payload &&
payload.aud === authData.web.client_id &&
new Date(payload.exp * 1000) > new Date() &&
acceptableISSs.has(payload.iss) &&
payload.hd === 'your-special-domain.example';
return tokenIsOK ? resolve() : reject();
});
});
};
When defining your provider, pass in a hash at the end with the 'hd' parameter. You can read up on that here. https://developers.google.com/accounts/docs/OpenIDConnect#hd-param
E.g., for config/initializers/devise.rb
config.omniauth :google_oauth2, 'identifier', 'key', {hd: 'yourdomain.com'}
Here's what I did using passport in node.js. profile is the user attempting to log in.
//passed, stringified email login
var emailString = String(profile.emails[0].value);
//the domain you want to whitelist
var yourDomain = '#google.com';
//check the x amount of characters including and after # symbol of passed user login.
//This means '#google.com' must be the final set of characters in the attempted login
var domain = emailString.substr(emailString.length - yourDomain.length);
//I send the user back to the login screen if domain does not match
if (domain != yourDomain)
return done(err);
Then just create logic to look for multiple domains instead of just one. I believe this method is secure because 1. the '#' symbol is not a valid character in the first or second part of an email address. I could not trick the function by creating an email address like mike#fake#google.com 2. In a traditional login system I could, but this email address could never exist in Google. If it's not a valid Google account, you can't login.
Since 2015 there has been a function in the library to set this without needing to edit the source of the library as in the workaround by aaron-bruce
Before generating the url just call setHostedDomain against your Google Client
$client->setHostedDomain("HOSTED DOMAIN")
For login with Google using Laravel Socialite
https://laravel.com/docs/8.x/socialite#optional-parameters
use Laravel\Socialite\Facades\Socialite;
return Socialite::driver('google')
->with(['hd' => 'pontomais.com.br'])
->redirect();

Resources