Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

// 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(
);
// 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() {
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() {
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() {
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() {
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");
});