1 // This file tests authentication prompt callbacks
2 // TODO NIT use do_check_eq(expected, actual) consistently, not sometimes eq(actual, expected)
6 const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
8 // Turn off the authentication dialog blocking for this test.
9 var prefs = Services.prefs;
10 prefs.setIntPref("network.auth.subresource-http-auth-allow", 2);
12 function URL(domain, path = "") {
13 if (path.startsWith("/")) {
14 path = path.substring(1);
16 return `http://${domain}:${httpserv.identity.primaryPort}/${path}`;
19 XPCOMUtils.defineLazyGetter(this, "PORT", function() {
20 return httpserv.identity.primaryPort;
23 const FLAG_RETURN_FALSE = 1 << 0;
24 const FLAG_WRONG_PASSWORD = 1 << 1;
25 const FLAG_BOGUS_USER = 1 << 2;
26 // const FLAG_PREVIOUS_FAILED = 1 << 3;
27 const CROSS_ORIGIN = 1 << 4;
28 // const FLAG_NO_REALM = 1 << 5;
29 const FLAG_NON_ASCII_USER_PASSWORD = 1 << 6;
31 function AuthPrompt1(flags) {
35 AuthPrompt1.prototype = {
39 expectedRealm: "secret",
41 QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]),
43 prompt: function ap1_prompt(title, text, realm, save, defaultText, result) {
44 do_throw("unexpected prompt call");
47 promptUsernameAndPassword: function ap1_promptUP(
55 if (!(this.flags & CROSS_ORIGIN)) {
56 if (!text.includes(this.expectedRealm)) {
57 do_throw("Text must indicate the realm");
59 } else if (text.includes(this.expectedRealm)) {
60 do_throw("There should not be realm for cross origin");
62 if (!text.includes("localhost")) {
63 do_throw("Text must indicate the hostname");
65 if (!text.includes(String(PORT))) {
66 do_throw("Text must indicate the port");
68 if (text.includes("-1")) {
69 do_throw("Text must contain negative numbers");
72 if (this.flags & FLAG_RETURN_FALSE) {
76 if (this.flags & FLAG_BOGUS_USER) {
77 this.user = "foo\nbar";
78 } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) {
82 user.value = this.user;
83 if (this.flags & FLAG_WRONG_PASSWORD) {
84 pw.value = this.pass + ".wrong";
85 // Now clear the flag to avoid an infinite loop
86 this.flags &= ~FLAG_WRONG_PASSWORD;
87 } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) {
95 promptPassword: function ap1_promptPW(title, text, realm, save, pwd) {
96 do_throw("unexpected promptPassword call");
100 function AuthPrompt2(flags) {
104 AuthPrompt2.prototype = {
108 expectedRealm: "secret",
110 QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
112 promptAuth: function ap2_promptAuth(channel, level, authInfo) {
113 authInfo.username = this.user;
114 authInfo.password = this.pass;
118 asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) {
119 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
123 function Requestor(flags, versions) {
125 this.versions = versions;
128 Requestor.prototype = {
129 QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
131 getInterface: function requestor_gi(iid) {
132 if (this.versions & 1 && iid.equals(Ci.nsIAuthPrompt)) {
133 // Allow the prompt to store state by caching it here
135 this.prompt1 = new AuthPrompt1(this.flags);
139 if (this.versions & 2 && iid.equals(Ci.nsIAuthPrompt2)) {
140 // Allow the prompt to store state by caching it here
142 this.prompt2 = new AuthPrompt2(this.flags);
147 throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
154 function RealmTestRequestor() {}
156 RealmTestRequestor.prototype = {
157 QueryInterface: ChromeUtils.generateQI([
158 "nsIInterfaceRequestor",
162 getInterface: function realmtest_interface(iid) {
163 if (iid.equals(Ci.nsIAuthPrompt2)) {
167 throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
170 promptAuth: function realmtest_checkAuth(channel, level, authInfo) {
171 Assert.equal(authInfo.realm, '"foo_bar');
176 asyncPromptAuth: function realmtest_async(chan, cb, ctx, lvl, info) {
177 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
181 function makeChan(url) {
182 let loadingUrl = Services.io
187 var principal = Services.scriptSecurityManager.createContentPrincipal(
191 return NetUtil.newChannel({
193 loadingPrincipal: principal,
194 securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
195 contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
199 function ntlm_auth(metadata, response) {
200 let challenge = metadata.getHeader("Authorization");
201 if (!challenge.startsWith("NTLM ")) {
202 response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
206 let decoded = atob(challenge.substring(5));
209 if (!decoded.startsWith("NTLMSSP\0")) {
210 response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
214 let isNegotiate = decoded.substring(8).startsWith("\x01\x00\x00\x00");
215 let isAuthenticate = decoded.substring(8).startsWith("\x03\x00\x00\x00");
218 response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
221 "NTLM TlRMTVNTUAACAAAAAAAAAAAoAAABggAAASNFZ4mrze8AAAAAAAAAAAAAAAAAAAAA",
227 if (isAuthenticate) {
229 response.bodyOutputStream.write(body, body.length);
233 // Something else went wrong.
234 response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
237 function basic_auth(metadata, response) {
238 let challenge = metadata.getHeader("Authorization");
239 if (!challenge.startsWith("Basic ")) {
240 response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
244 if (challenge == "Basic Z3Vlc3Q6Z3Vlc3Q=") {
245 response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
246 response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
248 let body = "success";
249 response.bodyOutputStream.write(body, body.length);
253 response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
254 response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
260 function bytesFromString(str) {
261 const encoder = new TextEncoder("utf-8");
262 return encoder.encode(str);
265 // return the two-digit hexadecimal code for a byte
266 function toHexString(charCode) {
267 return ("0" + charCode.toString(16)).slice(-2);
271 var data = bytesFromString(str);
272 var ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
273 ch.init(Ci.nsICryptoHash.MD5);
274 ch.update(data, data.length);
275 var hash = ch.finish(false);
276 return Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join("");
279 const nonce = "6f93719059cf8d568005727f3250e798";
280 const opaque = "1234opaque1234";
281 const digestChallenge = `Digest realm="secret", domain="/", qop=auth,algorithm=MD5, nonce="${nonce}" opaque="${opaque}"`;
286 function authDigest(metadata, response) {
287 var cnonceRE = /cnonce="(\w+)"/;
288 var responseRE = /response="(\w+)"/;
289 var usernameRE = /username="(\w+)"/;
291 // check creds if we have them
292 if (metadata.hasHeader("Authorization")) {
293 var auth = metadata.getHeader("Authorization");
294 var cnonce = auth.match(cnonceRE)[1];
295 var clientDigest = auth.match(responseRE)[1];
296 var username = auth.match(usernameRE)[1];
299 if (username != "guest") {
300 response.setStatusLine(metadata.httpVersion, 400, "bad request");
301 body = "should never get here";
303 // see RFC2617 for the description of this calculation
304 var A1 = "guest:secret:guest";
305 var A2 = "GET:/path";
306 var noncebits = [nonce, nc, cnonce, "auth", H(A2)].join(":");
307 var digest = H([H(A1), noncebits].join(":"));
309 if (clientDigest == digest) {
310 response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
315 handle_unauthorized(metadata, response);
320 // no header, send one
321 handle_unauthorized(metadata, response);
325 response.bodyOutputStream.write(body, body.length);
328 let challenges = ["NTLM", `Basic realm="secret"`, digestChallenge];
330 function handle_unauthorized(metadata, response) {
331 response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
333 for (let ch of challenges) {
334 response.setHeader("WWW-Authenticate", ch, true);
339 function auth_handler(metadata, response) {
340 if (!metadata.hasHeader("Authorization")) {
341 handle_unauthorized(metadata, response);
345 let challenge = metadata.getHeader("Authorization");
346 if (challenge.startsWith("NTLM ")) {
347 ntlm_auth(metadata, response);
351 if (challenge.startsWith("Basic ")) {
352 basic_auth(metadata, response);
356 if (challenge.startsWith("Digest ")) {
357 authDigest(metadata, response);
361 handle_unauthorized(metadata, response);
366 Services.prefs.setBoolPref("network.auth.force-generic-ntlm", true);
367 Services.prefs.setBoolPref("network.auth.force-generic-ntlm-v1", true);
368 Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
369 Services.prefs.setBoolPref("network.http.sanitize-headers-in-logs", false);
371 httpserv = new HttpServer();
372 httpserv.registerPathHandler("/path", auth_handler);
375 registerCleanupFunction(async () => {
376 Services.prefs.clearUserPref("network.auth.force-generic-ntlm");
377 Services.prefs.clearUserPref("network.auth.force-generic-ntlm-v1");
378 Services.prefs.clearUserPref("network.dns.native-is-localhost");
379 Services.prefs.clearUserPref("network.http.sanitize-headers-in-logs");
381 await httpserv.stop();
385 add_task(async function test_ntlm_first() {
386 Services.prefs.setBoolPref(
387 "network.auth.choose_most_secure_challenge",
390 challenges = ["NTLM", `Basic realm="secret"`, digestChallenge];
391 httpserv.identity.add("http", "ntlm.com", httpserv.identity.primaryPort);
392 let chan = makeChan(URL("ntlm.com", "/path"));
394 chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
395 let [req, buf] = await new Promise(resolve => {
397 new ChannelListener((req, buf) => resolve([req, buf]), null)
400 Assert.equal(buf, "OK");
401 Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
404 add_task(async function test_basic_first() {
405 Services.prefs.setBoolPref(
406 "network.auth.choose_most_secure_challenge",
409 challenges = [`Basic realm="secret"`, "NTLM", digestChallenge];
410 httpserv.identity.add("http", "basic.com", httpserv.identity.primaryPort);
411 let chan = makeChan(URL("basic.com", "/path"));
413 chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
414 let [req, buf] = await new Promise(resolve => {
416 new ChannelListener((req, buf) => resolve([req, buf]), null)
419 Assert.equal(buf, "success");
420 Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
423 add_task(async function test_digest_first() {
424 Services.prefs.setBoolPref(
425 "network.auth.choose_most_secure_challenge",
428 challenges = [digestChallenge, `Basic realm="secret"`, "NTLM"];
429 httpserv.identity.add("http", "digest.com", httpserv.identity.primaryPort);
430 let chan = makeChan(URL("digest.com", "/path"));
432 chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
433 let [req, buf] = await new Promise(resolve => {
435 new ChannelListener((req, buf) => resolve([req, buf]), null)
438 Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
439 Assert.equal(buf, "digest");
442 add_task(async function test_choose_most_secure() {
443 // When the pref is true, we rank the challenges by how secure they are.
444 // In this case, NTLM should be the most secure.
445 Services.prefs.setBoolPref("network.auth.choose_most_secure_challenge", true);
446 challenges = [digestChallenge, `Basic realm="secret"`, "NTLM"];
447 httpserv.identity.add(
450 httpserv.identity.primaryPort
452 let chan = makeChan(URL("ntlmstrong.com", "/path"));
454 chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
455 let [req, buf] = await new Promise(resolve => {
457 new ChannelListener((req, buf) => resolve([req, buf]), null)
460 Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
461 Assert.equal(buf, "OK");