Authorization header in img src link - javascript

I have an api that uses jwt for authencation. I am using this api for a vuejs app. I am trying to display an image in the app using
<img src="my/api/link" />
But the api expects Authorization header with jwt token in it.
Can I add headers to browser request like this(Answer to few questions here has made me believe it's not possible)?
Is there any way around it(using js) or should i change the api itself?

You can not perform authentication on images which are directly used as href in img tag. If you really want this type of authentication on your images, then it's better to fetch them using ajax and then embed in your html.

By default browsers are sending cookies.
You can prevent cookie sending in fetch if you set header's {credentials: 'omit'}. MDN
Full fetch example:
const user = JSON.parse(localStorage.getItem('user'));
let headers = {};
if (user && user.token) {
headers = { 'Authorization': 'Bearer ' + user.token };
}
const requestOptions = {
method: 'GET',
headers: headers,
credentials: 'omit'
};
let req = await fetch(`${serverUrl}/api/v2/foo`, requestOptions);
if (req.ok === true) {
...
Now, when you are login in, in your website, the webapp could save
to credentials into both localStorage and cookie.
Example:
let reqJson = await req.json();
// response is: {token: 'string'}
//// login successful if there's a jwt token in the response
if (reqJson.token) {
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('user', JSON.stringify({token: reqJson.token}));
document.cookie = `token=${reqJson.token};`; //set the cookies for img, etc
}
So your webapp uses localStorage, just like your smartphone application.
Browser gets all the static contents (img, video, a href) by sending cookies by default.
On the server side, you can copy the cookie to authorization header, if there is none.
Node.js+express example:
.use(function(req, res, next) { //function setHeader
if(req.cookies && req.headers &&
!Object.prototype.hasOwnProperty.call(req.headers, 'authorization') &&
Object.prototype.hasOwnProperty.call(req.cookies, 'token') &&
req.cookies.token.length > 0
) {
//req.cookies has no hasOwnProperty function,
// likely created with Object.create(null)
req.headers.authorization = 'Bearer ' + req.cookies.token.slice(0, req.cookies.token.length);
}
next();
})
I hope it helps someone.

You can use a Service Worker to intercept the img fetchs and add the Authorization header with the JWT token before hitting the server. Described in:
https://www.sjoerdlangkemper.nl/2021/01/06/adding-headers-to-image-request-using-service-workers/
https://www.twelve21.io/how-to-access-images-securely-with-oauth-2-0/#:~:text=4.%20USE%20SERVICE%20WORKERS

A workaround I often use is by leveraging a so-called nonce API endpoint. When calling this endpoint from the authenticated client, a short living string (could be a guid) is generated (for instance 30 seconds) and returned. Server-side you could of course add current user data to the nonce if you wish.
The nonce is then added to the image's query string and be validated server-side. The cost of this workaround is an extra API call.The main purpose of the workaround however is an increased security warrantee. Works like a charm ;) .

This is my solution based on Tapas' answer and this question How to display Base64 images in HTML?:
let jwtHeader = {headers: { Authorization: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX..."}
let r = await axios.get(`/path/image`, {...jwtHeader, responseType:"arraybuffer"});
let d = Buffer.from(r.data).toString('base64');
let a = document.createElement('img');
a.src = `data:image/png;base64, ${d}`;
a.width = 300;
a.height = 300;
document.getElementById("divImage").appendChild(a);
In this case the html would have a <div id="divImage">

<img src="/api/images/yourimage.jpg?token=here-your-token">
In the backend you validate JWT from queryparam.

There is another one method adds headers to HTTP request. Is it "Intercept HTTP requests". https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Intercept_HTTP_requests

Try this
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>测试获取图片</title>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</head>
<body>
<img id="test-img" src="" />
<script>
var request = new XMLHttpRequest();
request.open('GET','http://127.0.0.1/appApi/profile/cust/idcard/2021/12/30/533eed96-da1b-463b-b45d-7bdeab8256d5.jpg', true);
request.setRequestHeader('token', 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NDA5MTg1NTgsInVzZXJpZCI6IjMxIn0.TQmQE9E1xQwvVeAWRov858W2fqYpSMxZPCGlgvtcUDc');
request.responseType = 'arraybuffer';
request.onload = function(e) {
var data = new Uint8Array(this.response);
var raw = String.fromCharCode.apply(null, data);
var base64 = btoa(raw);
var src = "data:image;base64," + base64;
document.getElementById("test-img").src = src;
};
request.send();
</script>
</body>
</html>

Related

Azure Data Tables JS SDK - how to set Request Header - contentType: application/json;odata=nometadata

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();

fetch() sends the wrong URL Request to server

I have encountered a strange problem, doing a POST request using fetch(). It should have been easy, but I keep having an error code 405 from the server. Furthermore, the request URL should be only "http://localhost:3000/api/teddies/order", but somehow the local Visual_liveserver keeps adding in front of the URL request (the local server is hosted on this with port 5500 : http://127.0.0.1:5500)... In the image below you can see the error code 405 and this strange request URL.
Network inspector of the POST method : fetch()
By following this link, you will be able to access the Git of this projet. Don't hesitate to have a look at it ;) The file that calls these functions is called "pageFillingPanier.js".
But in short find below the code of the function that has the fetch in it:
const sendPurchaseRequest = async function (dataToSend) {
console.log(dataToSend);
try {
let response = await fetch('h​ttp://localhost:3000/api/teddies/order', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify(dataToSend)
});
console.log(response.ok); //it shows false...
let responseData = await response.json();
sessionStorage.setItem('memoryResponse', responseData.orderId);
//window.location = 'remerciement.html';
} catch (error){
console.log(error);
}
}
Find below the code that calls the function:
document.getElementById('bttFormSend').addEventListener('click', function (e) {
e.preventDefault();
let formPurchaseOrder = {
contact : {
firstName : document.getElementById('firstName').value,
lastName : document.getElementById('lastName').value,
email : document.getElementById('email').value,
address : document.getElementById('adress').value,
city : document.getElementById('city').value},
products : []
};
for (let index = 0; index < basketToDisplay.length; index++) {
formPurchaseOrder.products.push(basketToDisplay[index].id);
}
//this function send the POST request to the server
sendPurchaseRequest (formPurchaseOrder);
});
As the image suggests, there's a non visible unicode character present in the url. This happens sometimes when you copy and paste the url from some other places.
%E2%80%8B in url encoded form and ​ without encoded.
Remove it and browser will recognize it as a valid url.
console.log(encodeURIComponent('h​ttp://localhost:3000/api/teddies/order'))
The url http://127.0.0.1:5500/ is added because browser doesn't detect the supplied url as a valid url hence consider it as a path and prepend your current url to it.

Azure Data Lake Gen2 PUT authorization

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

Django - Sending POST request from javascript

I have a JS event-triggered function which goal is send a POST request in order to update some objects in my database. The event which triggers the function is a drop-event, so i initially avoided using forms to pass my request, but tell me if i did wrong.
Big Edit:
I found that my mistake was to not include the csrf_token on my post request.
However, i still have an error: my post request comes in empty when i do print(request.POST) on my django view.
My JS file:
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
var csrftoken = getCookie('csrftoken');
const dragDrop = function (e) {
e.preventDefault()
const droppedElId = e.dataTransfer.getData('Text/html').split('__id-')[1]
const request = new XMLHttpRequest()
request.open('POST', '', true)
request.setRequestHeader('X-CSRFToken', csrftoken)
request.setRequestHeader('Content-Type', 'application/json')
// request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8')
request.send(JSON.stringify({
"request_name":"change-extra-fields",
"type":"increase",
"id":droppedElId,
}))
}
The query-dict of the request.POST is empty when i do this. However, the request works if i change the Content-Type header to application/x-www-form-urlencoded, but it puts everything on the same key.
Example:
Result with 'application/json':
<QueryDict: {}>
Result with 'application/x-www-form-urlencoded':
<QueryDict: {'{"request_name":"change-extra-fields","type":"increase","id":"8"}': ['']}>
Anyways, i think that 'application/json' should be working and i have no idea why it isn't..
There is a typo I think
request.setRequestHeader('Content-Type', 'application/json');
As you mentioned in comments, your post request required you to be authenticated.
So, you first need to authenticate/login to the site(using another Ajax call perhaps). If the site supports jwt/api authentication you would get a token back which you have to send in attached with header in next (post)request. it would be something like this
xhr.setRequestHeader('Authorization', 'Bearer arandombereartoken');
if the site uses session/cookie authentication then I suggest consider using jQuery and its Ajax functions.
I this this(2nd one) should be helpful.
UPDATE:
if you want to get data as application/json you have to look in the body of the request
if request.method == "POST":
print(request.body)
this would give you a byte object. you have load it to a json if you want json. request.POST is only for Content-Type 'application/x-www-form-urlencoded'

How to enable CORS in an Azure App Registration when used in an OAuth Authorization Flow with PKCE?

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

Categories

Resources