// 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.import("resource://testing-common/httpd.js"); // 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}`; } XPCOMUtils.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("utf-8"); 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((req, buf) => resolve([req, buf]), 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((req, buf) => resolve([req, buf]), 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((req, buf) => resolve([req, buf]), 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((req, buf) => resolve([req, buf]), null) ); }); Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); Assert.equal(buf, "OK"); });