I have a script that runs when I click a "Test Connection" button in an Adaptive Insights Integration script. I am trying to log the XML that is returned, just to verify the information prior to coding the next part. The script I have is as follows:
function testConnection(context) {
// dataSource variable to get Data Source Setting Parameters
var dataSource = context.getDataSource();
// Get the static credentials parameters.
const userName = dataSource.getSetting("Adaptive Username").getValue(); // Not Nedeed after WD User Sync. Do Not delete
const password = dataSource.getSetting("Adaptive Password").getValue(); // Not Nedeed after WD User Sync. Do Not delete
const instanceCode = dataSource.getSetting("instanceCode").getValue();
//This example connects to Postman Echo using POST
var url = 'https://api.adaptiveinsights.com/api/v35';
var method = 'POST';
// XML request body, this is where the method type (ie. exportUsers) goes.
var body = '<?xml version="1.0" encoding="UTF-8"?><call method="exportUsers" callerName="AdaptiveInsightsAPI">'
body += '<credentials login="' + userName + '" password="' + password + '" /></call>';
var headers = { 'Content-Type': 'application/xml' };
var response = null;
let finalResponse = null;
try {
response = ai.https.request(url, method, body, headers);
finalResponse = response.getBody();
ai.log.logVerbose("API Call Successful", finalResponse);
return true;
}
catch (exception) {
ai.log.logInfo('api call failed', '' + exception);
return false;
}
}
The response will not show in the log, it only shows "...". However, if I change "finalResponse" to "finalResponse[1]" or try to log any other character from the string ([2],[3],[4],etc.), it displays the character in the string. I also tried to slice the characters to fit within the 1000 character limit, but that displays nothing at all when I try to log the response.
What can I do different to get the response body into the log?
Related
I have a pure Javascript app which attempts to get an access token from Azure using OAuth Authorization Flow with PKCE.
The app is not hosted in Azure. I only use Azure as an OAuth Authorization Server.
//Based on: https://developer.okta.com/blog/2019/05/01/is-the-oauth-implicit-flow-dead
var config = {
client_id: "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx",
redirect_uri: "http://localhost:8080/",
authorization_endpoint: "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize",
token_endpoint: "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token",
requested_scopes: "openid api://{tenant-id}/user_impersonation"
};
// PKCE HELPER FUNCTIONS
// Generate a secure random string using the browser crypto functions
function generateRandomString() {
var array = new Uint32Array(28);
window.crypto.getRandomValues(array);
return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
}
// Calculate the SHA256 hash of the input text.
// Returns a promise that resolves to an ArrayBuffer
function sha256(plain) {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return window.crypto.subtle.digest('SHA-256', data);
}
// Base64-urlencodes the input string
function base64urlencode(str) {
// Convert the ArrayBuffer to string using Uint8 array to convert to what btoa accepts.
// btoa accepts chars only within ascii 0-255 and base64 encodes them.
// Then convert the base64 encoded to base64url encoded
// (replace + with -, replace / with _, trim trailing =)
return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// Return the base64-urlencoded sha256 hash for the PKCE challenge
async function pkceChallengeFromVerifier(v) {
const hashed = await sha256(v);
return base64urlencode(hashed);
}
// Parse a query string into an object
function parseQueryString(string) {
if (string == "") { return {}; }
var segments = string.split("&").map(s => s.split("="));
var queryString = {};
segments.forEach(s => queryString[s[0]] = s[1]);
return queryString;
}
// Make a POST request and parse the response as JSON
function sendPostRequest(url, params, success, error) {
var request = new XMLHttpRequest();
request.open('POST', url, true);
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
request.onload = function () {
var body = {};
try {
body = JSON.parse(request.response);
} catch (e) { }
if (request.status == 200) {
success(request, body);
} else {
error(request, body);
}
}
request.onerror = function () {
error(request, {});
}
var body = Object.keys(params).map(key => key + '=' + params[key]).join('&');
request.send(body);
}
function component() {
const element = document.createElement('div');
const btn = document.createElement('button');
element.innerHTML = 'Hello'+ 'webpack';
element.classList.add('hello');
return element;
}
(async function () {
document.body.appendChild(component());
const isAuthenticating = JSON.parse(window.localStorage.getItem('IsAuthenticating'));
console.log('init -> isAuthenticating', isAuthenticating);
if (!isAuthenticating) {
window.localStorage.setItem('IsAuthenticating', JSON.stringify(true));
// Create and store a random "state" value
var state = generateRandomString();
localStorage.setItem("pkce_state", state);
// Create and store a new PKCE code_verifier (the plaintext random secret)
var code_verifier = generateRandomString();
localStorage.setItem("pkce_code_verifier", code_verifier);
// Hash and base64-urlencode the secret to use as the challenge
var code_challenge = await pkceChallengeFromVerifier(code_verifier);
// Build the authorization URL
var url = config.authorization_endpoint
+ "?response_type=code"
+ "&client_id=" + encodeURIComponent(config.client_id)
+ "&state=" + encodeURIComponent(state)
+ "&scope=" + encodeURIComponent(config.requested_scopes)
+ "&redirect_uri=" + encodeURIComponent(config.redirect_uri)
+ "&code_challenge=" + encodeURIComponent(code_challenge)
+ "&code_challenge_method=S256"
;
// Redirect to the authorization server
window.location = url;
} else {
// Handle the redirect back from the authorization server and
// get an access token from the token endpoint
var q = parseQueryString(window.location.search.substring(1));
console.log('queryString', q);
// Check if the server returned an error string
if (q.error) {
alert("Error returned from authorization server: " + q.error);
document.getElementById("error_details").innerText = q.error + "\n\n" + q.error_description;
document.getElementById("error").classList = "";
}
// If the server returned an authorization code, attempt to exchange it for an access token
if (q.code) {
// Verify state matches what we set at the beginning
if (localStorage.getItem("pkce_state") != q.state) {
alert("Invalid state");
} else {
// Exchange the authorization code for an access token
// !!!!!!! This POST fails because of CORS policy.
sendPostRequest(config.token_endpoint, {
grant_type: "authorization_code",
code: q.code,
client_id: config.client_id,
redirect_uri: config.redirect_uri,
code_verifier: localStorage.getItem("pkce_code_verifier")
}, function (request, body) {
// Initialize your application now that you have an access token.
// Here we just display it in the browser.
document.getElementById("access_token").innerText = body.access_token;
document.getElementById("start").classList = "hidden";
document.getElementById("token").classList = "";
// Replace the history entry to remove the auth code from the browser address bar
window.history.replaceState({}, null, "/");
}, function (request, error) {
// This could be an error response from the OAuth server, or an error because the
// request failed such as if the OAuth server doesn't allow CORS requests
document.getElementById("error_details").innerText = error.error + "\n\n" + error.error_description;
document.getElementById("error").classList = "";
});
}
// Clean these up since we don't need them anymore
localStorage.removeItem("pkce_state");
localStorage.removeItem("pkce_code_verifier");
}
}
}());
In Azure I only have an App registration (not an app service).
Azure App Registration
The first step to get the authorization code works.
But the POST to get the access token fails. (picture from here)
OAuth Authorization Code Flow with PKCE
Access to XMLHttpRequest at
'https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token' from
origin 'http://localhost:8080' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested
resource.
Where in Azure do I configure the CORS policy for an App Registration?
Okay, after days of banging my head against the stupidity of Azure's implementation I stumbled upon a little hidden nugget of information here: https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-browser#prerequisites
If you change the type of the redirectUri in the manifest from 'Web' to 'Spa' it gives me back an access token! We're in business!
It breaks the UI in Azure, but so be it.
You should define the internal url with your local host address.
https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/application-proxy-understand-cors-issues
When I first posted, the Azure AD token endpoint did not allow CORS requests from browsers to the token endpoint, but it does now. Some Azure AD peculiarities around scopes and token validation are explained in these posts and code in case useful:
Code Sample
Blog Post
I am returning a file from a WebAPI controller. The Content-Disposition header value is automatically filled, and it contains also my filename.
My backend code looks like this:
[HttpGet("Download")]
public async Task<ActionResult> Download([FromQuery]CompanySearchObject req)
{
string fileName = "SomeFileName_" + DateTime.UtcNow.Date.ToShortDateString() + ".csv";
var result = await _myService.GetData(req);
Stream stream = new System.IO.MemoryStream(result);
return File(stream, "text/csv", fileName.ToString());
}
And on my frontend:
exportData(params).then(response => {
console.log(response);
console.log('Response Headers:', response.headers);
const type = response.headers['content-type'];
const blob = new Blob([response.data], { type: type, encoding: 'UTF-8' });
//const filename = response.headers['content-disposition'].split('"')[1];
//alert(filename);
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = 'file.xlsx';
link.click();
link.remove();
});
};
But I'm struggling to fetch this data, because when I do console.log on frontend I can not see this.. as you can see I logged response console.log('Response Headers:', response.headers); but only thing I see is:
Shouldn't this data be somewhere? I'm wondering how can I read value from Content-Disposition and get filename?
Thanks guys
Cheers
Had the same problem. My problem was CORS. If you are using it, you need to expose it like this in your configuration:
app.UseCors(builder =>
{
builder.AllowAnyOrigin();
builder.AllowAnyMethod();
builder.AllowAnyHeader();
builder.WithExposedHeaders("Content-Disposition"); // this is the important line
});
The way I do it is by looping through all the request headers until I match the specific header I'm looking for.
// Headers
private void GetSetCustomHeaders(ref string theUsername, ref string thePassword, ref string theAPIKey)
{
try
{
foreach (KeyValuePair<string, IEnumerable<string>> header in this.Request.Headers)
{
string headerType = header.Key;
string headerTypeUpperTrim = headerType.Trim().ToUpper();
IEnumerable<string> headerValue = header.Value;
string fullHeaderValue = "";
bool isFirstHeaderValue = true;
foreach (string headerValuePart in headerValue)
{
if (isFirstHeaderValue)
{
fullHeaderValue = headerValuePart;
isFirstHeaderValue = false;
}
else
{
fullHeaderValue = fullHeaderValue + Environment.NewLine + headerValuePart;
}
}
if (headerTypeUpperTrim == "USERNAME")
{
theUsername = fullHeaderValue;
}
else if (headerTypeUpperTrim == "PASSWORD")
{
thePassword = fullHeaderValue;
}
else if (headerTypeUpperTrim == "APIKEY")
{
theAPIKey = fullHeaderValue;
}
}
}
catch (Exception)
{
//MessageBox.Show("Error at 'GetSetCustomHeaders'" + Environment.NewLine + Environment.NewLine + ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
In the example code above I'm looking for 'Username', 'Password' and 'APIKey'. They are passed through as ref parameters so If they're set in this method, they're set in the method calling this GetSetCustomHeaders method as well, because it references the same thing. So when I call this method initially my variables are set to string.Empty.
Hope this is helpful.
For Fetch Response Headers, it returns is an iterable, you have to loop through response.headers instead of log response.headers object.
Try code below:
response.headers.forEach(console.log);
console.log(response.headers.get('content-disposition'));
This is correct I face this issue.Try that.
It can be resolve only server side(Back end side).You must be add back end response headers like this,
Access-Control-Expose-Headers: [Content-Disposition , .....You can add multiple heders]
After that you can directly access this header using response.headers
I have a newbie question with receiving JSON data from Python working as a server. I need to tell you that the server is based on WebSockets and I'm sending JSON data from Python to JavaScript successfully but I can't find any source how to proceed with this data to parse this and show it in different divs like value of the first_name in the id="message-1" div, second_name in the message2 div etc. Could you help me figure this out? Here is the code and picture what my firefox return: Web page.
I almost forgot to mention that I'm using localhost with xampp or ngrok to "host" my client-side. Also, there is a connection because I'm receiving logs on the website as well as in python console
Thanks in advance :)
Here is python code:
import asyncio
import websockets
import json
async def time(websocket, path):
while True:
d = {'first_name': 'Guido',
'second_name': 'Rossum',
'titles': ['BDFL', 'Developer'],
}
parsed = json.dumps(d)
name = "Jeremy"
# now = datetime.datetime.utcnow().isoformat() + 'Z'
for i in range(1):
await websocket.send(parsed)
response = await websocket.recv()
print(response)
start_server = websockets.serve(time, '127.0.0.1', 4040)
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
Here is HTML/JS code
<body>
<div id="message-1"></div>
<div id="message-2"></div>
<div id="message-3"></div>
<script>
var ws = new WebSocket("ws://127.0.0.1:4040/");
ws.onopen = function () {
ws.send('Hello, Server!!');
//send a message to server once ws is opened.
console.log("It's working onopen log / awake");
};
ws.onmessage = function (event) {
if (typeof event.data === "string") {
// If the server has sent text data, then display it.
console.log("On message is working on log with onmessage :)");
var label = document.getElementById("message-1");
label.innerHTML = event.data;
}
};
ws.onerror = function (error) {
console.log('Error Logged: ' + error); //log errors
};
</script>
</body>
You need to parse the message you received and attach it to the dom!
ws.onmessage = function (event) {
try {
var obj = JSON.parse(event.data);
document.getElementById("message-1").innerHTML = obj.first_name;
document.getElementById("message-2").innerHTML = obj.second_name;
document.getElementById("message-3").innerHTML = obj.titles.join(" ");
} catch {
console.log("Object is not received, Message is:: ", event.data);
}
}
If this is not working, then check the json format your are sending!
Remember JSON Needs to be valid json, Replace ' (single-quote) with " (double-quote):
d = {
"first_name": "Guido",
"second_name": "Rossum",
"titles": ["BDFL", "Developer"]
}
I am rewriting an existing webform to use js libraries instead of using the vendor controls and microsoft ajax tooling (basically, updating the web app to use more contemporary methodologies).
The AS-IS page (webform) uses a button click handler on the server to process the submitted data and return a document containing xml, which document can either then be saved or opened (opening opens it up as another tab in the browser). This happens asynchronously.
The TO-BE page uses jquery ajax to submit the form to an MVC controller, where virtually the same exact code is executed as in the server-side postback case. I've verified in the browser that the same data is being returned from the caller, but, after returning it, the user is NOT prompted to save/open - the page just remains as if nothing ever happened.
I will put the code below, but I think I am just missing some key diferrence between the postback and ajax/controller contexts to prompt the browser to recognize the returned data as a separate attachment to be saved. My problem is that I have looked at and tried so many ad-hoc approaches that I'm not certain what I am doing wrong at this point.
AS-IS Server Side Handler
(Abridged, since the SendXml() method is what generates the response)
protected void btnXMLButton_Clicked(object sender, EventArgs e)
{
//generate server side biz objects
//formattedXml is a string of xml iteratively generated from each selected item that was posted back
var documentStream = MemStreamMgmt.StringToMemoryStream(formattedXml);
byte[] _documentXMLFile = documentStream.ToArray();
SendXml(_documentXMLFile);
}
private void SendXml(byte[] xmlDoc)
{
string _xmlDocument = System.Text.Encoding.UTF8.GetString(xmlDoc);
XDocument _xdoc = XDocument.Parse(_xmlDocument);
var _dcpXMLSchema = new XmlSchemaSet();
_dcpXMLSchema.Add("", Server.MapPath(#"~/Orders/DCP.xsd"));
bool _result = true;
try
{
_xdoc.Validate(_dcpXMLSchema, null);
}
catch (XmlSchemaValidationException)
{
//validation failed raise error
_result = false;
}
// return error message
if (!_result)
{
//stuff to display message
return;
}
// all is well .. download xml file
Response.ClearContent();
Response.Clear();
Response.ContentType = "text/plain";
Response.AddHeader("Content-disposition", "attachment; filename=" + "XMLOrdersExported_" + string.Format("{0:yyyy-MM-dd_hh-mm-ss-tt}.xml", DateTime.Now));
Response.BinaryWrite(xmlDoc);
Response.Flush();
Context.ApplicationInstance.CompleteRequest();
Response.End();
}
TO-BE (Using jquery to submit to a controller action)
Client code: button click handler:
queueModel.getXmlForSelectedOrders = function () {
//create form to submit
$('body').append('<form id="formXmlTest"></form>');
//submit handler
$('#formXmlTest').submit(function(event) {
var orderNbrs = queueModel.selectedItems().map(function (e) { return e.OrderId() });
console.log(orderNbrs);
var ordersForXml = orderNbrs;
var urlx = "http://localhost:1234/svc/OrderServices/GetXml";
$.ajax({
url: urlx,
type: 'POST',
data: { orders: ordersForXml },
dataType: "xml",
accepts: {
xml: 'application/xhtml+xml',
text: 'text/plain'
}
}).done(function (data) {
/*Updated per comments */
console.log(data);
var link = document.createElement("a");
link.target = "blank";
link.download = "someFile";//data.name
console.log(link.download);
link.href = "http://localhost:23968/svc/OrderServices/GetFile/demo.xml";//data.uri;
link.click();
});
event.preventDefault();
});
$('#formXmlTest').submit();
};
//Updated per comments
/*
[System.Web.Mvc.HttpPost]
public void GetXml([FromBody] string[] orders)
{
//same code to generate xml string
var documentStream = MemStreamMgmt.StringToMemoryStream(formattedXml);
byte[] _documentXMLFile = documentStream.ToArray();
//SendXml(_documentXMLFile);
string _xmlDocument = System.Text.Encoding.UTF8.GetString(_documentXMLFile);
XDocument _xdoc = XDocument.Parse(_xmlDocument);
var _dcpXMLSchema = new XmlSchemaSet();
_dcpXMLSchema.Add("", Server.MapPath(#"~/Orders/DCP.xsd"));
bool _result = true;
try
{
_xdoc.Validate(_dcpXMLSchema, null);
}
catch (XmlSchemaValidationException)
{
//validation failed raise error
_result = false;
}
Response.ClearContent();
Response.Clear();
Response.ContentType = "text/plain";
Response.AddHeader("Content-disposition", "attachment; filename=" + "XMLOrdersExported_" + string.Format("{0:yyyy-MM-dd_hh-mm-ss-tt}.xml", DateTime.Now));
Response.BinaryWrite(_documentXMLFile);
Response.Flush();
//Context.ApplicationInstance.CompleteRequest();
Response.End();
}
}*/
[System.Web.Mvc.HttpPost]
public FileResult GetXmlAsFile([FromBody] string[] orders)
{
var schema = Server.MapPath(#"~/Orders/DCP.xsd");
var formattedXml = OrderXmlFormatter.GenerateXmlForSelectedOrders(orders, schema);
var _result = validateXml(formattedXml.DocumentXmlFile, schema);
// return error message
if (!_result)
{
const string message = "The XML File(s) are not valid! Please check with your administrator!.";
return null;
}
var cd = new System.Net.Mime.ContentDisposition
{
FileName = "blargoWargo.xml",
Inline = false
};
System.IO.File.WriteAllBytes(Server.MapPath("~/temp/demo.xml"),formattedXml.DocumentXmlFile);
return File(formattedXml.DocumentXmlFile,MediaTypeNames.Text.Plain,"blarg.xml");
}
[System.Web.Mvc.HttpGet]
public FileResult GetFile(string fileName)
{
var cd = new System.Net.Mime.ContentDisposition
{
// for example foo.bak
FileName = fileName,
Inline = false
};
Response.AppendHeader("Content-Disposition", cd.ToString());
var fName = !string.IsNullOrEmpty(fileName)?fileName:"demo.xml";
var fArray = System.IO.File.ReadAllBytes(Server.MapPath("~/temp/" + fName));
System.IO.File.Delete(Server.MapPath("~/temp/" + fName));
return File(fArray, MediaTypeNames.Application.Octet);
}
UPDATE:
I just put the AS-IS/TO-BE side by side, and in the dev tools verified the ONLY difference (at least as far as dev tools shows) is that the ACCEPT: header for TO-BE is:
application/xhtml+xml, /; q=0.01
Whereas the header for AS-IS is
text/html, application/xhtml+xml, image/jxr, /
Update II
I've found a workaround using a 2-step process with a hyperlink. It is a mutt of a solution, but as I suspected, apparently when making an ajax call (at least a jQuery ajax call, as opposed to a straight XmlHttpRequest) it is impossible to trigger the open/save dialog. So, in the POST step, I create and save the desired file, then in the GET step (using a dynamically-created link) I send the file to the client and delete it from the server. I'm leaving this unanswered for now in the hopes someone who understands the difference deeply can explain why you can't retrieve the file in the course of a normal ajax call.
I'm stuck having to use a legacy web service that takes a username and password and returns xml indicating if the credentials are valid. The legacy service requires me to pass an http header with the request that contains the user's password. So, to get it to work, I had to hard code the password (actualUserPassword) in the header as follows:
var urlToCall = "https://ourlegacyauthserver/auth?uid=" + username);
var result = HTTP.call("GET", urlToCall, {headers:{"token:appname:127.0.0.1:actualUserPassword":""}});
This works when I hard code the proper password for the user on the server, but what I really need to do is build that header dynamically using the password variable, like this:
var urlToCall = "https://ourlegacyauthserver/auth?uid=" + username);
var headerString = "token:appname:127.0.0.1:" + password;
var result = HTTP.call("GET", urlToCall, {headers: {headerString: ""}});
When I do this, the auth server does not see the header coming in. What is wrong? I'm just trying to replace the hard coded string: "token:appname:127.0.0.1:actualUserPassword" with a string variable I built using the actual password passed in.
It's a javascript Object key problem. Try this instead:
var urlToCall = "https://ourlegacyauthserver/auth?uid=" + username);
var headerString = "token:appname:127.0.0.1:" + password;
var headerObject = {};
headerObject[headerString] = "";
var result = HTTP.call("GET", urlToCall, {headers: headerObject});