angular / angular.js

AngularJS - HTML enhanced for web apps!
https://angularjs.org
MIT License
58.81k stars 27.5k forks source link

Passing on username/password for XHR requests ($http) #1678

Open reezer opened 11 years ago

reezer commented 11 years ago

Currently angular.js doesn't pass on username and password paramaters. However, the open method of XMLHttpRequest supports this natively, so it should be able to pass them on using $http.

http://www.w3.org/TR/XMLHttpRequest/#the-open()-method

udayms commented 11 years ago

This is a pretty important feature for making any secure calls to the server. Anybody working on this?

ketema commented 11 years ago

See http://wemadeyoulook.at/blogg/implementing-basic-http-authentication-http-requests-angular/

wojtysim commented 11 years ago

@ketema The solution is nice but it does not cover whole spectrum of issue. Using xhr.open(...,username,password) causes browser to log in which is very useful for file downloads that need authentication as well. For example jQuery avails for that. You can set username and password in $ajax and you are good to go. Having such possibility in Angular will give it much more flexibility. Especially that it is hard/impossible to extend angulars $http.

defrex commented 10 years ago

To add another point here, these arguments are not just used for Basic Auth. Digest Auth is much more complicated to implement client-side, and the browser implementations are surely more secure, especially in browsers without access to window.crypto.getRandomValues.

defrex commented 10 years ago

I now have a working JS-based implementation of Digest Auth with $http. However, I've learned that it's impossible to prevent the browser from prompting the user for credentials when it receives the 401 challenge response.

This means Digest Auth is effectively impossible with Angular (or at least with $http).

SchizoDuckie commented 9 years ago

I've been implementing several api's for torrent web clients, and I'm now hitting angular's limits on this.

One perfect example of where this is starting to get problematic for instance is Tixati's web client authentication. It's a properly coded http digest authentication with authorization headers like this:

Authorization: Digest username="admin", realm="Tixati Web Interface", nonce="36a50e777c44c8be9be5f4b0daf23d02", uri="/home", response="29c38d686f991f56284c6287099d3c8f", opaque="52d82fd2372fcac187e8ae588ae01acd", qop=auth, nc=00000007, cnonce="1467dd8dbd04f966"

As you can see, there' s nonces, cnonces, and these are all rotating and would need manual handling, juggling and validating. i've looked at the specs, and that doesn't sound like a fun job.

Sure, there are some solutions for authentication in angular now, but they are all basically a very hacky way to work around this:

Crazy, since thereis beautiful support built right into the core of XMLHTTPRequest, and both these methods regularly force chrome's http authentication dialogs to pop up.

What would be a proper way to go forward on this? There is already a 'withCredentials' parameter in the function signature, Would the way to go be to add a 'credentials' parameter to the signature as well, and use those? Does anybody have an idea of how many unit tests this would touch?

SchizoDuckie commented 9 years ago

I just fixed this, in the in my opinion only correct way after consulting on #angularjs, by reimplementing $httpBackend with a patch that checks if headers.Authorization is an array with 2 items (username, password)

It's not the prettiest, but beats forking and will work in existing projects. It would really be awesome if we could implement something like this in the core and resolve this bug. The API is pretty elegant.

/**
 * A special decorator that modifies $httpBackend with a basically 1:1 copy of it, with one big twist.
 * it fixes angular bug #1678 ( https://github.com/angular/angular.js/issues/1678 )
 * Passing on username/password for XHR requests ($http)
 *
 * Interface:
 * Instead of passing a text HTTP Authorization header for authenticated requests,
 * pass it a username and a password in an array
 *
 * Example:
 * $http.get('http://myUrl/endpoint', { headers: { Authorization: ['admin','password']}});
 *
 */

