84

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?

Begueradj
  • 547
  • 7
  • 19
Ragas
  • 3,005
  • 6
  • 25
  • 42
  • The API should deliver the image as a static asset. This shouldn't require the JWT Authorization. – Ma Kobi Oct 09 '17 at 09:24
  • 23
    @MaKobi That's a big assumption about the use case. OP, some related reading and potential solutions in this question: https://stackoverflow.com/questions/34096744/how-should-i-load-images-if-i-use-token-based-authentication – CodingIntrigue Oct 09 '17 at 09:25
  • 8
    I need jwt authorization for images because they are private and only authenticated users should be able to see them – Ragas Oct 09 '17 at 09:30
  • You can use [vue-auth-image](https://www.npmjs.com/package/vue-auth-image) plugin in case you develop with Vue.js – Begueradj Mar 28 '19 at 11:06
  • to @Marc: _"..is ... a design flaw that JWTs aren't just normal cookies.."_ it is neither a HTTP header. And **It is possible to store JWT in cookies**, but be aware of the related security threats (one can start reading about this from this SO question: https://stackoverflow.com/q/27067251/1115187). – maxkoryukov Sep 24 '22 at 08:23

8 Answers8

30

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.

Tapas
  • 1,123
  • 8
  • 16
12

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.

arcol
  • 1,510
  • 1
  • 15
  • 20
  • When I tried this one it's giving error on requestOptions – Dev Gaud Jun 17 '20 at 09:42
  • 2
    This is a clever solution: map jwt to a proper web standard (cookies) on the client and unmap them on the backend! – Marc Dec 13 '20 at 08:34
  • 1
    Good solution indeed. As a reminder, you have to add the cookie-parser middleware to express.js for this to work. Otherwise, req.cookies will be undefined – Sebastian Dec 21 '20 at 22:09
9

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:

Nahuel Greco
  • 1,080
  • 14
  • 12
8

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">

Luis F.
  • 1,222
  • 1
  • 11
  • 12
  • It can be simplified by adding the token to authorization headers (`axios.defaults.headers["Authorization"] = "Bearer " + access_token`), than you don't need to append it to the urls (just check it on the server). I've used this before; the problem is that it feels noticeably slower then the native browser implementation ``. (Haven't done any benchmarks, though.) – HynekS May 29 '22 at 14:43
6

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

DotBert
  • 1,262
  • 2
  • 16
  • 29
2
<img src="/api/images/yourimage.jpg?token=here-your-token">

In the backend you validate JWT from queryparam.

Alexys
  • 156
  • 1
  • 5
  • 7
    But then your token is sent in plain text, isn't it? – Shafique Jamal Jan 27 '18 at 16:14
  • 8
    @ShafiqueJamal even if you include JWT in HTTP header it is still sent in plaintext. HTTPS is always a requirement when working with access token and the like from browser – Nagi Mar 29 '18 at 04:04
  • 42
    I don't recommend this, the token is like a password, if your user copies it and then gives it to someone else then they can see the image. If the user is malicious they can then use that token to get more information from the original user. – Exocomp Sep 02 '18 at 23:40
  • 18
    The JWT is in a cookie, or in a localstorage. If the user is malicious the can then user that token in any moment. Then the JWT is not the problem. – Alexys Oct 28 '18 at 23:02
  • @Nagi With https your headers are also encrypted so they are not in plaintext as long as you use https. – Mike Dec 29 '18 at 22:46
  • @Mike sure, that's why I wrote HTTPS is required. – Nagi Jan 02 '19 at 07:08
  • 6
    Possible risk: user rightclicks the image and opens it in another tab on a public computer. He leaves the computer. The attacker enter te computer and starts typing the base url (which is not secret) which is autocompleted by the browser with the param containing the jwt. – Jos Mar 05 '19 at 12:39
  • 3
    As @Exocomp is saying this is unsecure, the developer can improve the security if he reduces the token scope. – motia Sep 06 '19 at 00:14
  • 1
    @motia There are absolutely no security improvements in reducing token scopes since user's privacy is still at risk. As a rule of thumb, never make an assumption on security-related concepts. – Farzan Oct 24 '19 at 02:11
2

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

0

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>

  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Dec 31 '21 at 04:45