test_auth_multiple.js (11811B)
1 // This file tests authentication prompt callbacks 2 // TODO NIT use do_check_eq(expected, actual) consistently, not sometimes eq(actual, expected) 3 4 "use strict"; 5 6 const { HttpServer } = ChromeUtils.importESModule( 7 "resource://testing-common/httpd.sys.mjs" 8 ); 9 10 // Turn off the authentication dialog blocking for this test. 11 var prefs = Services.prefs; 12 prefs.setIntPref("network.auth.subresource-http-auth-allow", 2); 13 14 function URL(domain, path = "") { 15 if (path.startsWith("/")) { 16 path = path.substring(1); 17 } 18 return `http://${domain}:${httpserv.identity.primaryPort}/${path}`; 19 } 20 21 ChromeUtils.defineLazyGetter(this, "PORT", function () { 22 return httpserv.identity.primaryPort; 23 }); 24 25 const FLAG_RETURN_FALSE = 1 << 0; 26 const FLAG_WRONG_PASSWORD = 1 << 1; 27 const FLAG_BOGUS_USER = 1 << 2; 28 // const FLAG_PREVIOUS_FAILED = 1 << 3; 29 const CROSS_ORIGIN = 1 << 4; 30 // const FLAG_NO_REALM = 1 << 5; 31 const FLAG_NON_ASCII_USER_PASSWORD = 1 << 6; 32 33 function AuthPrompt1(flags) { 34 this.flags = flags; 35 } 36 37 AuthPrompt1.prototype = { 38 user: "guest", 39 pass: "guest", 40 41 expectedRealm: "secret", 42 43 QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]), 44 45 prompt: function ap1_prompt() { 46 do_throw("unexpected prompt call"); 47 }, 48 49 promptUsernameAndPassword: function ap1_promptUP( 50 title, 51 text, 52 realm, 53 savePW, 54 user, 55 pw 56 ) { 57 if (!(this.flags & CROSS_ORIGIN)) { 58 if (!text.includes(this.expectedRealm)) { 59 do_throw("Text must indicate the realm"); 60 } 61 } else if (text.includes(this.expectedRealm)) { 62 do_throw("There should not be realm for cross origin"); 63 } 64 if (!text.includes("localhost")) { 65 do_throw("Text must indicate the hostname"); 66 } 67 if (!text.includes(String(PORT))) { 68 do_throw("Text must indicate the port"); 69 } 70 if (text.includes("-1")) { 71 do_throw("Text must contain negative numbers"); 72 } 73 74 if (this.flags & FLAG_RETURN_FALSE) { 75 return false; 76 } 77 78 if (this.flags & FLAG_BOGUS_USER) { 79 this.user = "foo\nbar"; 80 } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) { 81 this.user = "é"; 82 } 83 84 user.value = this.user; 85 if (this.flags & FLAG_WRONG_PASSWORD) { 86 pw.value = this.pass + ".wrong"; 87 // Now clear the flag to avoid an infinite loop 88 this.flags &= ~FLAG_WRONG_PASSWORD; 89 } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) { 90 pw.value = "é"; 91 } else { 92 pw.value = this.pass; 93 } 94 return true; 95 }, 96 97 promptPassword: function ap1_promptPW() { 98 do_throw("unexpected promptPassword call"); 99 }, 100 }; 101 102 function AuthPrompt2(flags) { 103 this.flags = flags; 104 } 105 106 AuthPrompt2.prototype = { 107 user: "guest", 108 pass: "guest", 109 110 expectedRealm: "secret", 111 112 QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]), 113 114 promptAuth: function ap2_promptAuth(channel, level, authInfo) { 115 authInfo.username = this.user; 116 authInfo.password = this.pass; 117 return true; 118 }, 119 120 asyncPromptAuth: function ap2_async() { 121 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); 122 }, 123 }; 124 125 function Requestor(flags, versions) { 126 this.flags = flags; 127 this.versions = versions; 128 } 129 130 Requestor.prototype = { 131 QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), 132 133 getInterface: function requestor_gi(iid) { 134 if (this.versions & 1 && iid.equals(Ci.nsIAuthPrompt)) { 135 // Allow the prompt to store state by caching it here 136 if (!this.prompt1) { 137 this.prompt1 = new AuthPrompt1(this.flags); 138 } 139 return this.prompt1; 140 } 141 if (this.versions & 2 && iid.equals(Ci.nsIAuthPrompt2)) { 142 // Allow the prompt to store state by caching it here 143 if (!this.prompt2) { 144 this.prompt2 = new AuthPrompt2(this.flags); 145 } 146 return this.prompt2; 147 } 148 149 throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); 150 }, 151 152 prompt1: null, 153 prompt2: null, 154 }; 155 156 function RealmTestRequestor() {} 157 158 RealmTestRequestor.prototype = { 159 QueryInterface: ChromeUtils.generateQI([ 160 "nsIInterfaceRequestor", 161 "nsIAuthPrompt2", 162 ]), 163 164 getInterface: function realmtest_interface(iid) { 165 if (iid.equals(Ci.nsIAuthPrompt2)) { 166 return this; 167 } 168 169 throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); 170 }, 171 172 promptAuth: function realmtest_checkAuth(channel, level, authInfo) { 173 Assert.equal(authInfo.realm, '"foo_bar'); 174 175 return false; 176 }, 177 178 asyncPromptAuth: function realmtest_async() { 179 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); 180 }, 181 }; 182 183 function makeChan(url) { 184 let loadingUrl = Services.io 185 .newURI(url) 186 .mutate() 187 .setPathQueryRef("") 188 .finalize(); 189 var principal = Services.scriptSecurityManager.createContentPrincipal( 190 loadingUrl, 191 {} 192 ); 193 return NetUtil.newChannel({ 194 uri: url, 195 loadingPrincipal: principal, 196 securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, 197 contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, 198 }); 199 } 200 201 function ntlm_auth(metadata, response) { 202 let challenge = metadata.getHeader("Authorization"); 203 if (!challenge.startsWith("NTLM ")) { 204 response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); 205 return; 206 } 207 208 let decoded = atob(challenge.substring(5)); 209 info(decoded); 210 211 if (!decoded.startsWith("NTLMSSP\0")) { 212 response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); 213 return; 214 } 215 216 let isNegotiate = decoded.substring(8).startsWith("\x01\x00\x00\x00"); 217 let isAuthenticate = decoded.substring(8).startsWith("\x03\x00\x00\x00"); 218 219 if (isNegotiate) { 220 response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); 221 response.setHeader( 222 "WWW-Authenticate", 223 "NTLM TlRMTVNTUAACAAAAAAAAAAAoAAABggAAASNFZ4mrze8AAAAAAAAAAAAAAAAAAAAA", 224 false 225 ); 226 return; 227 } 228 229 if (isAuthenticate) { 230 let body = "OK"; 231 response.bodyOutputStream.write(body, body.length); 232 return; 233 } 234 235 // Something else went wrong. 236 response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); 237 } 238 239 function basic_auth(metadata, response) { 240 let challenge = metadata.getHeader("Authorization"); 241 if (!challenge.startsWith("Basic ")) { 242 response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); 243 return; 244 } 245 246 if (challenge == "Basic Z3Vlc3Q6Z3Vlc3Q=") { 247 response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); 248 response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); 249 250 let body = "success"; 251 response.bodyOutputStream.write(body, body.length); 252 return; 253 } 254 255 response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); 256 response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); 257 } 258 259 // 260 // Digest functions 261 // 262 function bytesFromString(str) { 263 const encoder = new TextEncoder(); 264 return encoder.encode(str); 265 } 266 267 // return the two-digit hexadecimal code for a byte 268 function toHexString(charCode) { 269 return ("0" + charCode.toString(16)).slice(-2); 270 } 271 272 function H(str) { 273 var data = bytesFromString(str); 274 var ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); 275 ch.init(Ci.nsICryptoHash.MD5); 276 ch.update(data, data.length); 277 var hash = ch.finish(false); 278 return Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join(""); 279 } 280 281 const nonce = "6f93719059cf8d568005727f3250e798"; 282 const opaque = "1234opaque1234"; 283 const digestChallenge = `Digest realm="secret", domain="/", qop=auth,algorithm=MD5, nonce="${nonce}" opaque="${opaque}"`; 284 // 285 // Digest handler 286 // 287 // /auth/digest 288 function authDigest(metadata, response) { 289 var cnonceRE = /cnonce="(\w+)"/; 290 var responseRE = /response="(\w+)"/; 291 var usernameRE = /username="(\w+)"/; 292 var body = ""; 293 // check creds if we have them 294 if (metadata.hasHeader("Authorization")) { 295 var auth = metadata.getHeader("Authorization"); 296 var cnonce = auth.match(cnonceRE)[1]; 297 var clientDigest = auth.match(responseRE)[1]; 298 var username = auth.match(usernameRE)[1]; 299 var nc = "00000001"; 300 301 if (username != "guest") { 302 response.setStatusLine(metadata.httpVersion, 400, "bad request"); 303 body = "should never get here"; 304 } else { 305 // see RFC2617 for the description of this calculation 306 var A1 = "guest:secret:guest"; 307 var A2 = "GET:/path"; 308 var noncebits = [nonce, nc, cnonce, "auth", H(A2)].join(":"); 309 var digest = H([H(A1), noncebits].join(":")); 310 311 if (clientDigest == digest) { 312 response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); 313 body = "digest"; 314 } else { 315 info(clientDigest); 316 info(digest); 317 handle_unauthorized(metadata, response); 318 return; 319 } 320 } 321 } else { 322 // no header, send one 323 handle_unauthorized(metadata, response); 324 return; 325 } 326 327 response.bodyOutputStream.write(body, body.length); 328 } 329 330 let challenges = ["NTLM", `Basic realm="secret"`, digestChallenge]; 331 332 function handle_unauthorized(metadata, response) { 333 response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); 334 335 for (let ch of challenges) { 336 response.setHeader("WWW-Authenticate", ch, true); 337 } 338 } 339 340 // /path 341 function auth_handler(metadata, response) { 342 if (!metadata.hasHeader("Authorization")) { 343 handle_unauthorized(metadata, response); 344 return; 345 } 346 347 let challenge = metadata.getHeader("Authorization"); 348 if (challenge.startsWith("NTLM ")) { 349 ntlm_auth(metadata, response); 350 return; 351 } 352 353 if (challenge.startsWith("Basic ")) { 354 basic_auth(metadata, response); 355 return; 356 } 357 358 if (challenge.startsWith("Digest ")) { 359 authDigest(metadata, response); 360 return; 361 } 362 363 handle_unauthorized(metadata, response); 364 } 365 366 let httpserv; 367 add_setup(() => { 368 Services.prefs.setBoolPref("network.auth.force-generic-ntlm", true); 369 Services.prefs.setBoolPref("network.auth.force-generic-ntlm-v1", true); 370 Services.prefs.setBoolPref("network.dns.native-is-localhost", true); 371 Services.prefs.setBoolPref("network.http.sanitize-headers-in-logs", false); 372 373 httpserv = new HttpServer(); 374 httpserv.registerPathHandler("/path", auth_handler); 375 httpserv.start(-1); 376 377 registerCleanupFunction(async () => { 378 Services.prefs.clearUserPref("network.auth.force-generic-ntlm"); 379 Services.prefs.clearUserPref("network.auth.force-generic-ntlm-v1"); 380 Services.prefs.clearUserPref("network.dns.native-is-localhost"); 381 Services.prefs.clearUserPref("network.http.sanitize-headers-in-logs"); 382 383 await httpserv.stop(); 384 }); 385 }); 386 387 add_task(async function test_ntlm_first() { 388 Services.prefs.setBoolPref( 389 "network.auth.choose_most_secure_challenge", 390 false 391 ); 392 challenges = ["NTLM", `Basic realm="secret"`, digestChallenge]; 393 httpserv.identity.add("http", "ntlm.com", httpserv.identity.primaryPort); 394 let chan = makeChan(URL("ntlm.com", "/path")); 395 396 chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2); 397 let [req, buf] = await new Promise(resolve => { 398 chan.asyncOpen( 399 new ChannelListener((request, buffer) => resolve([request, buffer]), null) 400 ); 401 }); 402 Assert.equal(buf, "OK"); 403 Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); 404 }); 405 406 add_task(async function test_choose_most_secure() { 407 // By default, we rank the challenges by how secure they are. 408 // In this case, NTLM should be the most secure. 409 challenges = [digestChallenge, `Basic realm="secret"`, "NTLM"]; 410 httpserv.identity.add( 411 "http", 412 "ntlmstrong.com", 413 httpserv.identity.primaryPort 414 ); 415 let chan = makeChan(URL("ntlmstrong.com", "/path")); 416 417 chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2); 418 let [req, buf] = await new Promise(resolve => { 419 chan.asyncOpen( 420 new ChannelListener((request, buffer) => resolve([request, buffer]), null) 421 ); 422 }); 423 Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); 424 Assert.equal(buf, "OK"); 425 });