I'm trying to access some google groups from the google-api using nodeJS and a service account file. I can't find a code sample that works quite like this. I've put together some code that seems to be working, but I keep getting an error 400 - Bad Request from the API without any clues what's wrong.
Am I using the right classes? Can I use a service account here? Am I missing scopes or something?
My code:
import { Auth, google } from "googleapis";
const main = async () => {
const auth = new Auth.GoogleAuth({
keyFile: "/path-to-service-account-file.json",
scopes: "https://www.googleapis.com/auth/admin.directory.group.readonly",
});
const client = await auth.getClient();
// Obtain a new drive client, making sure you pass along the auth client
const admin = google.admin({ version: 'directory_v1', auth: client });
const groups = await admin.groups.list();
console.log(groups.data.groups);
}
main().then(() => process.exit(0)).catch(err => {
console.error(err);
process.exit(1);
});
The file is (I think) a standard service account file:
{
"type": "service_account",
"project_id": "groupmembers-*******",
"private_key_id": "*********",
"private_key": ...,
"client_email": "test-admin-nodejs#groupmembers-****.iam.gserviceaccount.com",
"client_id": "*****",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test-admin-nodejs%40groupmembers-******.iam.gserviceaccount.com"
}
When I run this code, I get the following response:
status: 400,
statusText: 'Bad Request',
request: {
responseURL: 'https://admin.googleapis.com/admin/directory/v1/groups'
}
What's interesting to me is the bearer token it generates for me. The error also prints out the request info:
config: {
url: 'https://admin.googleapis.com/admin/directory/v1/groups',
method: 'GET',
userAgentDirectives: [ [Object] ],
paramsSerializer: [Function (anonymous)],
headers: {
'x-goog-api-client': 'gdcl/5.1.0 gl-node/17.4.0 auth/7.14.1',
'Accept-Encoding': 'gzip',
'User-Agent': 'google-api-nodejs-client/5.1.0 (gzip)',
Authorization: 'Bearer ya29.c.b0AX***********************************************jzxc........................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................',
Accept: 'application/json'
},
I don't know if this is normal or relevant, but I've never seen a bearer token with a bunch of dots at the end.
A 400 error is not an authorization/authentication issue, but instead is a client request issue. According to the official documentation, there are some query parameters required for the api call. One of them is the customer query parameter:
The unique ID for the customer's Google Workspace account. In case of a multi-domain account, to fetch all groups for a customer, fill this field instead of domain. As an account administrator, you can also use the my_customer alias to represent your account's customerId. The customerId is also returned as part of the Users
Based on experience, it is implied you must use this value. Although the documentation should be better and it should explicitly say this is required. So, in summary, your request should look like this:
const groups = await admin.groups.list({
customer: "my_customer"
});
console.log(groups.data.groups);
Related
I have a issue, which I dont currently understand.
I have a list of different tenants 100+ and this list keeps growing.
For each of these tenants I want to automatically created an app registration that gives access to some specific api permissions.
The first time I log onto an azure portant (for each tenant) I have the following information:
UserName (Tenant Owner...)
UserPassword (Tenant Owner...)
Meaning that I have all the neccesary rights to be able to create an app registration.
What I am having trouble with is making this automatic based on a script.
When I manually do this I can see on the browser creates an App Registration using the graph API.
endpoint: https://graph.microsoft.com/v1.0/myorganization/applications
headers:
application/json
+
Bearer token
post body
{
"displayName": "TestAccount12345678",
"spa": {
"redirectUris": []
},
"publicClient": {
"redirectUris": []
},
"web": {
"redirectUris": []
},
"signInAudience": "AzureADMyOrg",
"requiredResourceAccess": [
{
"resourceAppId": "00000003-0000-0000-c000-000000000000",
"resourceAccess": [
{
"id": "e1fe6dd8-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"type": "Scope"
}
]
}
]
}
My Code
So far I have tried this:
libraries:
import * as msRestNodeAuth from '#azure/ms-rest-nodeauth';
import { ResourceManagementClient } from '#azure/arm-resources';
let creds = await msRestNodeAuth.loginWithUsernamePassword(config.ServiceAccount, config.ServicePassword);
const ressourceCli = new ResourceManagementClient(creds, config.SubScriptionID);
let exists = await ressourceCli.resourceGroups.checkExistence(config.ResGrp);
let accessToken = await creds.getToken();
console.log(accessToken);
let body = {
displayName: 'TestAppRegistration',
spa: { redirectUris: [] },
publicClient: { redirectUris: [] },
web: { redirectUris: [] },
signInAudience: 'AzureADMyOrg',
requiredResourceAccess: [{ resourceAppId: '00000003-0000-0000-c000-000000000000', resourceAccess: [{ id: 'e1fe6dd8-xxxx-xxxx-xxxx-xxxxxxxxxxxx', type: 'Scope' }] }],
};
let rest = await fetch('https://graph.microsoft.com/v1.0/myorganization/applications', {
method: 'POST',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken.accessToken}` },
});
When I run this code, I get an 401 error that the bearer token is not valid, as a quick note, the variable exist has access to azure, as it correctly tells me if a random ressource grp exists.
If I inspect the browser when creating the registration manually, and hardcode the bearer token from there, I have no issues running this code and creating the app registration.
I have also tried looking into using this library:
import { Client, ClientOptions } from '#microsoft/microsoft-graph-client';
But no matter how I turn it, the issues seems to be that to authenticate to the graph API I need an app registration... I need an app registration to create an app registration... and I am trying to do it based on my username and password.
On another note, I have managed to make this run on powershell: But I really dont want to run powershell to do this.
$connectionCustomer = Connect-AzAccount
$customerContext = Get-AzContext
Set-AzContext -Context $customerContext
AzureADApp = New-AzADApplication -DisplayName $createNewAppName
Is there anything I am missing here?
Thanks in advance!
Hello everybody and thanks for all the good feedback.
I have gone ahead and used a multi tenant app, which grants me the correct privilege's.
My purpose was to do some management, and access the graph api, and thats to this, it has been done.
Code snippets:
const msalConfig: msal.Configuration = {
auth: {
clientId: AdminClientID,
authority: `https://login.microsoftonline.com/${adalToken.tenantId}`,
clientSecret: 'secret',
},
};
let pca = new msal.PublicClientApplication(msalConfig);
// see https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows#device-code for alternative code flows
let tokenObj = await pca.acquireTokenByUsernamePassword({ scopes: [], password: config.CustomerPassword, username: config.CustomerAccount });
if (!tokenObj) {
return;
}
let action: IManager = new AzureAppRegistrationManager(tokenObj.accessToken);
await action.verifyAndCreate();
I started using OAuth2 server with oidc in node js. Github link
My goal is simple, to access https://myserver/me which is UserInfo endpoint.
While trying to learn how to use the server I also used this guide enter link description here
Where I found that I could create token by sending request to endpoint /token.
Into the configuration I added this code(full server code is below):
{
client_id: 'test_oauth_app',
client_secret: 'super_secret',
grant_types: ['client_credentials'],
redirect_uris: [],
response_types: [],
}
In postman I was able to get my the access_token by this request
POST /token
Headers:
Content-Type: application/x-www-form-urlencoded
Authorization: Basic dGVzdF9vYXV0aF9hcHA6c3VwZXJfc2VjcmV0
Body:
grant_type=client_credentials&scopes=api1
I get this as a response:
{
"access_token": "zdjmZo7_BQSIl4iK9IMcBbKffxGO-wQ3jLzzQXTlyws",
"expires_in": 600,
"token_type": "Bearer"
}
When I checked the token by /token/introspection I found out that the token equals to jti.
So I think it actually returns token_id and by that I cannot access /me endpoint.
Here is the whole sample of server that I use:
const { Provider } = require('oidc-provider');
const configuration = {
features: {
introspection: { enabled: true },
clientCredentials: { enabled: true },
userinfo: { enabled: true },
jwtUserinfo: { enabled: true },
},
formats: {
AccessToken: 'jwt',
},
clients: [{
client_id: 'test_oauth_app',
client_secret: 'super_secret',
grant_types: ['client_credentials'],
redirect_uris: [],
response_types: []
}],
scopes: ['api1']
};
const oidc = new Provider('http://localhost:3000', configuration);
oidc.proxy = true
// express/nodejs style application callback (req, res, next) for use with express apps, see /examples/express.js
oidc.callback
// koa application for use with koa apps, see /examples/koa.js
oidc.app
// or just expose a server standalone, see /examples/standalone.js
const server = oidc.listen(3000, () => {
console.log('oidc-provider listening on port 3000, check https://localhost:3000/.well-known/openid-configuration');
});
The proxy is set to true because I have https set up on apache redirecting to this server.
I tried to change response_types, but than it required redirect_uri which I do not want to have in my scenario.
Here is the request I am trying to post it like so:
POST /me
Headers:
Content-Type: application/json
Authorization: Bearer zdjmZo7_BQSIl4iK9IMcBbKffxGO-wQ3jLzzQXTlyws
The response:
{
"error": "invalid_token",
"error_description": "invalid token provided"
}
Did anyone have a similar problem? I found almost the same problem here
but with no solution, unfortunately.
In case someone encounters the same problem. I was able to solve it.
I did not have enough information and I did not know what client_credentials grant type does.
It actually does not authorize the user, but rather some app. So you have no info about the user, hence you cannot get data about the user through the userinfo endpoint.
So if you want to get info about the user, you probably want to use grant type authorization_code.
I found a page where a lot of things is written pretty clearly, so if you are starting with
OAuth server you might want to give this a try.
https://oauth2.thephpleague.com/authorization-server/auth-code-grant/
I am not sure why the following code is throwing google api key permission denied.
I have the api or service enabled both in firebase console and google console.
export async function createJobDynamicLink(job){
if(job.jobStatus !== 'approved' || (job.dynamicLink).length > 2){
console.log('Dynamic link already exist!');
return false;
}
console.log(dynamic_links);
console.log(dynamic_links_key);
// Firebase web api key logs just fine
const options = {
method: 'POST',
uri: `https://firebasedynamiclinks.googleapis.com/v1/shortLinks?key=${dynamic_links_key}`,
body: {
"longDynamicLink": makeDynamicLongLink(job)
},
json: true
};
return await requestpromise(options)
.then(function (parsedBody) {
console.log(parsedBody);
return parsedBody.shortLink;
})
.then((shortLink) => {
//post.shareUrl = shortLink;
console.log('short link: ' + shortLink);
//return event.data.ref.set(post);
return shortLink;
})
}
export async function makeDynamicLongLink(job) {
return buildUrl(`${dynamic_links}`, {
queryParams: {
link: `https://app.com/jobs/${slugify(job.jobTitle)}-${job.id}`,
apn: "com.app.appe",
ibi: "com.app.app",
dfl: "https://app.com",
st: job.jobTitle,
}
});
}
Is something wrong with the way I am doing the request using request-promise?
StatusCodeError: 403 - {
"error": {
"code": 403,
"message": "Requests from referer <empty> are blocked.",
"status": "PERMISSION_DENIED",
"details": [{
"#type": "type.googleapis.com/google.rpc.Help",
"links": [{
"description":"Google developer console API key",
"url": "https://console.developers.google.com/project/904573jjwj/apiui/credential"
}]
}]
}
}
Go to the Google API Credentials https://console.developers.google.com/apis/credentials and see if there is any restriction on your API Key you're using.
If it is restricted by HTTP referrers, then add your website domain to it and add the Referrer header like the above answer.
Although in your use case, None or IP address restriction is a better choice.
Because you are invoking your function from a node.js environment, the HTTP Header Referer isn't being set. When you create requests through a browser, the browser will automatically fill this field for you.
You can get a suitable referrer value using:
"https://" + process.env.GCLOUD_PROJECT + ".cloudfunctions.net/createJobDynamicLink"
// becomes "https://your-project-id.cloudfunctions.net/createJobDynamicLink"
This generated URL isn't callable, because it doesn't start with a region, but it means you now have a URL that can be used to identify that the call is coming from a Cloud Function.
To use it, add it to your request-promise options object.
const options = {
method: 'POST',
uri: `https://firebasedynamiclinks.googleapis.com/v1/shortLinks?key=${dynamic_links_key}`,
body: {
"longDynamicLink": makeDynamicLongLink(job)
},
headers: {
"Referer": "https://" + process.env.GCLOUD_PROJECT + ".cloudfunctions.net/createJobDynamicLink"
},
json: true
};
Note: I'd use request-promise-native instead of request-promise - same API but skips loading Bluebird.
In my case i had
<meta name="referrer" content="no-referrer">
in the head so the referer was not being sent
I am using the passportjs library to authenticate users into the application. An access token is usually generated when users authenticate successfully with passportjs. I am attempting to create a branch with the github API with this access token but without much success, both using the octokit wrapper as well as posting with super-agent.
I first attempted to authenticate the octokit by providing it username and password, in this fashion.
let octokit=new Octokit({
auth:{
username:username,
password:password
}
});
I was then able to create a ref/branch without much issue. However, when I did the same but with accesstokens generated by github SSO, like this
passport.use(new GitHubStrategy({
clientID: keys.clientId,
clientSecret: keys.clientSecret,
callbackURL: "/auth/github/callback"
},
async (accessToken, refreshToken, profile, done) => {
let octokit = new Octokit(auth: `token ${accessToken}`);
const branchName = 'refs/heads/vue' + Math.random();
let masterHash = '123x123x1231232';
octokit.git.createRef({
owner: owner,
repo: 'gitDemo',
ref: branchName,
sha: masterHash
}).then((data) => {
console.log('data ', data);
});
}
I receive an HttpError: Not found error. Another method that I tried is to post directly to the end point with superagent, putting the acccess code in the authorization header.
const data={
ref:'refs/heads/FooBranch',
sha:masterHash
};
const res2=await request.post('https://api.github.com/repos/SomeOwner/SomeRepo/git/refs')
.set('Authorization','token '+accessToken)
.send(data);
However, I still receive an HttpError :not found issue. I am quite confused as to what I may have done wrong. Thank you and any help would be greatly appreciated!
I found the anwser here
Basically you don't send data using JSON but rather FormData.
So the post should look like this (copied from link):
let data = new FormData()
data.append('client_id', options.client_id)
data.append('client_secret', options.client_secret)
data.append('code', code)
fetch(`https://github.com/login/oauth/access_token`, {
method: 'POST',
body: data
})
In case anyone else comes across this in the future, you have to specify the Content-Type and Accept when making the request. Without specifying it in the headers you will have to send and receive FormData.
Kudos #Github for not mentioning this at all in their docs.
Using node's built in fetch:
const githubRes = await fetch(githubUrl, {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
code,
client_id: config.github.clientId,
client_secret: config.github.clientSecret,
redirect_uri: config.github.redirectUri,
}),
});
const githubJson = await githubRes.json();
const token = githubJson.access_token;
bellow is my node js script to get google user details using accessToken
var accessToken = req.body.accessToken;
console.log(accessToken)
var google = require('googleapis');
//google api
var plus = google.plus('v1');
var OAuth2 = google.auth.OAuth2;
var oauth2Client = new OAuth2(
config.google.clientID,
config.google.clientSecret,
config.google.redirect_uri
);
oauth2Client.setCredentials({access_token: accessToken});
plus.people.get({
userId: 'me',
auth: oauth2Client
}, function (err, response) {
// handle err and response
if (err) {
reject(err)
} else {
console.log(response);
resolve(response)
}
});
need to get google login user details using accessToken. what is wrong in code?
The most likely cause is the user in question has not created a google+ profile. Here are a few more options.
I am not sure what information you are trying to get but the best way to get user info is to authecate a user using the profile scope then request the data directly of the user info endpoint
Request
GET /userinfo/v2/me HTTP/1.1
Host: www.googleapis.com
Content-length: 0
Authorization: Bearer uzG4XqnvucBFk3jylgxnbtNhoOt2wCc3QvUcjk7PZhJ5m6G7ibtlBwbAQntJjJtLsxZfoDjhbASpzLmcFnlZ9o4qoMd2fCV2eRb4O5XrKRAXC
Response
{
"family_name": "Lawton",
"name": "Linda Lawton",
"picture": "https://lh5.googleusercontent.com/-a1CWlFnA5xE/AAAAAAAAAAI/AAAAAAAAl1I/UcwPajZOuN4/photo.jpg",
"locale": "en",
"gender": "female",
"link": "https://plus.google.com/+LindaLawton",
"given_name": "Linda",
"id": "117200475532672775346"
}
You can also go though the google people api using the same profile scope
GET /v1/people/me HTTP/1.1
Host: people.googleapis.com
Content-length: 0
Authorization: Bearer NuzG4XqnvucBFk3jylgxnbtNhoOt2wCc3QvUcjk7PZhJ5m6G7ibtlBwbAQntJjJtLsxZfoDjhbASpzLmcFnlZ9o4qoMd2fCV2eRb4O5XrKRAXC
But this endpoint reads from Google+ so if the user has not filled anything out on their Google+ profile you wont see much data here.
You can use request module to get the user detail on your node server.
But Before requesting the user data, make sure you have authorized the API by giving it the desired scope. In your case, you need to give https://www.googleapis.com/auth/userinfo.profile in the scope.
When you receive your accessToken, use that token to call this google api
https://www.googleapis.com/oauth2/v1/userinfo
const request = require('request');
// use any api you want to call.
request({
url: 'https://www.googleapis.com/oauth2/v1/userinfo',
method: 'GET',
headers: {
'Authorization': `Bearer ${YourAccessToken}`,
'Accept': 'application/json'
}
}, function(err, response, _user) {
console.log('User Data', _user);
})
I hope this will solve your problem. If still there is some problem, you can test your Google APIs on OAuth 2.0 Playground