Bug 1796551 [wpt PR 36570] - WebKit export of https://bugs.webkit.org/show_bug.cgi...
[gecko.git] / netwerk / test / unit / test_auth_multiple.js
blob81fa37060bb6c1488102eb827ee27abc1da622e6
1 // This file tests authentication prompt callbacks
2 // TODO NIT use do_check_eq(expected, actual) consistently, not sometimes eq(actual, expected)
4 "use strict";
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);
15   }
16   return `http://${domain}:${httpserv.identity.primaryPort}/${path}`;
19 XPCOMUtils.defineLazyGetter(this, "PORT", function() {
20   return httpserv.identity.primaryPort;
21 });
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) {
32   this.flags = flags;
35 AuthPrompt1.prototype = {
36   user: "guest",
37   pass: "guest",
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");
45   },
47   promptUsernameAndPassword: function ap1_promptUP(
48     title,
49     text,
50     realm,
51     savePW,
52     user,
53     pw
54   ) {
55     if (!(this.flags & CROSS_ORIGIN)) {
56       if (!text.includes(this.expectedRealm)) {
57         do_throw("Text must indicate the realm");
58       }
59     } else if (text.includes(this.expectedRealm)) {
60       do_throw("There should not be realm for cross origin");
61     }
62     if (!text.includes("localhost")) {
63       do_throw("Text must indicate the hostname");
64     }
65     if (!text.includes(String(PORT))) {
66       do_throw("Text must indicate the port");
67     }
68     if (text.includes("-1")) {
69       do_throw("Text must contain negative numbers");
70     }
72     if (this.flags & FLAG_RETURN_FALSE) {
73       return false;
74     }
76     if (this.flags & FLAG_BOGUS_USER) {
77       this.user = "foo\nbar";
78     } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) {
79       this.user = "é";
80     }
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) {
88       pw.value = "é";
89     } else {
90       pw.value = this.pass;
91     }
92     return true;
93   },
95   promptPassword: function ap1_promptPW(title, text, realm, save, pwd) {
96     do_throw("unexpected promptPassword call");
97   },
100 function AuthPrompt2(flags) {
101   this.flags = flags;
104 AuthPrompt2.prototype = {
105   user: "guest",
106   pass: "guest",
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;
115     return true;
116   },
118   asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) {
119     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
120   },
123 function Requestor(flags, versions) {
124   this.flags = flags;
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
134       if (!this.prompt1) {
135         this.prompt1 = new AuthPrompt1(this.flags);
136       }
137       return this.prompt1;
138     }
139     if (this.versions & 2 && iid.equals(Ci.nsIAuthPrompt2)) {
140       // Allow the prompt to store state by caching it here
141       if (!this.prompt2) {
142         this.prompt2 = new AuthPrompt2(this.flags);
143       }
144       return this.prompt2;
145     }
147     throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
148   },
150   prompt1: null,
151   prompt2: null,
154 function RealmTestRequestor() {}
156 RealmTestRequestor.prototype = {
157   QueryInterface: ChromeUtils.generateQI([
158     "nsIInterfaceRequestor",
159     "nsIAuthPrompt2",
160   ]),
162   getInterface: function realmtest_interface(iid) {
163     if (iid.equals(Ci.nsIAuthPrompt2)) {
164       return this;
165     }
167     throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
168   },
170   promptAuth: function realmtest_checkAuth(channel, level, authInfo) {
171     Assert.equal(authInfo.realm, '"foo_bar');
173     return false;
174   },
176   asyncPromptAuth: function realmtest_async(chan, cb, ctx, lvl, info) {
177     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
178   },
181 function makeChan(url) {
182   let loadingUrl = Services.io
183     .newURI(url)
184     .mutate()
185     .setPathQueryRef("")
186     .finalize();
187   var principal = Services.scriptSecurityManager.createContentPrincipal(
188     loadingUrl,
189     {}
190   );
191   return NetUtil.newChannel({
192     uri: url,
193     loadingPrincipal: principal,
194     securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
195     contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
196   });
199 function ntlm_auth(metadata, response) {
200   let challenge = metadata.getHeader("Authorization");
201   if (!challenge.startsWith("NTLM ")) {
202     response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
203     return;
204   }
206   let decoded = atob(challenge.substring(5));
207   info(decoded);
209   if (!decoded.startsWith("NTLMSSP\0")) {
210     response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
211     return;
212   }
214   let isNegotiate = decoded.substring(8).startsWith("\x01\x00\x00\x00");
215   let isAuthenticate = decoded.substring(8).startsWith("\x03\x00\x00\x00");
217   if (isNegotiate) {
218     response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
219     response.setHeader(
220       "WWW-Authenticate",
221       "NTLM TlRMTVNTUAACAAAAAAAAAAAoAAABggAAASNFZ4mrze8AAAAAAAAAAAAAAAAAAAAA",
222       false
223     );
224     return;
225   }
227   if (isAuthenticate) {
228     let body = "OK";
229     response.bodyOutputStream.write(body, body.length);
230     return;
231   }
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");
241     return;
242   }
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);
250     return;
251   }
253   response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
254   response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
258 // Digest functions
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);
270 function H(str) {
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}"`;
283 // Digest handler
285 // /auth/digest
286 function authDigest(metadata, response) {
287   var cnonceRE = /cnonce="(\w+)"/;
288   var responseRE = /response="(\w+)"/;
289   var usernameRE = /username="(\w+)"/;
290   var body = "";
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];
297     var nc = "00000001";
299     if (username != "guest") {
300       response.setStatusLine(metadata.httpVersion, 400, "bad request");
301       body = "should never get here";
302     } else {
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");
311         body = "digest";
312       } else {
313         info(clientDigest);
314         info(digest);
315         handle_unauthorized(metadata, response);
316         return;
317       }
318     }
319   } else {
320     // no header, send one
321     handle_unauthorized(metadata, response);
322     return;
323   }
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);
335   }
338 // /path
339 function auth_handler(metadata, response) {
340   if (!metadata.hasHeader("Authorization")) {
341     handle_unauthorized(metadata, response);
342     return;
343   }
345   let challenge = metadata.getHeader("Authorization");
346   if (challenge.startsWith("NTLM ")) {
347     ntlm_auth(metadata, response);
348     return;
349   }
351   if (challenge.startsWith("Basic ")) {
352     basic_auth(metadata, response);
353     return;
354   }
356   if (challenge.startsWith("Digest ")) {
357     authDigest(metadata, response);
358     return;
359   }
361   handle_unauthorized(metadata, response);
364 let httpserv;
365 add_setup(() => {
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);
373   httpserv.start(-1);
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();
382   });
385 add_task(async function test_ntlm_first() {
386   Services.prefs.setBoolPref(
387     "network.auth.choose_most_secure_challenge",
388     false
389   );
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 => {
396     chan.asyncOpen(
397       new ChannelListener((req, buf) => resolve([req, buf]), null)
398     );
399   });
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",
407     false
408   );
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 => {
415     chan.asyncOpen(
416       new ChannelListener((req, buf) => resolve([req, buf]), null)
417     );
418   });
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",
426     false
427   );
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 => {
434     chan.asyncOpen(
435       new ChannelListener((req, buf) => resolve([req, buf]), null)
436     );
437   });
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(
448     "http",
449     "ntlmstrong.com",
450     httpserv.identity.primaryPort
451   );
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 => {
456     chan.asyncOpen(
457       new ChannelListener((req, buf) => resolve([req, buf]), null)
458     );
459   });
460   Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
461   Assert.equal(buf, "OK");