How to get Microsoft Graph API Access token from Node Script? - node.js

I'd like to use this library to interact with the graph API for my AD - https://github.com/microsoftgraph/microsoft-graph-docs/blob/master/concepts/nodejs.md
However, all of the existing javascript libraries I've found to return access tokens expect a return URL to be passed in, as well as some other web-specific stuff, leading me to believe this is some kind of requirement on Microsoft's end.
Is there any good way to authenticate/receive an access token while running a backend node script (nothing web related) so that I can begin to make calls against the Microsoft Graph API? Thanks in advance for the advice.

BU0's answer didn't work correctly for me because Microsoft changed their way of using the graph API so I wasn't able to get all the data I needed. Here's how I did it using BU0 answer and this tutorial:
const request = require("request");
const endpoint = "https://login.microsoftonline.com/[Tenant]/oauth2/v2.0/token";
const requestParams = {
grant_type: "client_credentials",
client_id: "[ApplicationID]",
client_secret: "[Key]",
scope: "https://graph.microsoft.com/.default"
};
request.post({ url: endpoint, form: requestParams }, function (err, response, body) {
if (err) {
console.log("error");
}
else {
console.log("Body=" + body);
let parsedBody = JSON.parse(body);
if (parsedBody.error_description) {
console.log("Error=" + parsedBody.error_description);
}
else {
console.log("Access Token=" + parsedBody.access_token);
}
}
});
function testGraphAPI(accessToken) {
request.get({
url:"https://graph.microsoft.com/v1.0/users",
headers: {
"Authorization": "Bearer " + accessToken
}
}, function(err, response, body) {
console.log(body);
});
}

To run a back-end non-user-authenticated daemon connected to the Graph API, you want to use the app-only authentication flow. Here's a quick summary of the official steps:
Create your Azure AD Tenant. Note the yourtenant.onmicrosoft.com name, and copy this value down.
Register an application through the global Azure Active Directory blade's App Registrations section, not directly within the tenant properties. Copy the Application ID; we'll need it later.
Create a key tied to the registration and remember to copy it down. Once you click out, you can't get the key value back, so make sure to copy it.
Also update the registration's permissions to what you need, click Save, and then also hit the Grant Permissions button.
Make an HTTP request to the login.microsoftonline.com domain to obtain an access token.
Use the access token to make Graph API requests.
Here's a link to Microsofts Node.js example, and here's a link to the direct documentation on the HTTP call to make to retrieve an access token. And here's a super stripped-down example that will output the retrieved access token. Replace the [Tenant], [ApplicationID], and [Key] values:
const request = require("request");
const endpoint = "https://login.microsoftonline.com/[Tenant].onmicrosoft.com/oauth2/token";
const requestParams = {
grant_type: "client_credentials",
client_id: "[ApplicationID]",
client_secret: "[Key]",
resource: "https://graph.windows.net"
};
request.post({ url:endpoint, form: requestParams }, function (err, response, body) {
if (err) {
console.log("error");
}
else {
console.log("Body=" + body);
let parsedBody = JSON.parse(body);
if (parsedBody.error_description) {
console.log("Error=" + parsedBody.error_description);
}
else {
console.log("Access Token=" + parsedBody.access_token);
}
}
});
Once we have the access_token, we can call out to the Graph API. Assuming the apps permissions were configured correctly and applied from step #4, we can start making Graph API requests:
function testGraphAPI(accessToken) {
request.get({
url:"https://graph.windows.net/[Tenant]/users?api-version=1.6",
headers: {
"Authorization": accessToken
}
}, function(err, response, body) {
console.log(body);
});
}

I had somewhat of an issue for using the url string for the const endpoint
https://login.microsoftonline.com/[Tenant]/oauth2/v2.0/token
Instead, I passed tenant in this way instead from Microsoft graph api docs:
https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize
Reference from docs -> Request an authorization code

Another way:
'use strict';
const axios = require('axios');
const qs = require('qs');
const accessTokenWithCredentials = (tenantId, clientId, clientSecret, resource) => {
const data = {
resource: resource,
grant_type: 'client_credentials',
};
return axios({
url: `https://login.windows.net/${tenantId}/oauth2/token`,
method: "post",
headers: { 'content-type': 'application/x-www-form-urlencoded' },
auth: {
username: clientId,
password: clientSecret,
},
data: qs.stringify(data)
}).catch(error => {
throw error;
})
};
To call the function:
accessTokenWithCredentials(<tenantId>, <clientId>, <clientSecret>, 'https://graph.microsoft.com').then(response => {
console.log(`Got access token`);
const token = JSON.stringify(response.data.access_token);
// do what you need to do
}).catch(err => {
console.log("err " + err);
throw err;
});

Related

Add new fields to context variable in strapi backend for user management

