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
How can I retrieve an API-secret-key from Azure Key Vault URI (https://.vault.azure.net/secrets/Example ) and pass it to apiKey var in pure javascript code, not in node.js?
Can I use something like this:
var authorizationEndpoint = "https://<keyvault-
name>.vault.azure.net/secrets/Eg";
function RequestAuthorizationToken() {
if (authorizationEndpoint) {
var a = new XMLHttpRequest();
a.open("GET", authorizationEndpoint);
a.setRequestHeader("Authorization", "Bearer " + accessToken);
a.setRequestHeader("Content-Type", "application/json");
a.onload = function () {
var response = JSON.parse(xhr.responseText);
window.apiKey = response.value;
};
a.send();
}
}
I tried to reproduce the same in my environment via Postman and got below results:
I have one secret named srisecret in my key vault like below:
I registered one Azure AD application and added API permission for key vault like below:
Make sure to give get secret permission for your service principal in keyvault access policies like below:
Now, I generated one access token via Postman to access Key vault with below parameters:
POST https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/token
grant_type:client_credentials
client_id: <appID>
client_secret: <secret_value>
scope: https://vault.azure.net/.default
Response:
When I used above token to get keyvault secret, I got it successfully like below:
GET https://keyvaultname.vault.azure.net/secrets/<secretname>/<secretversion>?api-version=7.3
Authorization: Bearer <token>
Response:
Alternatively, you can use below c# code in fetching key vault secret:
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
namespace KVSecret
{
class Secret
{
static void Main(string[] args)
{
const string clientId = "appID";
const string tenantId = "tenantID";
const string clientSecret = "secret";
var credentials = new ClientSecretCredential(tenantId, clientId, clientSecret);
var client = new SecretClient(new Uri("https://yourkvname.vault.azure.net"), credentials);
var secret = client.GetSecretAsync("yoursecretname").GetAwaiter().GetResult();
Console.WriteLine(secret.Value.Value);
}
}
}
Response:
The mentioned php link in comments is for fetching keyvault secrets via managed identities.
For this, you need to enable system-assigned managed identity like below:
You need to give get secret permission to this managed identity by creating access policy like below:
Now, you can run below php code by installing az-keyvault-php package:
$secret = new AzKeyVault\Secret('https://yourkvname.vault.azure.net');
$value = $secret->getSecret('yoursecretname');
echo $value->secret;
I'm building a chrome extension where I get a file as input from the user and pass it to my background.js (service worker in case of manifest v3) to save it to my backend. Since making cross-origin requests are blocked from content scripts I have to pass the same to my background.js and use FETCH API to save the file. When I pass the FormData or File Object to the chrome.runtime.sendMessage API it uses JSON Serialization and what I receive in my background.js is an empty object. Refer to the below snippet.
//content-script.js
attachFile(event) {
let file = event.target.files[0];
// file has `File` object uploaded by the user with required contents.
chrome.runtime.sendMessage({ message: 'saveAttachment', attachment: file });
}
//background.js
chrome.runtime.onMessage.addListener((request, sender) => {
if (request.message === 'saveAttachment') {
let file = request.attachment; //here the value will be a plain object {}
}
});
The same happens even when we pass the FormData from the content script.
I referred to multiple solutions suggested by the old StackOverflow questions, to use URL.createObjectURL(myfile); and pass the URL to my background.js and fetch the same file. Whereas FETCH API does not support blob URL to fetch and also XMLHttpRequest is not supported in service worker as recommended here. Can someone help me in solving this? Am so blocked with this behaviour.
Currently only Firefox can transfer such types directly. Chrome might be able to do it in the future.
Workaround 1.
Serialize the object's contents manually to a string, send it, possibly in several messages if the length exceeds 64MB message size limit, then rebuild the object in the background script. Below is a simplified example without splitting, adapted from Violentmonkey. It's rather slow (encoding and decoding of 50MB takes several seconds) so you may want to write your own version that builds a multipart/form-data string in the content script and send it directly in the background script's fetch.
content script:
async function serialize(src) {
const wasBlob = src instanceof Blob;
const blob = wasBlob ? src : await new Response(src).blob();
const reader = new FileReader();
return new Promise(resolve => {
reader.onload = () => resolve([
reader.result,
blob.type,
wasBlob,
]);
reader.readAsDataURL(blob);
});
}
background script, inside onMessage listener:
const [body, type] = deserialize(message.body);
fetch(message.url, {
body,
headers: {
'Content-Type': type,
},
}).then(/*........*/);
function deserialize([base64, type, wasBlob]) {
const str = atob(base64.slice(base64.indexOf(',') + 1));
const len = str.length;
const arr = new Uint8Array(len);
for (let i = 0; i < len; i += 1) arr[i] = str.charCodeAt(i);
if (!wasBlob) {
type = base64.match(/^data:(.+?);base64/)[1].replace(/(boundary=)[^;]+/,
(_, p1) => p1 + String.fromCharCode(...arr.slice(2, arr.indexOf(13))));
}
return [arr, type];
}
Workaround 2.
Use an iframe for an html file in your extension exposed via web_accessible_resources.
The iframe will be able to do everything an extension can, like making a CORS request.
The File/Blob and other cloneable types can be transferred directly from the content script via postMessage. FormData is not clonable, but you can pass it as [...obj] and then assemble in new FormData() object.
It can also pass the data directly to the background script via navigator.serviceWorker messaging.
Example: see "Web messaging (two-way MessagePort)" in that answer.
I have a better solution: you can actually store Blob in the IndexedDB.
// client side (browser action or any page)
import { openDB } from 'idb';
const db = await openDB('upload', 1, {
upgrade(openedDB) {
openedDB.createObjectStore('files', {
keyPath: 'id',
autoIncrement: true,
});
},
});
await db.clear('files');
const fileID = await db.add('files', {
uploadURL: 'https://yours3bucketendpoint',
blob: file,
});
navigator.serviceWorker.controller.postMessage({
type: 'UPLOAD_MY_FILE_PLEASE',
payload: { fileID }
});
// Background Service worker
addEventListener('message', async (messageEvent) => {
if (messageEvent.data?.type === 'UPLOAD_MY_FILE_PLEASE') {
const db = await openDB('upload', 1);
const file = await db.get('files', messageEvent.data?.payload?.fileID);
const blob = file.blob;
const uploadURL = file.uploadURL;
// it's important here to use self.fetch
// so the service worker stays alive as long as the request is not finished
const response = await self.fetch(uploadURL, {
method: 'put',
body: blob,
});
if (response.ok) {
// Bravo!
}
}
});
I found another way to pass files from a content page (or from a popup page) to a service worker.
But, probably, it is not suitable for all situations,
You can intercept a fetch request sent from a content or popup page in a service worker. Then you can send this request through the service-worker, it can also be modified somehow
popup.js:
// simple fetch, but with a header indicating that the request should be intercepted
fetch(url, {
headers: {
'Some-Marker': 'true',
},
});
background.js:
self.addEventListener('fetch', (event) => {
// You can check that the request should be intercepted in other ways, for example, by the request URL
if (event.request.headers.get('Some-Marker')) {
event.respondWith((async () => {
// event.request contains data from the original fetch that was sent from the content or popup page.
// Here we make a request already in the background.js (service-worker page) and then we send the response to the content page, if it is still active
// Also here you can modify the request hoy you want
const result = await self.fetch(event.request);
return result;
})());
}
return null;
});
I am trying to upload file from Apex to Sharepoint but getting error as '400 Bad Request'.But work from JS CODE. Following is my code snippet :
Apex Code
Http http = new Http();
HttpRequest httpRequestToSend = new HttpRequest();
httpRequestToSend.setEndpoint('https://sample.sharepoint.com/sites/siteName/_api/web/GetFolderByServerRelativeUrl(\''+'/sites/siteName/Shared Documents'+'\')/Files/Add(url=\''+'document3.txt'+'\', overwrite=true)');
httpRequestToSend.setMethod('POST');
httpRequestToSend.setHeader('Authorization', 'Bearer ' + token);
httpRequestToSend.setHeader('Content-Type','application/json; odata=verbose');
httpRequestToSend.setBodyAsBlob(Blob.ValueOf('test Message'));
System.debug('***** httpRequestToSend-->' + httpRequestToSend);
Http http1 = new Http();
HttpResponse httpResponse1 = http1.send(httpRequestToSend);
System.debug('***** httpResponse-->' + httpResponse1.toString());
System.debug(httpResponse1.getBody());
JS CODE
var myHeaders = new Headers();
myHeaders.append("Authorization", "Bearer " + Token);
myHeaders.append("Content-Type", "application/json;odata=verbose");
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: fileBuffer,
};
fetch('https://sample.sharepoint.com/sites/siteName/_api/web/GetFolderByServerRelativeUrl(\'/sites/siteName/Shared Documents\')/Files/Add(url=\'test.txt\', overwrite=true)', requestOptions)
.then(response => response.text())
.then(result => console.log(result))
.catch(error => alert('error', error));
}
Thankyou
As per the documentation salesforce can't communicate to FTP directly that means can't Read/Write file at FTP Server.
I also faced this issue and this is how I resolved it:-
Step1: Create & host an External API on in any language(C#, Python) that takes two parameters one as fileName and the other one as fileData and uploads that file.
Step2: At Salesforce end, consume that API using HttpRequest and pass your file as filedata and fileName.
public void uploadFileToFTP_Service(string fileName,string fileData)
{
string value='{"fileName":"'+fileName+'","fileData": "'+fileData+'"}';
HttpRequest req = new HttpRequest();
req.setEndpoint('http://yourhostedApIPath:9001/data');
req.setMethod('POST');
req.setTimeout(120000);
req.setHeader('content-type','application/json; charset=utf-8');
req.setBody(value);
Http http = new Http();
HttpResponse res = http.send(req);
system.debug('Status code: ' + res.getStatusCode());
}
I was beating my head on this for some time today, and was finally able to get past the "400 Bad Request" error by escaping the space characters in my relative url path (I expect the filename may need the same workaround).
Each space character (" ") must be replaced with the ASCII reference "%20"
so in your example, the endpoint url:
'https://sample.sharepoint.com/sites/siteName/_api/web/GetFolderByServerRelativeUrl(\'/sites/siteName/Shared Documents\')/Files/Add(url=\'test.txt\', overwrite=true)'
should be changed to:
'https://sample.sharepoint.com/sites/siteName/_api/web/GetFolderByServerRelativeUrl(\'/sites/siteName/Shared%20Documents\')/Files/Add(url=\'test.txt\', overwrite=true)'
At least in my case, this corrected the issue.
I'm trying to add a user who clicks on a button in a SharePoint (online) site to a Office 365 group. I know this can be done via JSON using the Add Member API.
https://github.com/OfficeDev/microsoft-graph-docs/blob/master/api-reference/v1.0/api/group_post_members.md
I am however really inexperienced when it comes to JSON and keep messing up the POST function. This is the code I have currently, everything before the comma has been working fine.
function showButton() {
$('btn-1').on('click', function(event) {
var userProfileProperties
var clientContext = new SP.ClientContext.get_current();
var peopleManager = new SP.UserProfiles.PeopleManager(clientContext);
userProfileProperties = peopleManager.getMyProperties();
clientContext.load(userProfileProperties);
clientContext.executeQueryAsync(onSuccess, onFail);
function onSuccess(){
accountProperties = userProfileProperties.get_userProfileProperties();
accountId = accountProperties['msOnline-ObjectId'];
//JSON Query
jQuery.ajax({
url: "https://mysite.sharepoint.com/groups/groupID/members/$ref";
method: "POST";
contentType: "application/json";
dataType: 'json',
{
"#odata.id": "https://graph.microsoft.com/v1.0/directoryObjects/" + accountId
};
});
};
function onFail(){
alert(failed);
};
});
};
In your document , you will find authentication token is required in the Request headers .Without authentication token ,you will receive an error as :
"code": "InvalidAuthenticationToken", "message": "Bearer access token is empty."
As a solution , you could try following steps :
1.Register a javascript Application in Azure AD and configure your app to allow the OAuth 2.0 implicit grant flow.Tokens are obtained using the OAuth 2.0 implicit grant flow. Using implicit grant, your application requests an access token from Azure AD for the currently signed-in user by sending the user to an authorization URL where the user signs in with their Office 365 credentials and then is redirected back to the app with the access token in the URL .
2.Add permissions to Graph API .
3.Add an html page to your sharepoint online(using Explorer mode) .
4.Edit the html , write below function to get an access token:
function requestToken() {
// Change clientId and replyUrl to reflect your app's values
// found on the Configure tab in the Azure Management Portal.
// Also change {your_subdomain} to your subdomain for both endpointUrl and resource.
var clientId = '3dadb44e-feaa-4158-90f5-e129e15db66d';//ID of your App in Azure
var replyUrl = 'https://o365e3w15.sharepoint.com/sites/XXX/app.html'; //my sharepoint page that requests
//an oauth 2 authentification and data
//It is also referenced in the REPLY URL field of my App in Azure
var endpointUrl = 'https://graph.microsoft.com/v1.0/me/messages';
var resource = "https://graph.microsoft.com/";
var authServer = 'https://login.windows.net/common/oauth2/authorize?';
//var authServer = 'https://login.microsoftonline.com/common/oauth2/authorize?';//this works either
var responseType = 'token';
var url = authServer +
"response_type=" + encodeURI(responseType) + "&" +
"client_id=" + encodeURI(clientId) + "&" +
"resource=" + encodeURI(resource) + "&" +
"redirect_uri=" + encodeURI(replyUrl);
window.location = url;
}
After that ,you could make an ajax call to graph api endpoint to get/post request, for example, get current user's messages:
var endpointUrl = "https://graph.microsoft.com/v1.0/me/messages";
var xhr = new XMLHttpRequest();
xhr.open("GET", endpointUrl);
var myToken = getToken();
// The APIs require an OAuth access token in the Authorization header, formatted like this:
//'Authorization: Bearer <token>'.
xhr.setRequestHeader("Authorization", "Bearer " + myToken);
// Process the response from the API.
xhr.onload = function () {
if (xhr.status == 200) {
//alert('data received');
var message="";
var object = JSON.parse(xhr.response);
for(i=0;i<object.value.length;i++){
message+='Subject: ' + object.value[i].subject + '<br>';
}
document.getElementById("results").innerHTML = message;
} else { }
}
// Make request.
xhr.send();
display this app.html into any SharePoint Webpart page by calling it within an iframe tag.
All detail steps you will find in this article , i have tested and work fine in my side .
I am building an application that consumes the Caspio API. I am having some trouble authenticating against their API. I have spent 2-3 days trying to figure this out but it may be due to some understanding on my end. I have read countless articles on stackoverflow post and otherwise but have not solved the issue. Below is a code example of my solution based on what i have looked at and i am getting a 400 Status code message; What am i doing wrong here? (Please provide well commented code example and i would prefer to NOT have links posted here referencing other material as i have looked at these extensively. Thanks!):
Some references i have looked at:
1) Pure JavaScript code for HTTP Basic Authentication?
2) How to make http authentication in REST API call from javascript
I would like to use this authentication method as described by caspio below:
As an alternative to including credentials in the request body, a client can use the HTTP Basic authentication scheme. In this case, authentication request will be setup in the following way:
Method: POST
URL: Your token endpoint
Body: grant_type=client_credentials
Header parameter:
Authorization: Basic Basic authentication realm
Below are my Javascript and HTML code.
JavaScript:
var userName = "clientID";
var passWord = "secretKey";
function authenticateUser(user, password)
{
var token = user + ":" + password;
// Should i be encoding this value????? does it matter???
// Base64 Encoding -> btoa
var hash = btoa(token);
return "Basic " + hash;
}
function CallWebAPI() {
// New XMLHTTPRequest
var request = new XMLHttpRequest();
request.open("POST", "https://xxx123.caspio.com/oauth/token", false);
request.setRequestHeader("Authorization", authenticateUser(userName, passWord));
request.send();
// view request status
alert(request.status);
response.innerHTML = request.responseText;
}
HTML:
<div>
<div id="response">
</div>
<input type="button" class="btn btn-primary" value="Call Web API" onclick="javascript:CallWebAPI();" />
After Spending quite a bit of time looking into this, i came up with the solution for this; In this solution i am not using the Basic authentication but instead went with the oAuth authentication protocol. But to use Basic authentication you should be able to specify this in the "setHeaderRequest" with minimal changes to the rest of the code example. I hope this will be able to help someone else in the future:
var token_ // variable will store the token
var userName = "clientID"; // app clientID
var passWord = "secretKey"; // app clientSecret
var caspioTokenUrl = "https://xxx123.caspio.com/oauth/token"; // Your application token endpoint
var request = new XMLHttpRequest();
function getToken(url, clientID, clientSecret) {
var key;
request.open("POST", url, true);
request.setRequestHeader("Content-type", "application/json");
request.send("grant_type=client_credentials&client_id="+clientID+"&"+"client_secret="+clientSecret); // specify the credentials to receive the token on request
request.onreadystatechange = function () {
if (request.readyState == request.DONE) {
var response = request.responseText;
var obj = JSON.parse(response);
key = obj.access_token; //store the value of the accesstoken
token_ = key; // store token in your global variable "token_" or you could simply return the value of the access token from the function
}
}
}
// Get the token
getToken(caspioTokenUrl, userName, passWord);
If you are using the Caspio REST API on some request it may be imperative that you to encode the paramaters for certain request to your endpoint; see the Caspio documentation on this issue;
NOTE: encodedParams is NOT used in this example but was used in my solution.
Now that you have the token stored from the token endpoint you should be able to successfully authenticate for subsequent request from the caspio resource endpoint for your application
function CallWebAPI() {
var request_ = new XMLHttpRequest();
var encodedParams = encodeURIComponent(params);
request_.open("GET", "https://xxx123.caspio.com/rest/v1/tables/", true);
request_.setRequestHeader("Authorization", "Bearer "+ token_);
request_.send();
request_.onreadystatechange = function () {
if (request_.readyState == 4 && request_.status == 200) {
var response = request_.responseText;
var obj = JSON.parse(response);
// handle data as needed...
}
}
}
This solution does only considers how to successfully make the authenticated request using the Caspio API in pure javascript. There are still many flaws i am sure...
Today we use Bearer token more often that Basic Authentication but if you want to have Basic Authentication first to get Bearer token then there is a couple ways:
const request = new XMLHttpRequest();
request.open('GET', url, false, username,password)
request.onreadystatechange = function() {
// D some business logics here if you receive return
if(request.readyState === 4 && request.status === 200) {
console.log(request.responseText);
}
}
request.send()
Full syntax is here
Second Approach using Ajax:
$.ajax
({
type: "GET",
url: "abc.xyz",
dataType: 'json',
async: false,
username: "username",
password: "password",
data: '{ "key":"sample" }',
success: function (){
alert('Thanks for your up vote!');
}
});
Hopefully, this provides you a hint where to start API calls with JS. In Frameworks like Angular, React, etc there are more powerful ways to make API call with Basic Authentication or Oauth Authentication. Just explore it.
To bring this question up to date, a node.js solution (using node-fetch) would be as follows:
const auth = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
fetch("https://some-oauth2.server.com/connect/token", {
method: "POST",
body: "grant_type=client_credentials",
headers: {
"Content-type": "application/x-www-form-urlencoded",
Authorization: `Basic ${auth}`,
},
})
.then((response) => response.json())
.then((response) => {
console.log(response); //response.access_token is bearer token, response.expires_in is lifetime of token
});
Sensitive requests like this should be server-to-server, and keeping the credential details in the Header rather than QueryString means it's less likely to be visible in web server logs
EncodedParams variable is redefined as params variable will not work. You need to have same predefined call to variable, otherwise it looks possible with a little more work. Cheers! json is not used to its full capabilities in php there are better ways to call json which I don't recall at the moment.
change var to const for the username, password, token_, and key variables.