Cannot grant access to Common Data Service with NodeJS
I am implementing a simple Node function which will get some data from Common Data Service. I can get the accessToken already, but when I use this accessToken to access Common Data Service, the response is ‘Unauthorized’.
I followed the instruction here ( https://learn.microsoft.com/en-us/powerapps/developer/common-data-service/walkthrough-registering-configuring-simplespa-application-adal-js ) and is able to get it worked with simple page app.
I just want to port it to Node and have the app grant access to Common Data Service without requiring a user to login.
const fetch = require('node-fetch');
const AuthenticationContext = require('adal-node').AuthenticationContext;
module.exports = async function (context, req) {
context.log('JavaScript HTTP trigger function processed a request.');
const resource = "https://my-org.crm5.dynamics.com";
const clientId = 'my client id';
const clientSecret = 'my client secret';
const authorityHostUrl = 'https://login.microsoftonline.com';
const tenant = 'my-tenant-name.onmicrosoft.com'; // AAD Tenant name.
const authorityUrl = authorityHostUrl + '/' + tenant;
const authContext = new AuthenticationContext(authorityUrl);
const tokenResp = await new Promise((resolve, reject) => {
authContext.acquireTokenWithClientCredentials(resource, clientId, clientSecret, function (err, tokenResponse) {
if (err) {
context.error("cannot get token: " + err.stack);
return reject(err.stack);
} else {
return resolve(tokenResponse);
}
});
});
context.log("tokenResp: ", tokenResp); // The tokenResp contains accessToken
const cdsHeaders = {};
cdsHeaders["Authorization"] = "Bearer " + tokenResp.accessToken;
cdsHeaders["Accept"] = "application/json";
cdsHeaders["Content-Type"] = "application/json; charset=utf-8";
cdsHeaders["OData-MaxVersion"] = "4.0";
cdsHeaders["OData-Version"] = "4.0";
const endpointUrl = encodeURI(resource + "/api/data/v9.0/accounts?$select=name,address1_city&$top=10");
const dataResponse = await fetch(endpointUrl, { method: 'GET', headers: cdsHeaders });
console.log("response: ", dataResponse); // The dataResponse is 401 Unauthorized
context.res = { body: "Done" };
};
I got the solution: I have to 'Manually create a CDS for Apps application user' in order for it to work, regarding this document: https://learn.microsoft.com/en-us/powerapps/developer/common-data-service/authenticate-oauth#connect-as-an-app
Although the sample code is in C#, there are not too many differences between C# and Node.js clients.
Related
I am working on an azure function that is invoking a web service in node.js and it works fine. I have a function GetDetails to make an SQL query to retreive data from sql server database.
const sql = require("mssql");
const dataSQL = {};
const GUID = "";
const navServiceKey = "";
const navUserName = "";
async function GetDetails() {
var email = "yamen#gmail.com";
var password = "password";
try {
console.log("nav service" + navServiceKey);
// make sure that any items are correctly URL encoded in the connection string
await sql.connect(
"Server=tcp:app.windows.net,1433;Database=BHUB_TEST;User Id=AppUser;Password=password;Encrypt=true MultipleActiveResultSets=False;TrustServerCertificate=False;ConnectionTimeout=30;"
);
const result =
await sql.query`select * from users where email = ${email} AND password = ${password} `;
if (result.rowsAffected[0] >= 1) {
dataSQL = result.recordset[0];
navServiceKey = JSON.stringify(dataSQL.navServiceKey);
GUID = JSON.stringify(dataSQL.userGUID);
navUserName = JSON.stringify(dataSQL.navUserName);
} else {
console.log("failed");
}
} catch (err) {
}}
so since this is in node.js if i were to test this sql function only i'd do the following i.e. node index.js - then function will be executed successfully and return result. However, I am calling this function within the azure function like below but when I run the azure function project, then I copy the URL given to test it on postman, the sql function won't return anything !
Any idea of how to execute SQL query function in Azure function if that makes sense ?
module.exports = async function (context, req) {
GetDetails();
const axios = require("axios");
const data = {
email: req.query.email,
password: req.query.password,
};
var cred = "YAMEN" + ":" + "jbdv******";
const encoded = Buffer.from(cred, "utf8").toString("base64");
var credbase64 = "Basic " + encoded;
const headers = {
Authorization: credbase64,
"Content-Type": " application/json",
};
{
try {
const url = `https://tegos/BC19-NUP/QryEnwisAppUser?filter=UserSecurityID eq ${GUID}`;
const response = await axios.get(url, {
headers,
});
console.log(response);
console.log(response.data);
context.res = {
// status: 200, /* Defaults to 200 */
body: response.data,
};
} catch (e) {
// maybe return the error
console.error(e);
}}};
That is not how you connect to a SQL database from an Azure application. You need to use the pyodbc module instead.
Quickstart: Use Python to query a database
I can't use /regenerateKey [1] to regenerate keys for a Storage Account with the Azure Management API.
I'm using the following code in JavaScript (the resource has the subscription removed)
const { ClientSecretCredential } = require('#azure/identity');
const { SecretClient } = require('#azure/keyvault-secrets');
const MSRestAzure = require('ms-rest-azure');
const keyVaultName = process.env.KEY_VAULT_NAME;
const KVUri = `https://${keyVaultName}.vault.azure.net`;
const credential = new ClientSecretCredential(
process.env.AZURE_TENANT_ID,
process.env.AZURE_CLIENT_ID,
process.env.AZURE_CLIENT_SECRET,
);
const vault = new SecretClient(KVUri, credential);
function getCreds() {
return new Promise((res, rej) => {
MSRestAzure.loginWithServicePrincipalSecret(
process.env.AZURE_CLIENT_ID,
process.env.AZURE_CLIENT_SECRET,
process.env.AZURE_TENANT_ID,
(err, creds) => {
if (err) {
rej(err);
return;
}
res(creds);
},
);
});
}
const getResourceUrl = (resource, action) => `https://management.azure.com${resource}/${action}?api-version=2019-04-01`;
const resource = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg/providers/Microsoft.Storage/storageAccounts/MyStore
const creds = await getCreds();
const client = new MSRestAzure.AzureServiceClient(creds);
const regenUrl = getResourceUrl(resource, 'regenerateKey');
await client.sendRequest({ method: 'POST', url: regenUrl }).then(console.log);
I'm getting an UnexpectedException response -
{
"error": {
"code": "UnexpectedException",
"message": "The server was unable to complete your request."
}
}
The Client ID/Secret belongs to an app registration that has access to the storage account, as well as Contributor and Storage Account Key Operator over that subscription.
I'm lead to think that I've not formed the request properly.
I am able to reproduce the error if I don't specify the request body.
Please provide the request body in the following format:
{
keyName: "key1 or key2 (basically which key you want to regenerate)"
}
I have a scenario, where I am trying to fetch the API responses of the following host url of my account spaces and orgs in cloudfoundry SAP.
https://api.cf.eu10.hana.ondemand.com
I am using nodejs (wrapper cf-client) script to authenticate but whenever I try to login it provides below error
Error: {"error":"unauthorized","error_description":"{"error":"invalid_grant","error_description":"User authentication failed: INVALID_AUTHORIZATION_HEADER_LENGTH"}"}
Here is my nodejs script
"use-strict";
const endpoint = "https://api.cf.eu10.hana.ondemand.com";
const username = "myusername"; //I have created a trial account
const password = "Password"; //I have created a trial account
const JsonFind = require('json-find');
const fs = require('fs')
const util = require('util');
const dJSON = require('dirty-json');
const CloudController = new (require("cf-client")).CloudController(endpoint);
const UsersUAA = new (require("cf-client")).UsersUAA;
const Apps = new (require("cf-client")).Apps(endpoint);
const Spaces = new (require("cf-client")).Spaces(endpoint);
const Orgs = new (require("cf-client")).Organizations(endpoint);
CloudController.getInfo().then( (result) => {
UsersUAA.setEndPoint(result.authorization_endpoint);
return UsersUAA.login(username, password);
}).then( (result) => {
Orgs.setToken(result);
return Orgs.getOrganizations();
}).then((result) => {
all_orgs = result.resources //returns api
get_orgs=util.inspect(all_orgs, {depth: null});
console.log(get_orgs)
});
What I have seen is when I normaly login with cf client it requires sso passcode along with username password.
how can i provide that here or any idea how can I login here and fetch the data.
Since I did little more research on this and found out that a token generator can be used to overcome this, below code.
"use-strict";
var totp = require('totp-generator');
const endpoint = "https://api.cf.eu10.hana.ondemand.com";
var token = totp('clientsecretidgoeshere');
const username = "myusername"; //I have created a trial account
const password = "Password"+token; //I have created a trial account
const JsonFind = require('json-find');
const fs = require('fs')
const util = require('util');
const dJSON = require('dirty-json');
const CloudController = new (require("cf-client")).CloudController(endpoint);
const UsersUAA = new (require("cf-client")).UsersUAA;
const Apps = new (require("cf-client")).Apps(endpoint);
const Spaces = new (require("cf-client")).Spaces(endpoint);
const Orgs = new (require("cf-client")).Organizations(endpoint);
CloudController.getInfo().then( (result) => {
UsersUAA.setEndPoint(result.authorization_endpoint);
return UsersUAA.login(username, password);
}).then( (result) => {
Orgs.setToken(result);
return Orgs.getOrganizations();
}).then((result) => {
all_orgs = result.resources //returns api
get_orgs=util.inspect(all_orgs, {depth: null});
console.log(get_orgs)
});
I'm trying to make twilio access token in AWS lambda, but I get the error "callback is not a function". How can I fix it?
const AccessToken = require('twilio').jwt.AccessToken;
const VoiceGrant = AccessToken.VoiceGrant;
exports.generateToken = function(identity, callback) {
// Used when generating any kind of tokens
const accountSid = 'xxxxxxxxx';
const apiKey = 'xxxxx';
const apiSecret = 'xxx';
// Used specifically for creating Voice tokens
const pushCredSid = 'xxx';
const outgoingApplicationSid = 'xxxxx';
// Create an access token which we will sign and return to the client,
// containing the grant we just created
const voiceGrant = new VoiceGrant({
outgoingApplicationSid: outgoingApplicationSid,
pushCredentialSid: pushCredSid
});
// Create an access token which we will sign and return to the client,
// containing the grant we just created
const token = new AccessToken(accountSid, apiKey, apiSecret);
token.addGrant(voiceGrant);
token.identity = identity;
console.log('Token:' + token.toJwt());
callback(null, token.toJwt());
};
As Roland Starke said, it's worth changing this exports.generateToken = function(identity, callback) to exports.generateToken = function(event, context, callback) and everything will work fine.
I'm trying to create a simple client for Lync using Nodejs.
Base on http://ucwa.lync.com/documentation/KeyTasks-CreateApplication I've made someting like this.
It works until last step #9 when I should register my app with UCWA. Server responds with code 500 and silly explanation
There is a problem with the resource you are looking for, and it cannot be displayed
And headers
x-ms-diagnostics': '28032;source="mysource";reason="The web ticket is invalid."'
var http = require('request-promise');
var lync = {};
lync.setup = function(email, password){
var self = this;
var hostname = email.split('#');
this.username = email;
//discover urls
return http.get('http://lyncdiscover.'+hostname[1])
.then(function(d) {
var parsed = JSON.parse(d);
self.urls = {
self: parsed._links.self.href,
user: parsed._links.user.href,
xframe: parsed._links.xframe.href
};
return http.get(self.urls.user);
}).catch(function(err){
if(err.statusCode == 401){
var toParse = err.response.headers['www-authenticate'];
var Oauth = toParse.match(/https:\/\/[\d\w\./_-]*/i)[0];
var loginPost = {
grant_type: 'password',
username: email,
password: password
};
return http.post(Oauth, {form:loginPost});
}
return false
}).then(function(data){
var parsed = JSON.parse(data);
//setup authorization
http = http.defaults({
headers: {Authorization: parsed.token_type + ' ' + parsed.access_token}
});
//console.log(self.urls.user);
//console.log('Authorization:'+ parsed.token_type + ' ' + parsed.access_token);
return http.get(self.urls.user);
}).then(function(data){
var parsed = JSON.parse(data);
self.urls.applications = parsed._links.applications.href;
var registerApp = {
culture : "en-us",
endpointId : "2d9dc28d-4673-4035-825c-feb64be28e4e",
userAgent : "Test"
};
var r = "{'userAgent': 'NodeJs', 'endpointId' : '2d9dc28d-4673-4035-825c-feb64be28e4e', 'culture': 'en-US'}";
return http.post(self.urls.applications, {body: registerApp, json:true});
})
.then(function(data){
console.log(data);
})
.catch(function(err){
console.log(err);
return false;
});
};
//run app
lync.setup('login#domain.com', 'password').then(function(ret){
});
One key point here. It's not my server. I just have an account over there and I can login with official Lync client or Pidgin plugin.
Are there some extra steps to "allow" my app to work with UCWA?
#ShelbyZ
I can easily authorize using Oauth. I'm receiving authorization token so I'm logged in.
I'm receiving json similar to
"_links":{
"self":{"href":"link"},
"applications":{"href":"i need this"},
"xframe":{"href":"link"}
} }
Now. I need to "register my application" doing POST.
In this last step I get 500 code response.
I hope It's not related with that #Matthew Proctor said..
becouse I cannot simple administrate the server
Thank you #ShelbyZ
You were right, it was split-domain scenario. Now authorization works, and I can register my app. Also example for future generations
var http = require('request-promise');
var lync = {};
lync._authorize = function(){
var self = this;
var orgDomain = self.urls.user.match(/https:\/\/([\w\d\.]+)/i)[0];
//console.log(orgDomain);
http.get(self.urls.user).catch(function(err){
if(err.statusCode == 401){
var toParse = err.response.headers['www-authenticate'];
var Oauth = toParse.match(/https:\/\/[\d\w\./_-]+/i)[0];
var loginPost = {
grant_type: 'password',
username: self.username,
password: self.password
};
return http.post(Oauth, {form:loginPost});
}
}).then(function(data){
if(data) {
var parsed = JSON.parse(data);
//setup authorization
http = http.defaults({
headers: {Authorization: parsed.token_type + ' ' + parsed.access_token}
});
return http.get(self.urls.user);
}
}).then(function(data){
//check for split-domain scenario
var parsed = JSON.parse(data);
var domain = parsed._links.self.href.match(/https:\/\/([\w\d\.]+)/i)[0];
console.log('[1] '+orgDomain);
console.log('[2] '+domain);
if(domain!== orgDomain){
//split domain scenario
self.urls.user = self.urls.user.replace(orgDomain, domain);
http = http.defaults({
headers: {Authorization: null}
});
self._authorize();
} else { //create app
var parsed = JSON.parse(data);
self.urls.applications = parsed._links.applications.href;
var registerApp = {
culture : "en-us",
endpointId : "2d9dc28d-4673-4035-825c-feb64be28e4e",
userAgent : "NodeJs client"
};
return http.post(self.urls.applications, {body: registerApp, json:true});
}
}).then(function(app){
console.log(app);
});
};
lync.setup = function(email, password){
var self = this;
var hostname = email.split('#');
this.username = email;
this.password = password;
//discover urls
return http.get('http://lyncdiscover.'+hostname[1])
.then(function(d) {
var parsed = JSON.parse(d);
self.urls = {
self: parsed._links.self.href,
user: parsed._links.user.href,
xframe: parsed._links.xframe.href
};
return self._authorize();
});
};
//run app
lync.setup('username#domain.com', 'password');
I was getting the same error before I added my test domain to the list of Allowed Domains.
This can be updated via PowerShell, full details below:
Enabling UCWA and Configuring Allowed Domains
I've had clients see similar errors when running my code from http://localhost/, their fix was to test their code on a FQDN (ie http://testmyucwacode.mydomain.com/).