I am using strapi as backend and react in the front-end. So the use case is that the user will signup and that signup will be done using auth0. I have defined some roles for the users signing up as shown on auth0
Roles based on plan taken by user
const _ = require("lodash");
const axios = require("axios");
const jwt = require("../jwt");
module.exports = async (ctx, next) => {
let role;
if (ctx.state.user) {
// request is already authenticated in a different way
return next();
}
try {
const tokenInfo = await axios({ method: "post",
url: `${process.env.AUTH0_URL}/userinfo`,
headers: { Authorization: ctx.request.header.authorization,
},
});
let user_id = tokenInfo.data.sub;
var config = { method: "get",
url: `${process.env.AUTH0_URL}/api/v2/users/${user_id}/roles`,
headers: {Authorization: `Bearer ${jwt.jwtSecret}`,
},
};
axios(config).then(function (response) {
ctx.state.roles = response.data[0].name; // This part does not work in the next policy as ctx.state.role gives undefined in route specific policy
}).catch(function (error) {
console.log(error);
});
// console.log(tokenInfo.data, "tokeninfo");
if (tokenInfo && tokenInfo.data) {
return await next();
}
} catch (err) {
console.log(err.message);
return handleErrors(ctx, err, "unauthorized");
}
Currently these will be managed here only. Now I have a collection which has some research articles which can only be accessed depending upon the plan user has been assigned. In order to protect the route and strapi access I have installed user-permissions plugin in strapi and managing userinfo using a global policy as shown
Project Structure
. So here is the code through which I am checking the user info on every route
Now there are two ways in which I tried solving my problem. First I read the tokenInfo data from userInfo route but unfortunately auth0 is not returning roles assigned. It is only returning standard data like
"name": "ansh5#gmail.com",
"nickname": "ansh5",
"picture": "https://s.gravatar.com/avatar/6fdb83f10321dd7712ac2457b11ea34e?
s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fan.png",
"updated_at": "2021-07-19T08:03:50.461Z",
"user_id": "auth0|60ec0b3721224b0078ac95f4",
So in order to get user role I used the other API and configured it with my auth0 account.
${process.env.AUTH0_URL}/api/v2/users/${user_id}/roles
I am getting the correct response but when I am doing this assignment.
ctx.state.roles = response.data[0].name;
I am getting undefined in my ctx.state.roles in my route specific policy. Does anybody have idea how we manage strapi and auth0 together.
Yes, it's because the axios calls are asynchronous in nature. So as per your code, axios will try to get the user information over a network call, but strapi will not really wait for the response. Instead it will just move forward to the next policy, hence resulting in an undefined user role. To fix this, you need to await for the api response from axios. Try the code below:
const axios = require("axios");
const jwt = require("../jwt");
module.exports = async (ctx, next) => {
let role;
if (ctx.state.user) {
// request is already authenticated in a different way
return next();
}
try {
const tokenInfo = await axios({
method: "post",
url: `${process.env.AUTH0_URL}/userinfo`,
headers: {
Authorization: ctx.request.header.authorization,
},
});
let user_id = tokenInfo.data.sub;
var config = {
method: "get",
url: `${process.env.AUTH0_URL}/api/v2/users/${user_id}/roles`,
headers: {
Authorization: `Bearer ${jwt.jwtSecret}`,
},
};
const resp = await axios(config);
ctx.state.roles = response.data[0].name;
console.log(ctx.state.roles);
// console.log(tokenInfo.data, "tokeninfo");
if (tokenInfo && tokenInfo.data) {
return await next();
}
} catch (err) {
console.log(err.message);
return handleErrors(ctx, err, "unauthorized");
}
}

How to CORRECTLY get token for Cloud Functions in NodeJS running on AppEngine?

I am having problems getting the correct token for triggering my cloud function.
When testing through POSTMAN I get the token by running the following command:
gcloud auth print-identity-token
and my functions works correctly.
But on my server I am using the following code. I also do see the token but I get 401 with this token.
// Constants------------
const metadataServerTokenURL = 'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=';
async function gToken(){
let token='';
try{
// Fetch the token
const tokenResponse = await fetch(metadataServerTokenURL + 'https://'+process.env.CLOUD_URL, { //URL WITHOUT THE PATH
headers: {
'Metadata-Flavor': 'Google',
},
});
token = await tokenResponse.text();
} catch (err){
console.log(err);
}
return token;
}
---------EDIT-------
The calling function::
app.get("/", async function(req , res){
try {
const token = await getToken();
console.log(`Token: ${token}`);
const functionResponse = await fetch('https://'+process.env.CLOUD_URL+process.env.PATH_TO_FUNC, {
method: 'post',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`},
});
console.log(`Status: ${await functionResponse.status}`);
res.sendStatus(200);
} catch (err){
console.log(err);
res.status(400).send('Something went wrong')
}
})
My server is my NodeJS code running on AppEngine.
What am I doing wrong please?
----------EDIT 2--------------
I entered the two tokens received using two different ways, they show different information for some reason. Please see below::
Token from the server
Token using gcloud command locally (which works)::
Server code and cloud functions are both hosted in the same region, and are a part of the same project.
process.env.CLOUD_URL > "e****-***2-c******-e******2.cloudfunctions.net"
What #Charles and #John mentioned in the comment is correct. You should include the name of the receiving function in the audience:
As mentioned in the docs:
In the calling function, you'll need to create a Google-signed OAuth ID token with the audience (aud) set to the URL of the receiving function.
const metadataServerTokenURL = 'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=';
...
// Fetch the token
const tokenResponse = await fetch(metadataServerTokenURL + `https://${process.env.CLOUD_URL}/${FUNCTION_NAME}`, {
headers: {
'Metadata-Flavor': 'Google',
}
The audience should look like your HTTP trigger URL. If you decode your JWT ID token, aud looks like this:
{
"aud": "https://[REGION]-[PROJECT_ID].cloudfunctions.net/func-post",
"azp": "117513711437850867551",
"exp": 1614653346,
"iat": 1614649746,
"iss": "https://accounts.google.com",
"sub": "117513711437850867551"
}

Power BI API returning 403 forbidden when fetching dashboard in My workspace

I would try the Power BI API.
So I start by getting Embed Token ( I'm using application owns data scenario ).
1.access Token
var options = {
'method': 'POST',
'url': `https://login.microsoftonline.com/${process.env.TENANT_ID}/oauth2/token`,
'headers': {
'Content-Type': 'multipart/form-data'
},
formData: {
'grant_type': process.env.GRANT_TYPE,
'client_id': process.env.CLIENT_ID,
'client_secret': process.env.CLIENT_SECRET,
'resource': process.env.RESSOURCE,
'Scope': process.env.SCOPE
}
};
// get Access token from AAD to retrieve Embed Token in PBI API
let response;
try {
response = await new Promise((resolve, reject) => {
request(options, (error, response, data) => {
if (error) reject(error)
else resolve(data)
})
})
}
catch (error) {
context.error(error)
}
2.Fetch embed Token (Docs)
var data = '{accessLevel:"View"}';
var config = {
method: 'post',
url: `https://api.powerbi.com/v1.0/myorg/groups/${process.env.GROUP_ID}/dashboards/${process.env.DASHBOARD_ID}/GenerateToken`,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${JSON.parse(response).access_token}`
},
data: data
};
const embedtoken = await axios(config)
context.res = {
// status: 200, /* Defaults to 200 */
body: embedtoken.data
};
3. I have delegated permissions on azure
I'm getting an embed token.
4. fetch dashboard infos
I'm using postman to fetch dashboard infos from the same group id and dashboard id that I mentioned in the api to get embed token
( I add that token in authorization section )
The problem is that I'm getting 403 Forbidden error.
PS: In this post some limitations of service principal method are mentioned . is it the source of my problem ? Am I obliged to use master user method ?
You have a misunderstanding of the usage of Embed Token. It cannot be used to call https://api.powerbi.com/v1.0/myorg/groups/{group id}/dashboards/{dashboard id}/ directly. AAD token is needed here.
To use an Embed Token, you should call the Embed URL with it.
The format of Embed URL is like:
https://app.powerbi.com/reportEmbed?reportId=f6bfd646-b718-44dc-a378-b73e6b528204&groupId=be8908da-da25-452e-b220-163f52476cdd&config=eyJjbHVzdGVyVXJsIjoiaHR0cHM6Ly9XQUJJLVVTLU5PUlRILUNFTlRSQUwtcmVkaXJlY3QuYW5hbHlzaXMud2luZG93cy5uZXQiLCJlbWJlZEZlYXR1cmVzIjp7Im1vZGVybkVtYmVkIjp0cnVlfX0%3d
An .net example:
// You need to provide the workspaceId where the dashboard resides.
ODataResponseListReport reports = await client.Reports.GetReportsInGroupAsync(workspaceId);
// Get the first report in the group.
Report report = reports.Value.FirstOrDefault();
// Generate Embed Token.
var generateTokenRequestParameters = new GenerateTokenRequest(accessLevel: "view");
EmbedToken tokenResponse = client.Reports.GenerateTokenInGroup(workspaceId, report.Id, generateTokenRequestParameters);
// Generate Embed Configuration.
var embedConfig = new EmbedConfig()
{
EmbedToken = tokenResponse,
EmbedUrl = report.EmbedUrl,
Id = report.Id
};
Then you can call the EmbedUrl with EmbedToken.
Reference here.

Node.js back-end application on Firebase to access Azure Active Directory users

I have a Node.js back which currently is running on Firebase in the form of cloud functions. Except for that, I also have an Azure Active Directory with some users that I have invited
So, I want to be able to access them from the Node.js get a list of their emails and names. From what I understood, I can achieve that by making a reference to Microsoft's Graph API and more specifically to their Users API. As every request to Azure AD needs to be OAuth2 authenticated, I was wondering what is the best way of achieving that in my situation. What client flow do I need to implement? I am currently focused on the one which is based on client credentials.
Thanks in advance and whatever general suggestion are more than welcome!
This issue gets Access token and calls Microsoft Graph API using node.js.
The user API of Azure Active Directory Graph API is no longer updating. This MS graph API is newer.
Get access token using client credentials flow:
const request = require("request");
const endpoint = "https://login.microsoftonline.com/[Tenant]/oauth2/v2.0/token";
const requestParams = {
grant_type: "client_credentials",
client_id: "[ApplicationID]",
client_secret: "[Key]",
scope: "https://graph.microsoft.com/.default"
};
request.post({ url:endpoint, form: requestParams }, function (err, response, body) {
if (err) {
console.log("error");
}
else {
console.log("Body=" + body);
let parsedBody = JSON.parse(body);
if (parsedBody.error_description) {
console.log("Error=" + parsedBody.error_description);
}
else {
console.log("Access Token=" + parsedBody.access_token);
}
}
});
Call MS Graph API:
function testListGroupGraphAPI(accessToken) {
request.get({
url:"https://graph.microsoft.com/v1.0/users",
headers: {
"Authorization": "Bearer " + accessToken
}
}, function(err, response, body) {
console.log(body);
});
}

How to access form submissions from Netlify for logged in users (with certain roles) only

I have a question about getting form submissions, but only for registered users with a certain role in my application.
My first failed attempt:
I have tried to create a lambda function which accesses form submissions using the access_token received from a succesfull (invite-user) login on my client. However, I am only getting an empty response.
My second attempt:
Instead of using the user's access_token, I created a new Personal access token, stored it as an environment variable in my application on Netlify, and I use the token in my lambda function using process.env.ACCESS_TOKEN. Using this approach is valid, and I received all form submissions.
Here is my lambda function, test.js:
const request = require('request');
exports.handler = function(event, context, callback) {
let ACCESS_TOKEN = process.env.ACCESS_TOKEN; // when using my personal API token created in Netlify. Does work
let ACCESS_TOKEN = event["queryStringParameters"]['access_token']; // passing the user's `access_token` from the client in request url. Does not work.
const options={
url: "https://api.netlify.com/api/v1/sites/MY_SITE_ID/forms/MY_FORM_ID/submissions",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${ACCESS_TOKEN}`
}
}
request(options, (error, response, body) => {
console.log("access_token", ACCESS_TOKEN); // I know the ACCESS_TOKEN is present in both of my scenarios.
callback(null, {
headers: { "Access-Control-Allow-Origin": "http://localhost:4200","Access-Control-Allow-Headers": "Content-Type"},
statusCode: 200,
body: body
});
});
}
My request url from the client looks like this:
https://MY_DOMAIN.com/.netlify/functions/test?access_token=ACCESS_TOKEN.
I am wondering how I can get form submissions for users with certain roles only. What am I doing wrong? What is the best practice for my scenario?
I sent my question to the team at Netlify, and they were able to help me out. It seems that the acces_token I was sending to the lamdba function was a JWT (json web token). All I needed to do was to decode and verify the JWT in order to access my data. I could then use the user object decoded from the JWT to read the roles. I ended up using code similar to this:
const request = require('request');
const jwt = require('jsonwebtoken');
exports.handler = function(event, context, callback) {
const JWT_SECRET = "MY SECRET"; // secret used to verify the signature of the jwt
let ACCESS_TOKEN;
let user;
if (event && event.queryStringParameters && event.queryStringParameters.access_token) {
const jwt_token = event.queryStringParameters.access_token;
jwt.verify(jwt_token, JWT_SECRET,
(err, decoded) => {
user = decoded;
});
}
if(user && user.app_metadata && user.app_metadata.roles &&
(user.app_metadata.roles.includes("admin") || user.app_metadata.roles.includes("editor"))){
if(process.env && process.env.ACCESS_TOKEN){
ACCESS_TOKEN = process.env.ACCESS_TOKEN;
}
}
const options={
url: "https://api.netlify.com/api/v1/sites/SITE_ID/forms/FORM_ID/submissions",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${ACCESS_TOKEN}`
}
};
request(options, (error, response, body) => {
callback(null, {
headers: { "Access-Control-Allow-Origin": "http://localhost:4200","Access-Control-Allow-Headers": "Content-Type"},
statusCode: 200,
body: body
});
});
}

Resources