I'm trying to create a Shared Access Signature client side in my Node app. The reason being that I do not want to stream files through my app. I want the user to be able to upload a file to my Azure Data Lake Gen2 Blob Storage container directly.
I have looked at all examples I can find, but they are all server side. So I tried to generate generateDataLakeSASQueryParameters and use them in the PUT request. The process looks like it works and I return it to the client.
Server side:
async getFileUploadUrl(path) {
const now = new Date().toUTCString();
const startsOn = new Date(now);
startsOn.setMinutes(startsOn.getMinutes() - 10); // Skip clock skew with server
const expiresOn = new Date(now);
expiresOn.setHours(expiresOn.getHours() + 1); // Expires in one hour
const sharedKeyCredential = new StorageSharedKeyCredential(this.storageAccountName, this.accountKey);
const sas = generateDataLakeSASQueryParameters({
fileSystemName: this.fileSystemClient.name,
ipRange: { start: "0.0.0.0", end: "255.255.255.255" },
expiresOn,
protocol: SASProtocol.HttpsAndHttp,
permissions: DataLakeSASPermissions.parse("c").toString(), // Read (r), Write (w), Delete (d), List (l), Add (a), Create (c), Update (u), Process (p)
resourceTypes: AccountSASResourceTypes.parse("o").toString(), // Service (s), Container (c), Object (o)
services: AccountSASServices.parse("b").toString(), // Blob (b), Table (t), Queue (q), File (f)
startsOn,
version: "2019-12-12"
},
sharedKeyCredential);
const encodedURI = encodeURI(path);
const filePath = `${this.fileSystemClient.url}/${encodedURI}`;
return {
url: filePath,
signature: sas.signature,
};
}
Client side:
const { url, signature } = serverResponse;
const file = [file takes from an input tag];
const request = new XMLHttpRequest();
request.open('PUT', url, true);
request.setRequestHeader("x-ms-date", new Date().toUTCString());
request.setRequestHeader("x-ms-version", '2019-12-12');
request.setRequestHeader("x-ms-blob-type", 'BlockBlob');
request.setRequestHeader("Authorization", `SharedKey [storageaccount]:${signature}`);
request.send(file);
And what I keep getting back is a 403 with the following error:
The MAC signature found in the HTTP request '[signature]' is not the
same as any computed signature. Server used following string to sign:
'PUT\n\n\n1762213\n\nimage/png\n\n\n\n\n\n\nx-ms-date:Thu, 24 Sep 2020
12:24:05 GMT\nx-ms-version:2019-12-12\n/[account name]/[container
name]/[folder name]/image.png'.
Obviously I removed the actual signature since I have gotten it to work server side, but it looks something like this: hGhg765+NIGjhgluhuUYG686dnH90HKYFytf6= (I made this up, but it looks as if it's in the correct format).
I have also tried to return the parsed query string and used in a PUT request, but then I get errors stating there is a required header missing, and I cannot figure out which one that should be. No Authorization for instance should be required.
The method generateDataLakeSASQueryParameters is used to create a service sas token. After doing that, we can call Azure Datalake Rest API with the sas token as the query paramater
For example
Create sas token with method generateDataLakeSASQueryParameters. When we call method generateDataLakeSASQueryParameters, we should define a DataLakeSASSignatureValues class : https://learn.microsoft.com/en-us/javascript/api/#azure/storage-file-datalake/datalakesassignaturevalues?view=azure-node-latest
const {
StorageSharedKeyCredential,
generateDataLakeSASQueryParameters,
DataLakeSASPermissions,
} = require("#azure/storage-file-datalake");
const accountName = "testadls05";
const accountKey ="";
const now = new Date().toUTCString();
const startsOn = new Date(now);
startsOn.setMinutes(startsOn.getMinutes() - 10); // Skip clock skew with server
const expiresOn = new Date(now);
expiresOn.setHours(expiresOn.getHours() + 1); // Expires in one hour
const fileSas = generateDataLakeSASQueryParameters(
{
fileSystemName: "test",
pathName: "test.jpg",
permissions: DataLakeSASPermissions.parse("racwd"),
startsOn: startsOn,
expiresOn: expiresOn,
},
new StorageSharedKeyCredential(accountName, accountKey)
).toString();
console.log(fileSas);
Test (create file)
PUT http:// https://{accountName}.{dnsSuffix}/{filesystem}/{path}
?{sas token you create in step1}
Headers:
Content-Type:image/jpeg
Content-Length:0
Related
I'm trying to make a direct websocket connection to appsync, but right after connecting I get keep getting an NoProtocolError error (
{"payload":{"errors":[{"message":"NoProtocolError","errorCode":400}]},"type":"connection_error"}')
This is my code
let ws = undefined;
const startWebsocket = () => {
const url = 'wss://XXXX.appsync-realtime-api.YYYY.amazonaws.com/graphql';
ws = new WebSocket(url);
// just log everything
ws.onopen = (e) => {console.log('Socket opened', e);};
ws.onmessage = (e) => {console.log('Msg received', e);};
ws.onclose = (e) => {console.log('Socket closed', e);};
ws.onerror = (e) => {console.log('Socket error', e);};
};
Regarding the immediate question
The message is referring to the second parameter of the Websocket constructor: protocols. (MDN reference here)
You need to specify the protocol (more precise subprotocol) like below:
ws = new WebSocket(url, ['graphql-ws']);
However
Be aware, that you're also missing header information, payload information and the whole handshake.
Only changing adding the protocol will present you with the next error: Both, the "header", and the "payload" query string parameters are missing.
Further Information
Read more about how to build up a connection on aws.amazon.com.
Open websocket with header and payload
const api_header = {
host: 'XXXX.appsync-api.YYYY.amazonaws.com',
'x-api-key': '<YOUR APPSYNC API KEY>',
};
// payload should be an empty JSON object
const payload = {};
const base64_api_header = btoa(JSON.stringify(api_header));
const base64_payload = btoa(JSON.stringify(payload));
const appsync_url = url + '?header=' + base64_api_header + '&payload=' + base64_payload;
ws = new WebSocket(appsync_url, ['graphql-ws']);
Note that the host is appsync-api and not appsync-realtime-api
With this, you should see a 'ka' message and can go forward with the handshake (see next section).
How to do the handshake
See this document on AWS
References
WebSocket API referene (mentioning of second parameter)
PHP implementation for WebSocket to AppSync
Amazon's doc on how to connect to AppSync
Question: is there a way to set the request header item
contentType: application/json;odata=nometadata
prior to a call to TableClient.listEntities.
Objective: to receive data payloads uncluttered with odata metadata.
I am using the Azure Data Tables JavaScript API, and would like to specify request header item as follows:-
contentType: application/json;odata=nometadata
I've looked through the documentation (https://learn.microsoft.com/en-us/javascript/api/#azure/data-tables/?view=azure-node-latest) and there are some methods which facilitate changes to the request header, e.g. TableInsertEntityHeaders interface includes a property 'contentType'.
the TableClient.listEntities method includes a parameter (options?: ListTableEntitiesOptions) which does not include header access. So, as far as I can see, there is no obvious functionality supplied by the API to change the Request Header.
thank you
You can specify this in format parameter in the query options. Please see the sample code below:
const { TableClient, AzureNamedKeyCredential } = require("#azure/data-tables");
const account = "account-name";
const accountKey = "account-key";
const tableName = "table-name";
const credential = new AzureNamedKeyCredential(account, accountKey);
const client = new TableClient(`https://${account}.table.core.windows.net`, tableName, credential);
async function main() {
let entitiesIter = client.listEntities({
queryOptions: {
format: "application/json;odata=nometadata"
}
});
let i = 1;
for await (const entity of entitiesIter) {
console.log(`Entity ${i}:`);
console.log(entity);
console.log('==================');
i++;
}
}
main();
I wrote an interface to make requests to the internal audible api with python. Every API request needs to be signed with RSA SHA256.
Now I want to test the endpoints of the API with Postman and make use of the pre request script function. But I'm not firm with javascript. Maybe someone can help me in translate the following python function to a Postman script:
def sign_request(
request: httpx.Request, adp_token: str, private_key: str
) -> httpx.Request:
"""
Helper function who creates a signed requests for authentication.
:param request: The request to be signed
:param adp_token: the token is obtained after register as device
:param private_key: the rsa key obtained after register as device
:returns: The signed request
"""
method = request.method
path = request.url.path
query = request.url.query
body = request.content.decode("utf-8")
date = datetime.utcnow().isoformat("T") + "Z"
if query:
path += f"?{query}"
data = f"{method}\n{path}\n{date}\n{body}\n{adp_token}"
key = rsa.PrivateKey.load_pkcs1(private_key.encode())
cipher = rsa.pkcs1.sign(data.encode(), key, "SHA-256")
signed_encoded = base64.b64encode(cipher)
signed_header = {
"x-adp-token": adp_token,
"x-adp-alg": "SHA256withRSA:1.0",
"x-adp-signature": f"{signed_encoded.decode()}:{date}"
}
request.headers.update(signed_header)
return request
I found out how to get the request method and the body. I can get the path and query with pm.request.url.getPathWithQuery(). To add the headers to the request I use pm.request.headers.add.
But I doesn't know how to get the datetime in isoformat, join strings and sign the data.
I'm get it running with the pm lib. Thank you for your help.
The only issue is getting the private cert, who contains newlines, from env var gives an error with these code sig.init(privateKey);. I had to write the private cert string directly in the pre request script.
Here are my script.
eval( pm.globals.get('pmlib_code') );
var CryptoJS = require("crypto-js");
var moment = require("moment");
const adpToken = pm.environment.get("adp-token")
// private-key loaded from env var doesn't work because of newlines in it; bugfix
const privateKey = pm.environment.get("private-key")
// use private-key in pre request script directly make use of newline correctly
const privateKey2 = "-----BEGIN RSA PRIVATE KEY-----\nMIIE...==\n-----END RSA PRIVATE KEY-----\n"
signRequest(pm.request, adpToken, privateKey);
function signRequest(request, adpToken, privateKey2) {
const method = request.method;
const path = request.url.getPathWithQuery();
const body = request.body || "";
const date = moment.utc().format();
const data = `${method}\n${path}\n${date}\n${body}\n${adpToken}`;
var sig = new pmlib.rs.KJUR.crypto.Signature({"alg": "SHA256withRSA"});
sig.init(privateKey);
var hash = sig.signString(data);
const signedEncoded = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Hex.parse(hash));
pm.request.headers.add({
key: 'x-adp-token',
value: adpToken
});
pm.request.headers.add({
key: 'x-adp-alg',
value: 'SHA256withRSA:1.0'
});
pm.request.headers.add({
key: 'x-adp-signature',
value: `${signedEncoded}:${date}`
});
}
UPDATE:
Now reading the device cert from env var works. I had to replace const privateKey = pm.environment.get("private-key") with const privateKey = pm.environment.get("private-key").replace(/\\n/g, "\n")
The problem to make this in Postman is that you can use only those packages available in sandbox. So for this, you have crypto-js as the unique package helper for crypto operations.
var CryptoJS = require("crypto-js");
var moment = require("moment");
signRequest(pm.request, "yourAdpToken", "yourPrivateKey")
function signRequest(request, adpToken, privateKey) {
const method = request.method;
const path = request.url.getPathWithQuery();
const body = request.body.raw;
const date = moment.utc().format();
const data = `${method}\n${path}\n${date}\n${body}\n${adpToken}`
const hash = CryptoJS.HmacSHA256(data, privateKey);
const signedEncoded = CryptoJS.enc.Base64.stringify(hash);
pm.request.headers.add({
key: 'x-adp-token',
value: adpToken
});
pm.request.headers.add({
key: 'x-adp-alg',
value: 'SHA256withRSA:1.0'
});
pm.request.headers.add({
key: 'x-adp-signature',
value: `${CryptoJS.enc.Base64.parse(signedEncoded)}:${date}`
});
}
Adding the above to the Pre-request Script will add your wanted headers to the request like this:
You may need to change the encode parts, check the encode options
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 was going through the following article :
https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Fetching_data
Here , the concept of AJAX is being illustrated however , for simple illustration ,instead of connecting to the server ,the content is being fetched from the system which has the browser in it .
So in the following code lines from the above mentioned link :
var url = verse + '.txt';
var request = new XMLHttpRequest();
request.open('GET', url);
Here a GET verb is to fetch the contents of the file in the local system and no server is present there .
Similarly , by using javascript and in the absence of a server
can we add some parameters to GET or POST verb and run a code in the local system which processes these parameters and sends an output .
Like :
var url = 'verse + '.txt' + '?' 'name = ' + 'vim' ; //Adding parameters
and there will be some javascript file , which takes these parameter "name "
and returns it in uppercase , like "VIM " .
Can we do anything like that using Javascript only (not nodejs or anything that sets up a server " ) without server listening ?
To achieve the requirement you can use Chromium or Chrome browser launched with --allow-file-access-from-files flag set.
fetch() does not permit requesting local files having file: protocol, though XMLHttpRequest() does. fetch() does allow requesting data URL and Blob URL.
For
some javascript file , which takes these parameter "name " and returns
it in uppercase , like "VIM "
Worker can be used to get the contents of a local file, manipulate the content, then postMessage() can be called to communicate with main thread.
For example
worker.js
onmessage = e => {
// do stuff
let url = new URL(e.data);
let [[,param]] = [...url.searchParams]; // get query string parameter `'vim'`
let request = new XMLHttpRequest();
request.open('GET', 'file:///path/to/local/file' /* e.data */);
request.onload = e => {
console.log(request.responseText); // `setTimeout()` can be inside `load` handler
}
request.send();
// asynchronous response
setTimeout(() => {
// set `data URL` that `fetch()` will request
postMessage(`data:text/plain,${param.toUpperCase()}`);
}, Math.floor(Math.random() * 2000));
}
At console or within a script
const localRequest = async(url) => {
const request = await fetch(await new Promise(resolve => {
const worker = new Worker('worker.js');
worker.onmessage = e => {
resolve(e.data);
worker.terminate();
}
worker.postMessage(url);
}));
const response = await request.text();
console.log(response);
}
localRequest('file:///verse.txt?name=vim');