johnbrett / hapi-auth-bearer-token

Simple Bearer authentication scheme plugin for hapi, accepts token by Header, Cookie or Query parameter.
MIT License
218 stars 46 forks source link

Can't use auth with CORS #19

Closed jgauna closed 9 years ago

jgauna commented 9 years ago

Here's my route:

    syncImage: {
        handler: function(req, reply) {
            server.methods.uploadImage(req, reply);
        },
        payload:{
            maxBytes:209715200,
            output:'stream',
            parse: false
        },
        auth: 'simple',
        cors: {
            origin: ['*'],
            credentials: true,
            matchOrigin: false
        },
        id: 'syncImage'
    },

And in the client:

            var form = document.getElementById("imageForm");
            var formData = new FormData(form);
            var imageInput = document.getElementById("imageInput");
            var file = imageInput.files[0];
            formData.append('file', file);

            var xhr = new XMLHttpRequest();
            if ('withCredentials' in xhr) {
              xhr.open('POST', form.getAttribute('action'), true);
              xhr.setRequestHeader("Authorization", "Bearer 1234");
              xhr.withCredentials = "true";
              xhr.onreadystatechange = function (aEvt) {
                if (xhr.readyState == 4) {
                   if(xhr.status == 200) {
                      if (xhr.responseText === 'isFileOk') {
                        isFileUploaded = true;
                        imagePath = 'http://localhost:3000/uploads/' + file.name;
                        $('#saveOrder').removeAttr('disabled');
                        $('#wrapper').css('display', 'none');  
                      }
                   } else {
                    isFileUploaded = false;
                     alert("Error !");
                   }
                }
              }
              xhr.send(formData);
              return false;
            }

Am I missing something?

Regards

johnbrett commented 9 years ago

Hey @jgauna, I use this plugin with CORS all the time so it definitely supports it :)

What response are you getting back from the api when this xhr returns?

jgauna commented 9 years ago

I'm getting

XMLHttpRequest cannot load http://192.168.0.21:3000/api/syncImage. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access. The response had HTTP status code 404. 

And the call

Remote Address:192.168.0.21:3000
Request URL:http://192.168.0.21:3000/api/syncImage
Request Method:OPTIONS
Status Code:404 Not Found
Request Headersview source
Accept:*/*
Accept-Encoding:gzip,deflate,sdch
Accept-Language:es-419,es;q=0.8
Access-Control-Request-Headers:authorization, content-type
Access-Control-Request-Method:POST
Connection:keep-alive
Host:192.168.0.21:3000
Origin:null
User-Agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36
Response Headersview source
cache-control:no-cache
Connection:keep-alive
content-encoding:gzip
content-type:application/json; charset=utf-8
Date:Tue, 31 Mar 2015 14:26:53 GMT
Transfer-Encoding:chunked
vary:accept-encoding

The odd thing is that if do not use this in my ajax call it works.

server

auth: 'simple'

ajax call

  xhr.setRequestHeader("Authorization", "Bearer 1234");
  xhr.withCredentials = "true";

On the other side if the domain is the same it works great with authorization.

Any ideas?

johnbrett commented 9 years ago

Ok after looking into it in more details there seems to be two problems here.

  1. Adding custom headers launches a preflight OPTIONS request, for which you don't have a the OPTIONS route I imagine, thus the 404. This is solved by using a library like jQuery or you can handle this on the server with a catch all route like this:
server.route({
  path: '/{p*}',
  method: 'OPTIONS',
  handler: function(req, reply){
    console.log('test');
    reply({method: 'options'});
  },
  config: {
    auth: false,
    cors: true
  }
});
  1. xhr.withCredentials = "true" is causing the problem here too, I get the following error: A wildcard '*' cannot be used in the 'Access-Control-Allow-Origin' header when the credentials flag is true. Origin 'http://127.0.0.1:8080' is therefore not allowed access. in Chrome. I think this is used for basic auth so messing up the headers. After removing that line form the request everything worked for me.

Really I'd advise using a library for your xhr's though as they abstract away having to deal with the different browser quirks around xhr's. Let me know how you get on!

jgauna commented 9 years ago

I can't make it work yet. Here's my full code:

As it's right now I'm getting: XMLHttpRequest cannot load http://192.168.0.21:3000/api/syncImage. Invalid HTTP status code 401

Route code (Just know I'm using a boilerplate and maybe the hapi code looks odd but it's ok)

    syncImage: {
        handler: function(req, reply) {
            console.log("UPLOADING ......");
            server.methods.uploadImage(req, reply);
        },
        payload:{
            maxBytes:209715200,
            output:'stream',
            parse: false
        },
        auth: 'simple',
        cors: {
            origin: ['*'],
            credentials: true
        },
        id: 'syncImage'
    }

And the client side:

            var cvs = document.createElement('canvas');
            cvs.width = compressed_img_obj.naturalWidth;
            cvs.height = compressed_img_obj.naturalHeight;
            var ctx = cvs.getContext("2d").drawImage(compressed_img_obj, 0, 0);

            //ADD sendAsBinary compatibilty to older browsers
            if (XMLHttpRequest.prototype.sendAsBinary === undefined) {
                XMLHttpRequest.prototype.sendAsBinary = function(string) {
                    var bytes = Array.prototype.map.call(string, function(c) {
                        return c.charCodeAt(0) & 0xff;
                    });
                    this.send(new Uint8Array(bytes).buffer);
                };
            }

            var type = "image/jpeg";
            if(filename.substr(-4)==".png"){
                type = "image/png";
            }

            var data = cvs.toDataURL(type);
            data = data.replace('data:' + type + ';base64,', '');

            var xhr = new XMLHttpRequest();
            xhr.open('OPTIONS', 'http://192.168.0.21:3000/api/syncImage', true);
            xhr.setRequestHeader("Authorization", "Bearer 1234");
            xhr.withCredentials = true;
            var boundary = 'someboundary';

            xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);

            // Set custom request headers if customHeaders parameter is provided
            if (customHeaders && typeof customHeaders === "object") {
              for (var headerKey in customHeaders){
                xhr.setRequestHeader(headerKey, customHeaders[headerKey]);
              }
            }

            // If a duringCallback function is set as a parameter, call that to notify about the upload progress
            if (duringCallback && duringCallback instanceof Function) {
              xhr.onprogress = function (evt) {
                if (evt.lengthComputable) {  
                  return (evt.loaded / evt.total)*100;  
                }
              };
            }

            xhr.sendAsBinary(['--' + boundary, 'Content-Disposition: form-data; name="' + file_input_name + '"; filename="' + filename + '"', 'Content-Type: ' + type, '', atob(data), '--' + boundary + '--'].join('\r\n'));

            xhr.onreadystatechange = function() {
              if (this.readyState == 4){
                if (this.status == 200) {
                  successCallback(this.responseText);
                }else if (this.status >= 400) {
                  if (errorCallback &&  errorCallback instanceof Function) {
                    errorCallback(this.responseText);
                  }
                }
              }
            };

Extra info:

I really appreciate a lot your help !

johnbrett commented 9 years ago

Hey @jgauna I'm on holiday at the moment sorry for delay on this - will be back on the 18th if I don't get to it before then!!

jgauna commented 9 years ago

Thanks for the answer @johnbrett. I was able to fix it using:

// Create a new server
var server = new Hapi.Server({
  connections: {
    routes: {
      cors: true
    }
  }
});

Now, all routes will have cors available and working with Authorization headers :bowtie:

Have a nice holidays!

johnbrett commented 9 years ago

Hey @jgauna, glad you got sorted on this. Thanks for posting the solution also!