app.config(function($provide) {
    $provide.decorator('$httpBackend', function($delegate, $browser, $window, $document) {

        function createXhr() {
            return new window.XMLHttpRequest();
        }

        function createHttpAuthBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) {
            // TODO(vojta): fix the signature
            return function(method, url, post, callback, headers, timeout, withCredentials, responseType) {
                function noop() {}

                function isPromiseLike(obj) {
                    return obj && isFunction(obj.then);
                }

                function isDefined(value) {
                    return typeof value !== 'undefined';
                }

                function isFunction(value) {
                    return typeof value === 'function';
                }

                function addEventListenerFn(element, type, fn) {
                    element.addEventListener(type, fn, false);
                }

                function removeEventListenerFn(element, type, fn) {
                    element.removeEventListener(type, fn, false);
                }

                // had to embed some of angular's privates and wanted to touch the original code as little as possible, hence the with
                with(angular) {

                    $browser.$$incOutstandingRequestCount();
                    url = url || $browser.url();

                    if (lowercase(method) == 'jsonp') {
                        var callbackId = '_' + (callbacks.counter++).toString(36);
                        callbacks[callbackId] = function(data) {
                            callbacks[callbackId].data = data;
                            callbacks[callbackId].called = true;
                        };

                        var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId),
                            callbackId, function(status, text) {
                                completeRequest(callback, status, callbacks[callbackId].data, "", text);
                                callbacks[callbackId] = noop;
                            });
                    } else {

                        var xhr = createXhr();

                        if (('Authorization' in headers) && headers.Authorization.length == 2) {
                            xhr.open(method, url, true, headers.Authorization[0], headers.Authorization[1]);
                            delete headers.Authorization;
                        } else {
                            xhr.open(method, url, true);
                        }

                        forEach(headers, function(value, key) {
                            if (isDefined(value)) {
                                xhr.setRequestHeader(key, value);
                            }
                        });

                        xhr.onload = function requestLoaded() {
                            var statusText = xhr.statusText || '';

                            // responseText is the old-school way of retrieving response (supported by IE8 & 9)
                            // response/responseType properties were introduced in XHR Level2 spec (supported by IE10)
                            var response = ('response' in xhr) ? xhr.response : xhr.responseText;

                            // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)
                            var status = xhr.status === 1223 ? 204 : xhr.status;

                            // fix status code when it is 0 (0 status is undocumented).
                            // Occurs when accessing file resources or on Android 4.1 stock browser
                            // while retrieving files from application cache.
                            if (status === 0) {
                                status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0;
                            }

                            completeRequest(callback,
                                status,
                                response,
                                xhr.getAllResponseHeaders(),
                                statusText);
                        };

                        var requestError = function() {
                            // The response is always empty
                            // See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error
                            completeRequest(callback, -1, null, null, '');
                        };

                        xhr.onerror = requestError;
                        xhr.onabort = requestError;

                        if (withCredentials) {
                            xhr.withCredentials = true;
                        }

                        if (responseType) {
                            try {
                                xhr.responseType = responseType;
                            } catch (e) {
                                // WebKit added support for the json responseType value on 09/03/2013
                                // https://bugs.webkit.org/show_bug.cgi?id=73648. Versions of Safari prior to 7 are
                                // known to throw when setting the value "json" as the response type. Other older
                                // browsers implementing the responseType
                                //
                                // The json response type can be ignored if not supported, because JSON payloads are
                                // parsed on the client-side regardless.
                                if (responseType !== 'json') {
                                    throw e;
                                }
                            }
                        }

                        xhr.send(post);
                    }

                    if (timeout > 0) {
                        var timeoutId = $browserDefer(timeoutRequest, timeout);
                    } else if (isPromiseLike(timeout)) {
                        timeout.then(timeoutRequest);
                    }

                    function timeoutRequest() {
                        jsonpDone && jsonpDone();
                        xhr && xhr.abort();
                    }

                    function completeRequest(callback, status, response, headersString, statusText) {
                        // cancel timeout and subsequent timeout promise resolution
                        if (timeoutId !== undefined) {
                            $browserDefer.cancel(timeoutId);
                        }
                        jsonpDone = xhr = null;

                        callback(status, response, headersString, statusText);
                        $browser.$$completeOutstandingRequest(noop);
                    }
                };

                function jsonpReq(url, callbackId, done) {
                    // we can't use jQuery/jqLite here because jQuery does crazy stuff with script elements, e.g.:
                    // - fetches local scripts via XHR and evals them
                    // - adds and immediately removes script elements from the document
                    var script = rawDocument.createElement('script'),
                        callback = null;
                    script.type = "text/javascript";
                    script.src = url;
                    script.async = true;

                    callback = function(event) {
                        removeEventListenerFn(script, "load", callback);
                        removeEventListenerFn(script, "error", callback);
                        rawDocument.body.removeChild(script);
                        script = null;
                        var status = -1;
                        var text = "unknown";

                        if (event) {
                            if (event.type === "load" && !callbacks[callbackId].called) {
                                event = {
                                    type: "error"
                                };
                            }
                            text = event.type;
                            status = event.type === "error" ? 404 : 200;
                        }

                        if (done) {
                            done(status, text);
                        }
                    };

                    addEventListenerFn(script, "load", callback);
                    addEventListenerFn(script, "error", callback);
                    rawDocument.body.appendChild(script);
                    return callback;
                }
            };
        }
        return createHttpAuthBackend($browser, createXhr, $browser.defer, $window.angular.callbacks, $document[0]);
    });
});
gkalpak commented 8 years ago

Having an Authorization header be an array and translates to something else (that is not a header), sounds more complicated than necessary.

Why not introduce a username and a password configuration option to the $http config object? Seems more straightforward:

$http({
  method: 'GET',
  url: '/foo/bar',
  username: 'baz',
  password: 'qux'
});

// or

$http.get('/foo/bar', {
  username: 'baz',
  password: 'qux'
});