summaryrefslogtreecommitdiffstats
path: root/netwerk/test/unit/test_auth_multiple.js
diff options
context:
space:
mode:
Diffstat (limited to 'netwerk/test/unit/test_auth_multiple.js')
-rw-r--r--netwerk/test/unit/test_auth_multiple.js464
1 files changed, 464 insertions, 0 deletions
diff --git a/netwerk/test/unit/test_auth_multiple.js b/netwerk/test/unit/test_auth_multiple.js
new file mode 100644
index 0000000000..3cf039ea7d
--- /dev/null
+++ b/netwerk/test/unit/test_auth_multiple.js
@@ -0,0 +1,464 @@
+// This file tests authentication prompt callbacks
+// TODO NIT use do_check_eq(expected, actual) consistently, not sometimes eq(actual, expected)
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+// Turn off the authentication dialog blocking for this test.
+var prefs = Services.prefs;
+prefs.setIntPref("network.auth.subresource-http-auth-allow", 2);
+
+function URL(domain, path = "") {
+ if (path.startsWith("/")) {
+ path = path.substring(1);
+ }
+ return `http://${domain}:${httpserv.identity.primaryPort}/${path}`;
+}
+
+ChromeUtils.defineLazyGetter(this, "PORT", function () {
+ return httpserv.identity.primaryPort;
+});
+
+const FLAG_RETURN_FALSE = 1 << 0;
+const FLAG_WRONG_PASSWORD = 1 << 1;
+const FLAG_BOGUS_USER = 1 << 2;
+// const FLAG_PREVIOUS_FAILED = 1 << 3;
+const CROSS_ORIGIN = 1 << 4;
+// const FLAG_NO_REALM = 1 << 5;
+const FLAG_NON_ASCII_USER_PASSWORD = 1 << 6;
+
+function AuthPrompt1(flags) {
+ this.flags = flags;
+}
+
+AuthPrompt1.prototype = {
+ user: "guest",
+ pass: "guest",
+
+ expectedRealm: "secret",
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]),
+
+ prompt: function ap1_prompt(title, text, realm, save, defaultText, result) {
+ do_throw("unexpected prompt call");
+ },
+
+ promptUsernameAndPassword: function ap1_promptUP(
+ title,
+ text,
+ realm,
+ savePW,
+ user,
+ pw
+ ) {
+ if (!(this.flags & CROSS_ORIGIN)) {
+ if (!text.includes(this.expectedRealm)) {
+ do_throw("Text must indicate the realm");
+ }
+ } else if (text.includes(this.expectedRealm)) {
+ do_throw("There should not be realm for cross origin");
+ }
+ if (!text.includes("localhost")) {
+ do_throw("Text must indicate the hostname");
+ }
+ if (!text.includes(String(PORT))) {
+ do_throw("Text must indicate the port");
+ }
+ if (text.includes("-1")) {
+ do_throw("Text must contain negative numbers");
+ }
+
+ if (this.flags & FLAG_RETURN_FALSE) {
+ return false;
+ }
+
+ if (this.flags & FLAG_BOGUS_USER) {
+ this.user = "foo\nbar";
+ } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) {
+ this.user = "é";
+ }
+
+ user.value = this.user;
+ if (this.flags & FLAG_WRONG_PASSWORD) {
+ pw.value = this.pass + ".wrong";
+ // Now clear the flag to avoid an infinite loop
+ this.flags &= ~FLAG_WRONG_PASSWORD;
+ } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) {
+ pw.value = "é";
+ } else {
+ pw.value = this.pass;
+ }
+ return true;
+ },
+
+ promptPassword: function ap1_promptPW(title, text, realm, save, pwd) {
+ do_throw("unexpected promptPassword call");
+ },
+};
+
+function AuthPrompt2(flags) {
+ this.flags = flags;
+}
+
+AuthPrompt2.prototype = {
+ user: "guest",
+ pass: "guest",
+
+ expectedRealm: "secret",
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
+
+ promptAuth: function ap2_promptAuth(channel, level, authInfo) {
+ authInfo.username = this.user;
+ authInfo.password = this.pass;
+ return true;
+ },
+
+ asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+};
+
+function Requestor(flags, versions) {
+ this.flags = flags;
+ this.versions = versions;
+}
+
+Requestor.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
+
+ getInterface: function requestor_gi(iid) {
+ if (this.versions & 1 && iid.equals(Ci.nsIAuthPrompt)) {
+ // Allow the prompt to store state by caching it here
+ if (!this.prompt1) {
+ this.prompt1 = new AuthPrompt1(this.flags);
+ }
+ return this.prompt1;
+ }
+ if (this.versions & 2 && iid.equals(Ci.nsIAuthPrompt2)) {
+ // Allow the prompt to store state by caching it here
+ if (!this.prompt2) {
+ this.prompt2 = new AuthPrompt2(this.flags);
+ }
+ return this.prompt2;
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ prompt1: null,
+ prompt2: null,
+};
+
+function RealmTestRequestor() {}
+
+RealmTestRequestor.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIInterfaceRequestor",
+ "nsIAuthPrompt2",
+ ]),
+
+ getInterface: function realmtest_interface(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt2)) {
+ return this;
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ promptAuth: function realmtest_checkAuth(channel, level, authInfo) {
+ Assert.equal(authInfo.realm, '"foo_bar');
+
+ return false;
+ },
+
+ asyncPromptAuth: function realmtest_async(chan, cb, ctx, lvl, info) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+};
+
+function makeChan(url) {
+ let loadingUrl = Services.io
+ .newURI(url)
+ .mutate()
+ .setPathQueryRef("")
+ .finalize();
+ var principal = Services.scriptSecurityManager.createContentPrincipal(
+ loadingUrl,
+ {}
+ );
+ return NetUtil.newChannel({
+ uri: url,
+ loadingPrincipal: principal,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+}
+
+function ntlm_auth(metadata, response) {
+ let challenge = metadata.getHeader("Authorization");
+ if (!challenge.startsWith("NTLM ")) {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ return;
+ }
+
+ let decoded = atob(challenge.substring(5));
+ info(decoded);
+
+ if (!decoded.startsWith("NTLMSSP\0")) {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ return;
+ }
+
+ let isNegotiate = decoded.substring(8).startsWith("\x01\x00\x00\x00");
+ let isAuthenticate = decoded.substring(8).startsWith("\x03\x00\x00\x00");
+
+ if (isNegotiate) {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader(
+ "WWW-Authenticate",
+ "NTLM TlRMTVNTUAACAAAAAAAAAAAoAAABggAAASNFZ4mrze8AAAAAAAAAAAAAAAAAAAAA",
+ false
+ );
+ return;
+ }
+
+ if (isAuthenticate) {
+ let body = "OK";
+ response.bodyOutputStream.write(body, body.length);
+ return;
+ }
+
+ // Something else went wrong.
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+}
+
+function basic_auth(metadata, response) {
+ let challenge = metadata.getHeader("Authorization");
+ if (!challenge.startsWith("Basic ")) {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ return;
+ }
+
+ if (challenge == "Basic Z3Vlc3Q6Z3Vlc3Q=") {
+ response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+
+ let body = "success";
+ response.bodyOutputStream.write(body, body.length);
+ return;
+ }
+
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+}
+
+//
+// Digest functions
+//
+function bytesFromString(str) {
+ const encoder = new TextEncoder();
+ return encoder.encode(str);
+}
+
+// return the two-digit hexadecimal code for a byte
+function toHexString(charCode) {
+ return ("0" + charCode.toString(16)).slice(-2);
+}
+
+function H(str) {
+ var data = bytesFromString(str);
+ var ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
+ ch.init(Ci.nsICryptoHash.MD5);
+ ch.update(data, data.length);
+ var hash = ch.finish(false);
+ return Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join("");
+}
+
+const nonce = "6f93719059cf8d568005727f3250e798";
+const opaque = "1234opaque1234";
+const digestChallenge = `Digest realm="secret", domain="/", qop=auth,algorithm=MD5, nonce="${nonce}" opaque="${opaque}"`;
+//
+// Digest handler
+//
+// /auth/digest
+function authDigest(metadata, response) {
+ var cnonceRE = /cnonce="(\w+)"/;
+ var responseRE = /response="(\w+)"/;
+ var usernameRE = /username="(\w+)"/;
+ var body = "";
+ // check creds if we have them
+ if (metadata.hasHeader("Authorization")) {
+ var auth = metadata.getHeader("Authorization");
+ var cnonce = auth.match(cnonceRE)[1];
+ var clientDigest = auth.match(responseRE)[1];
+ var username = auth.match(usernameRE)[1];
+ var nc = "00000001";
+
+ if (username != "guest") {
+ response.setStatusLine(metadata.httpVersion, 400, "bad request");
+ body = "should never get here";
+ } else {
+ // see RFC2617 for the description of this calculation
+ var A1 = "guest:secret:guest";
+ var A2 = "GET:/path";
+ var noncebits = [nonce, nc, cnonce, "auth", H(A2)].join(":");
+ var digest = H([H(A1), noncebits].join(":"));
+
+ if (clientDigest == digest) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
+ body = "digest";
+ } else {
+ info(clientDigest);
+ info(digest);
+ handle_unauthorized(metadata, response);
+ return;
+ }
+ }
+ } else {
+ // no header, send one
+ handle_unauthorized(metadata, response);
+ return;
+ }
+
+ response.bodyOutputStream.write(body, body.length);
+}
+
+let challenges = ["NTLM", `Basic realm="secret"`, digestChallenge];
+
+function handle_unauthorized(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+
+ for (let ch of challenges) {
+ response.setHeader("WWW-Authenticate", ch, true);
+ }
+}
+
+// /path
+function auth_handler(metadata, response) {
+ if (!metadata.hasHeader("Authorization")) {
+ handle_unauthorized(metadata, response);
+ return;
+ }
+
+ let challenge = metadata.getHeader("Authorization");
+ if (challenge.startsWith("NTLM ")) {
+ ntlm_auth(metadata, response);
+ return;
+ }
+
+ if (challenge.startsWith("Basic ")) {
+ basic_auth(metadata, response);
+ return;
+ }
+
+ if (challenge.startsWith("Digest ")) {
+ authDigest(metadata, response);
+ return;
+ }
+
+ handle_unauthorized(metadata, response);
+}
+
+let httpserv;
+add_setup(() => {
+ Services.prefs.setBoolPref("network.auth.force-generic-ntlm", true);
+ Services.prefs.setBoolPref("network.auth.force-generic-ntlm-v1", true);
+ Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
+ Services.prefs.setBoolPref("network.http.sanitize-headers-in-logs", false);
+
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/path", auth_handler);
+ httpserv.start(-1);
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.auth.force-generic-ntlm");
+ Services.prefs.clearUserPref("network.auth.force-generic-ntlm-v1");
+ Services.prefs.clearUserPref("network.dns.native-is-localhost");
+ Services.prefs.clearUserPref("network.http.sanitize-headers-in-logs");
+
+ await httpserv.stop();
+ });
+});
+
+add_task(async function test_ntlm_first() {
+ Services.prefs.setBoolPref(
+ "network.auth.choose_most_secure_challenge",
+ false
+ );
+ challenges = ["NTLM", `Basic realm="secret"`, digestChallenge];
+ httpserv.identity.add("http", "ntlm.com", httpserv.identity.primaryPort);
+ let chan = makeChan(URL("ntlm.com", "/path"));
+
+ chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
+ let [req, buf] = await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener((request, buffer) => resolve([request, buffer]), null)
+ );
+ });
+ Assert.equal(buf, "OK");
+ Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+});
+
+add_task(async function test_basic_first() {
+ Services.prefs.setBoolPref(
+ "network.auth.choose_most_secure_challenge",
+ false
+ );
+ challenges = [`Basic realm="secret"`, "NTLM", digestChallenge];
+ httpserv.identity.add("http", "basic.com", httpserv.identity.primaryPort);
+ let chan = makeChan(URL("basic.com", "/path"));
+
+ chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
+ let [req, buf] = await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener((request, buffer) => resolve([request, buffer]), null)
+ );
+ });
+ Assert.equal(buf, "success");
+ Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+});
+
+add_task(async function test_digest_first() {
+ Services.prefs.setBoolPref(
+ "network.auth.choose_most_secure_challenge",
+ false
+ );
+ challenges = [digestChallenge, `Basic realm="secret"`, "NTLM"];
+ httpserv.identity.add("http", "digest.com", httpserv.identity.primaryPort);
+ let chan = makeChan(URL("digest.com", "/path"));
+
+ chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
+ let [req, buf] = await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener((request, buffer) => resolve([request, buffer]), null)
+ );
+ });
+ Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+ Assert.equal(buf, "digest");
+});
+
+add_task(async function test_choose_most_secure() {
+ // When the pref is true, we rank the challenges by how secure they are.
+ // In this case, NTLM should be the most secure.
+ Services.prefs.setBoolPref("network.auth.choose_most_secure_challenge", true);
+ challenges = [digestChallenge, `Basic realm="secret"`, "NTLM"];
+ httpserv.identity.add(
+ "http",
+ "ntlmstrong.com",
+ httpserv.identity.primaryPort
+ );
+ let chan = makeChan(URL("ntlmstrong.com", "/path"));
+
+ chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
+ let [req, buf] = await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener((request, buffer) => resolve([request, buffer]), null)
+ );
+ });
+ Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+ Assert.equal(buf, "OK");
+});