mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 935232 - Implement a client for the Firefox Accounts auth server. r=markh
This commit is contained in:
parent
896c4bea7f
commit
09185a9092
@ -847,3 +847,6 @@ pref("media.webspeech.synth.enabled", true);
|
||||
// Downloads API
|
||||
pref("dom.mozDownloads.enabled", true);
|
||||
pref("dom.downloads.max_retention_days", 7);
|
||||
|
||||
// The URL of the Firefox Accounts auth server backend
|
||||
pref("identity.fxaccounts.auth.uri", "https://api-accounts.dev.lcip.org/v1");
|
||||
|
@ -1335,3 +1335,6 @@ pref("network.disable.ipc.security", true);
|
||||
|
||||
// CustomizableUI debug logging.
|
||||
pref("browser.uiCustomization.debug", false);
|
||||
|
||||
// The URL of the Firefox Accounts auth server backend
|
||||
pref("identity.fxaccounts.auth.uri", "https://api-accounts.dev.lcip.org/v1");
|
||||
|
@ -35,6 +35,26 @@ function do_check_throws(aFunc, aResult, aStack) {
|
||||
do_throw("Expected result " + aResult + ", none thrown.", aStack);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test whether specified function throws exception with expected
|
||||
* result.
|
||||
*
|
||||
* @param func
|
||||
* Function to be tested.
|
||||
* @param message
|
||||
* Message of expected exception. <code>null</code> for no throws.
|
||||
*/
|
||||
function do_check_throws_message(aFunc, aResult) {
|
||||
try {
|
||||
aFunc();
|
||||
} catch (e) {
|
||||
do_check_eq(e.message, aResult);
|
||||
return;
|
||||
}
|
||||
do_throw("Expected an error, none thrown.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Print some debug message to the console. All arguments will be printed,
|
||||
* separated by spaces.
|
||||
|
@ -204,6 +204,14 @@ this.CommonUtils = {
|
||||
return hex;
|
||||
},
|
||||
|
||||
hexToBytes: function hexToBytes(str) {
|
||||
let bytes = [];
|
||||
for (let i = 0; i < str.length - 1; i += 2) {
|
||||
bytes.push(parseInt(str.substr(i, 2), 16));
|
||||
}
|
||||
return String.fromCharCode.apply(String, bytes);
|
||||
},
|
||||
|
||||
/**
|
||||
* Base32 encode (RFC 4648) a string
|
||||
*/
|
||||
|
@ -11,6 +11,20 @@ Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
this.CryptoUtils = {
|
||||
xor: function xor(a, b) {
|
||||
let bytes = [];
|
||||
|
||||
if (a.length != b.length) {
|
||||
throw new Error("can't xor unequal length strings: "+a.length+" vs "+b.length);
|
||||
}
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
bytes[i] = a.charCodeAt(i) ^ b.charCodeAt(i);
|
||||
}
|
||||
|
||||
return String.fromCharCode.apply(String, bytes);
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate a string of random bytes.
|
||||
*/
|
||||
@ -109,6 +123,22 @@ this.CryptoUtils = {
|
||||
return hasher;
|
||||
},
|
||||
|
||||
/**
|
||||
* HMAC-based Key Derivation (RFC 5869).
|
||||
*/
|
||||
hkdf: function hkdf(ikm, xts, info, len) {
|
||||
const BLOCKSIZE = 256 / 8;
|
||||
if (typeof xts === undefined)
|
||||
xts = String.fromCharCode(0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0);
|
||||
let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256,
|
||||
CryptoUtils.makeHMACKey(xts));
|
||||
let prk = CryptoUtils.digestBytes(ikm, h);
|
||||
return CryptoUtils.hkdfExpand(prk, info, len);
|
||||
},
|
||||
|
||||
/**
|
||||
* HMAC-based Key Derivation Step 2 according to RFC 5869.
|
||||
*/
|
||||
@ -458,9 +488,8 @@ this.CryptoUtils = {
|
||||
|
||||
let contentType = CryptoUtils.stripHeaderAttributes(options.contentType);
|
||||
|
||||
if (!artifacts.hash &&
|
||||
options.hasOwnProperty("payload") &&
|
||||
options.payload) {
|
||||
if (!artifacts.hash && options.hasOwnProperty("payload")
|
||||
&& options.payload) {
|
||||
let hasher = Cc["@mozilla.org/security/hash;1"]
|
||||
.createInstance(Ci.nsICryptoHash);
|
||||
hasher.init(hash_algo);
|
||||
@ -469,8 +498,9 @@ this.CryptoUtils = {
|
||||
CryptoUtils.updateUTF8(options.payload, hasher);
|
||||
CryptoUtils.updateUTF8("\n", hasher);
|
||||
let hash = hasher.finish(false);
|
||||
// HAWK specifies this .hash to include trailing "==" padding.
|
||||
let hash_b64 = CommonUtils.encodeBase64URL(hash, true);
|
||||
// HAWK specifies this .hash to use +/ (not _-) and include the
|
||||
// trailing "==" padding.
|
||||
let hash_b64 = btoa(hash);
|
||||
artifacts.hash = hash_b64;
|
||||
}
|
||||
|
||||
|
@ -93,16 +93,28 @@ function expand_hex(prk, info, len) {
|
||||
return CommonUtils.bytesAsHex(CryptoUtils.hkdfExpand(prk, info, len));
|
||||
}
|
||||
|
||||
function hkdf_hex(ikm, salt, info, len) {
|
||||
ikm = _hexToString(ikm);
|
||||
if (salt)
|
||||
salt = _hexToString(salt);
|
||||
info = _hexToString(info);
|
||||
return CommonUtils.bytesAsHex(CryptoUtils.hkdf(ikm, salt, info, len));
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
_("Verifying Test Case 1");
|
||||
do_check_eq(extract_hex(tc1.salt, tc1.IKM), tc1.PRK);
|
||||
do_check_eq(expand_hex(tc1.PRK, tc1.info, tc1.L), tc1.OKM);
|
||||
do_check_eq(hkdf_hex(tc1.IKM, tc1.salt, tc1.info, tc1.L), tc1.OKM);
|
||||
|
||||
_("Verifying Test Case 2");
|
||||
do_check_eq(extract_hex(tc2.salt, tc2.IKM), tc2.PRK);
|
||||
do_check_eq(expand_hex(tc2.PRK, tc2.info, tc2.L), tc2.OKM);
|
||||
do_check_eq(hkdf_hex(tc2.IKM, tc2.salt, tc2.info, tc2.L), tc2.OKM);
|
||||
|
||||
_("Verifying Test Case 3");
|
||||
do_check_eq(extract_hex(tc3.salt, tc3.IKM), tc3.PRK);
|
||||
do_check_eq(expand_hex(tc3.PRK, tc3.info, tc3.L), tc3.OKM);
|
||||
do_check_eq(hkdf_hex(tc3.IKM, tc3.salt, tc3.info, tc3.L), tc3.OKM);
|
||||
do_check_eq(hkdf_hex(tc3.IKM, undefined, tc3.info, tc3.L), tc3.OKM);
|
||||
}
|
||||
|
330
services/fxaccounts/FxAccountsClient.jsm
Normal file
330
services/fxaccounts/FxAccountsClient.jsm
Normal file
@ -0,0 +1,330 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["FxAccountsClient"];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
|
||||
// Default can be changed by the preference 'identity.fxaccounts.auth.uri'
|
||||
let _host = "https://api-accounts.dev.lcip.org/v1";
|
||||
try {
|
||||
_host = Services.prefs.getCharPref("identity.fxaccounts.auth.uri");
|
||||
} catch(keepDefault) {}
|
||||
|
||||
const HOST = _host;
|
||||
const PREFIX_NAME = "identity.mozilla.com/picl/v1/";
|
||||
|
||||
const XMLHttpRequest =
|
||||
Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1");
|
||||
|
||||
|
||||
function stringToHex(str) {
|
||||
let encoder = new TextEncoder("utf-8");
|
||||
let bytes = encoder.encode(str);
|
||||
return bytesToHex(bytes);
|
||||
}
|
||||
|
||||
// XXX Sadly, CommonUtils.bytesAsHex doesn't handle typed arrays.
|
||||
function bytesToHex(bytes) {
|
||||
let hex = [];
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
hex.push((bytes[i] >>> 4).toString(16));
|
||||
hex.push((bytes[i] & 0xF).toString(16));
|
||||
}
|
||||
return hex.join("");
|
||||
}
|
||||
|
||||
this.FxAccountsClient = function(host = HOST) {
|
||||
this.host = host;
|
||||
};
|
||||
|
||||
this.FxAccountsClient.prototype = {
|
||||
/**
|
||||
* Create a new Firefox Account and authenticate
|
||||
*
|
||||
* @param email
|
||||
* The email address for the account (utf8)
|
||||
* @param password
|
||||
* The user's password
|
||||
* @return Promise
|
||||
* Returns a promise that resolves to an object:
|
||||
* {
|
||||
* uid: the user's unique ID
|
||||
* sessionToken: a session token
|
||||
* }
|
||||
*/
|
||||
signUp: function (email, password) {
|
||||
let uid;
|
||||
let hexEmail = stringToHex(email);
|
||||
let uidPromise = this._request("/raw_password/account/create", "POST", null,
|
||||
{email: hexEmail, password: password});
|
||||
|
||||
return uidPromise.then((result) => {
|
||||
uid = result.uid;
|
||||
return this.signIn(email, password)
|
||||
.then(function(result) {
|
||||
result.uid = uid;
|
||||
return result;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Authenticate and create a new session with the Firefox Account API server
|
||||
*
|
||||
* @param email
|
||||
* The email address for the account (utf8)
|
||||
* @param password
|
||||
* The user's password
|
||||
* @return Promise
|
||||
* Returns a promise that resolves to an object:
|
||||
* {
|
||||
* uid: the user's unique ID
|
||||
* sessionToken: a session token
|
||||
* isVerified: flag indicating verification status of the email
|
||||
* }
|
||||
*/
|
||||
signIn: function signIn(email, password) {
|
||||
let hexEmail = stringToHex(email);
|
||||
return this._request("/raw_password/session/create", "POST", null,
|
||||
{email: hexEmail, password: password});
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroy the current session with the Firefox Account API server
|
||||
*
|
||||
* @param sessionTokenHex
|
||||
* The session token endcoded in hex
|
||||
* @return Promise
|
||||
*/
|
||||
signOut: function (sessionTokenHex) {
|
||||
return this._request("/session/destroy", "POST",
|
||||
this._deriveHawkCredentials(sessionTokenHex, "sessionToken"));
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the verification status of the user's FxA email address
|
||||
*
|
||||
* @param sessionTokenHex
|
||||
* The current session token endcoded in hex
|
||||
* @return Promise
|
||||
*/
|
||||
recoveryEmailStatus: function (sessionTokenHex) {
|
||||
return this._request("/recovery_email/status", "GET",
|
||||
this._deriveHawkCredentials(sessionTokenHex, "sessionToken"));
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve encryption keys
|
||||
*
|
||||
* @param keyFetchTokenHex
|
||||
* A one-time use key fetch token encoded in hex
|
||||
* @return Promise
|
||||
* Returns a promise that resolves to an object:
|
||||
* {
|
||||
* kA: an encryption key for recevorable data
|
||||
* wrapKB: an encryption key that requires knowledge of the user's password
|
||||
* }
|
||||
*/
|
||||
accountKeys: function (keyFetchTokenHex) {
|
||||
let creds = this._deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken");
|
||||
let keyRequestKey = creds.extra.slice(0, 32);
|
||||
let morecreds = CryptoUtils.hkdf(keyRequestKey, undefined,
|
||||
PREFIX_NAME + "account/keys", 3 * 32);
|
||||
let respHMACKey = morecreds.slice(0, 32);
|
||||
let respXORKey = morecreds.slice(32, 96);
|
||||
|
||||
return this._request("/account/keys", "GET", creds).then(resp => {
|
||||
if (!resp.bundle) {
|
||||
throw new Error("failed to retrieve keys");
|
||||
}
|
||||
|
||||
let bundle = CommonUtils.hexToBytes(resp.bundle);
|
||||
let mac = bundle.slice(-32);
|
||||
|
||||
let hasher = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256,
|
||||
CryptoUtils.makeHMACKey(respHMACKey));
|
||||
|
||||
let bundleMAC = CryptoUtils.digestBytes(bundle.slice(0, -32), hasher);
|
||||
if (mac !== bundleMAC) {
|
||||
throw new Error("error unbundling encryption keys");
|
||||
}
|
||||
|
||||
let keyAWrapB = CryptoUtils.xor(respXORKey, bundle.slice(0, 64));
|
||||
|
||||
return {
|
||||
kA: keyAWrapB.slice(0, 32),
|
||||
wrapKB: keyAWrapB.slice(32)
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends a public key to the FxA API server and returns a signed certificate
|
||||
*
|
||||
* @param sessionTokenHex
|
||||
* The current session token endcoded in hex
|
||||
* @param serializedPublicKey
|
||||
* A public key (usually generated by jwcrypto)
|
||||
* @param lifetime
|
||||
* The lifetime of the certificate
|
||||
* @return Promise
|
||||
* Returns a promise that resolves to the signed certificate. The certificate
|
||||
* can be used to generate a Persona assertion.
|
||||
*/
|
||||
signCertificate: function (sessionTokenHex, serializedPublicKey, lifetime) {
|
||||
let creds = this._deriveHawkCredentials(sessionTokenHex, "sessionToken");
|
||||
|
||||
let body = { publicKey: serializedPublicKey,
|
||||
duration: lifetime };
|
||||
return Promise.resolve()
|
||||
.then(_ => this._request("/certificate/sign", "POST", creds, body))
|
||||
.then(resp => resp.cert,
|
||||
err => {dump("HAWK.signCertificate error: " + err + "\n");
|
||||
throw err;});
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if an account exists
|
||||
*
|
||||
* @param email
|
||||
* The email address to check
|
||||
* @return Promise
|
||||
* The promise resolves to true if the account exists, or false
|
||||
* if it doesn't. The promise is rejected on other errors.
|
||||
*/
|
||||
accountExists: function (email) {
|
||||
let hexEmail = stringToHex(email);
|
||||
return this._request("/auth/start", "POST", null, { email: hexEmail })
|
||||
.then(
|
||||
// the account exists
|
||||
(result) => true,
|
||||
(err) => {
|
||||
// the account doesn't exist
|
||||
if (err.errno === 102) {
|
||||
return false;
|
||||
}
|
||||
// propogate other request errors
|
||||
throw err;
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* The FxA auth server expects requests to certain endpoints to be authorized using Hawk.
|
||||
* Hawk credentials are derived using shared secrets, which depend on the context
|
||||
* (e.g. sessionToken vs. keyFetchToken).
|
||||
*
|
||||
* @param tokenHex
|
||||
* The current session token endcoded in hex
|
||||
* @param context
|
||||
* A context for the credentials
|
||||
* @param size
|
||||
* The size in bytes of the expected derived buffer
|
||||
* @return credentials
|
||||
* Returns an object:
|
||||
* {
|
||||
* algorithm: sha256
|
||||
* id: the Hawk id (from the first 32 bytes derived)
|
||||
* key: the Hawk key (from bytes 32 to 64)
|
||||
* extra: size - 64 extra bytes
|
||||
* }
|
||||
*/
|
||||
_deriveHawkCredentials: function (tokenHex, context, size) {
|
||||
let token = CommonUtils.hexToBytes(tokenHex);
|
||||
let out = CryptoUtils.hkdf(token, undefined, PREFIX_NAME + context, size || 3 * 32);
|
||||
|
||||
return {
|
||||
algorithm: "sha256",
|
||||
key: out.slice(32, 64),
|
||||
extra: out.slice(64),
|
||||
id: CommonUtils.bytesAsHex(out.slice(0, 32))
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* A general method for sending raw API calls to the FxA auth server.
|
||||
* All request bodies and responses are JSON.
|
||||
*
|
||||
* @param path
|
||||
* API endpoint path
|
||||
* @param method
|
||||
* The HTTP request method
|
||||
* @param credentials
|
||||
* Hawk credentials
|
||||
* @param jsonPayload
|
||||
* A JSON payload
|
||||
* @return Promise
|
||||
* Returns a promise that resolves to the JSON response of the API call,
|
||||
* or is rejected with an error. Error responses have the following properties:
|
||||
* {
|
||||
* "code": 400, // matches the HTTP status code
|
||||
* "errno": 107, // stable application-level error number
|
||||
* "error": "Bad Request", // string description of the error type
|
||||
* "message": "the value of salt is not allowed to be undefined",
|
||||
* "info": "https://docs.dev.lcip.og/errors/1234" // link to more info on the error
|
||||
* }
|
||||
*/
|
||||
_request: function hawkRequest(path, method, credentials, jsonPayload) {
|
||||
let deferred = Promise.defer();
|
||||
let xhr = new XMLHttpRequest({mozSystem: true});
|
||||
let URI = this.host + path;
|
||||
let payload;
|
||||
|
||||
xhr.mozBackgroundRequest = true;
|
||||
|
||||
if (jsonPayload) {
|
||||
payload = JSON.stringify(jsonPayload);
|
||||
}
|
||||
|
||||
xhr.open(method, URI);
|
||||
xhr.channel.loadFlags = Ci.nsIChannel.LOAD_BYPASS_CACHE |
|
||||
Ci.nsIChannel.INHIBIT_CACHING;
|
||||
|
||||
// When things really blow up, reconstruct an error object that follows the general format
|
||||
// of the server on error responses.
|
||||
function constructError(err) {
|
||||
return { error: err, message: xhr.statusText, code: xhr.status, errno: xhr.status };
|
||||
}
|
||||
|
||||
xhr.onerror = function() {
|
||||
deferred.reject(constructError('Request failed'));
|
||||
};
|
||||
|
||||
xhr.onload = function onload() {
|
||||
try {
|
||||
let response = JSON.parse(xhr.responseText);
|
||||
if (xhr.status !== 200 || response.error) {
|
||||
// In this case, the response is an object with error information.
|
||||
return deferred.reject(response);
|
||||
}
|
||||
deferred.resolve(response);
|
||||
} catch (e) {
|
||||
deferred.reject(constructError(e));
|
||||
}
|
||||
};
|
||||
|
||||
let uri = Services.io.newURI(URI, null, null);
|
||||
|
||||
if (credentials) {
|
||||
let header = CryptoUtils.computeHAWK(uri, method, {
|
||||
credentials: credentials,
|
||||
payload: payload,
|
||||
contentType: "application/json"
|
||||
});
|
||||
xhr.setRequestHeader("authorization", header.field);
|
||||
}
|
||||
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.send(payload);
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
};
|
||||
|
8
services/fxaccounts/moz.build
Normal file
8
services/fxaccounts/moz.build
Normal file
@ -0,0 +1,8 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
TEST_DIRS += ['tests']
|
||||
EXTRA_JS_MODULES += ['FxAccountsClient.jsm']
|
7
services/fxaccounts/tests/moz.build
Normal file
7
services/fxaccounts/tests/moz.build
Normal file
@ -0,0 +1,7 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += ['xpcshell/xpcshell.ini']
|
18
services/fxaccounts/tests/xpcshell/head.js
Normal file
18
services/fxaccounts/tests/xpcshell/head.js
Normal file
@ -0,0 +1,18 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
|
||||
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
(function initFxAccountsTestingInfrastructure() {
|
||||
do_get_profile();
|
||||
|
||||
let ns = {};
|
||||
Cu.import("resource://testing-common/services-common/logging.js", ns);
|
||||
|
||||
ns.initTestLogging("Trace");
|
||||
}).call(this);
|
||||
|
231
services/fxaccounts/tests/xpcshell/test_client.js
Normal file
231
services/fxaccounts/tests/xpcshell/test_client.js
Normal file
@ -0,0 +1,231 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
function deferredStop(server) {
|
||||
let deferred = Promise.defer();
|
||||
server.stop(deferred.resolve);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
add_test(function test_hawk_credentials() {
|
||||
let client = new FxAccountsClient();
|
||||
|
||||
let sessionToken = "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf";
|
||||
let result = client._deriveHawkCredentials(sessionToken, "session");
|
||||
|
||||
do_check_eq(result.id, "639503a218ffbb62983e9628be5cd64a0438d0ae81b2b9dadeb900a83470bc6b");
|
||||
do_check_eq(CommonUtils.bytesAsHex(result.key), "3a0188943837ab228fe74e759566d0e4837cbcc7494157aac4da82025b2811b2");
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_authenticated_get_request() {
|
||||
|
||||
let message = "{\"msg\": \"Great Success!\"}";
|
||||
let credentials = {
|
||||
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
|
||||
key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
|
||||
algorithm: "sha256"
|
||||
};
|
||||
let method = "GET";
|
||||
|
||||
let server = httpd_setup({"/foo": function(request, response) {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(message, message.length);
|
||||
}
|
||||
});
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
|
||||
let result = yield client._request("/foo", method, credentials);
|
||||
do_check_eq("Great Success!", result.msg);
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_authenticated_post_request() {
|
||||
let credentials = {
|
||||
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
|
||||
key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
|
||||
algorithm: "sha256"
|
||||
};
|
||||
let method = "POST";
|
||||
|
||||
let server = httpd_setup({"/foo": function(request, response) {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.bodyOutputStream.writeFrom(request.bodyInputStream, request.bodyInputStream.available());
|
||||
}
|
||||
});
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
|
||||
let result = yield client._request("/foo", method, credentials, {foo: "bar"});
|
||||
do_check_eq("bar", result.foo);
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_500_error() {
|
||||
|
||||
let message = "<h1>Ooops!</h1>";
|
||||
let method = "GET";
|
||||
|
||||
let server = httpd_setup({"/foo": function(request, response) {
|
||||
response.setStatusLine(request.httpVersion, 500, "Internal Server Error");
|
||||
response.bodyOutputStream.write(message, message.length);
|
||||
}
|
||||
});
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
|
||||
try {
|
||||
yield client._request("/foo", method);
|
||||
} catch (e) {
|
||||
do_check_eq(500, e.code);
|
||||
do_check_eq("Internal Server Error", e.message);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_api_endpoints() {
|
||||
let sessionMessage = JSON.stringify({sessionToken: "NotARealToken"});
|
||||
let creationMessage = JSON.stringify({uid: "NotARealUid"});
|
||||
let signoutMessage = JSON.stringify({});
|
||||
let certSignMessage = JSON.stringify({cert: {bar: "baz"}});
|
||||
let emailStatus = JSON.stringify({verified: true});
|
||||
|
||||
let authStarts = 0;
|
||||
|
||||
function writeResp(response, msg) {
|
||||
response.bodyOutputStream.write(msg, msg.length);
|
||||
}
|
||||
|
||||
let server = httpd_setup(
|
||||
{
|
||||
"/raw_password/account/create": function(request, response) {
|
||||
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
||||
let jsonBody = JSON.parse(body);
|
||||
do_check_eq(jsonBody.email, "796f75406578616d706c652e636f6d");
|
||||
do_check_eq(jsonBody.password, "biggersecret");
|
||||
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(creationMessage, creationMessage.length);
|
||||
},
|
||||
"/raw_password/session/create": function(request, response) {
|
||||
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
||||
let jsonBody = JSON.parse(body);
|
||||
if (jsonBody.password === "bigsecret") {
|
||||
do_check_eq(jsonBody.email, "6dc3a9406578616d706c652e636f6d");
|
||||
} else if (jsonBody.password === "biggersecret") {
|
||||
do_check_eq(jsonBody.email, "796f75406578616d706c652e636f6d");
|
||||
}
|
||||
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(sessionMessage, sessionMessage.length);
|
||||
},
|
||||
"/recovery_email/status": function(request, response) {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(emailStatus, emailStatus.length);
|
||||
},
|
||||
"/session/destroy": function(request, response) {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(signoutMessage, signoutMessage.length);
|
||||
},
|
||||
"/certificate/sign": function(request, response) {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
||||
let jsonBody = JSON.parse(body);
|
||||
do_check_eq(JSON.parse(jsonBody.publicKey).foo, "bar");
|
||||
do_check_eq(jsonBody.duration, 600);
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(certSignMessage, certSignMessage.length);
|
||||
},
|
||||
"/auth/start": function(request, response) {
|
||||
if (authStarts === 0) {
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
writeResp(response, JSON.stringify({}));
|
||||
} else if (authStarts === 1) {
|
||||
response.setStatusLine(request.httpVersion, 400, "NOT OK");
|
||||
writeResp(response, JSON.stringify({errno: 102, error: "no such account"}));
|
||||
} else if (authStarts === 2) {
|
||||
response.setStatusLine(request.httpVersion, 400, "NOT OK");
|
||||
writeResp(response, JSON.stringify({errno: 107, error: "boom"}));
|
||||
}
|
||||
authStarts++;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
let result = undefined;
|
||||
|
||||
result = yield client.signUp('you@example.com', 'biggersecret');
|
||||
do_check_eq("NotARealUid", result.uid);
|
||||
|
||||
result = yield client.signIn('mé@example.com', 'bigsecret');
|
||||
do_check_eq("NotARealToken", result.sessionToken);
|
||||
|
||||
result = yield client.signOut('NotARealToken');
|
||||
do_check_eq(typeof result, "object");
|
||||
|
||||
result = yield client.recoveryEmailStatus('NotARealToken');
|
||||
do_check_eq(result.verified, true);
|
||||
|
||||
result = yield client.signCertificate('NotARealToken', JSON.stringify({foo: "bar"}), 600);
|
||||
do_check_eq("baz", result.bar);
|
||||
|
||||
result = yield client.accountExists('hey@example.com');
|
||||
do_check_eq(result, true);
|
||||
result = yield client.accountExists('hey2@example.com');
|
||||
do_check_eq(result, false);
|
||||
try {
|
||||
result = yield client.accountExists('hey3@example.com');
|
||||
} catch(e) {
|
||||
do_check_eq(e.errno, 107);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_error_response() {
|
||||
let errorMessage = JSON.stringify({error: "Oops", code: 400, errno: 99});
|
||||
|
||||
let server = httpd_setup(
|
||||
{
|
||||
"/raw_password/session/create": function(request, response) {
|
||||
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
||||
|
||||
response.setStatusLine(request.httpVersion, 400, "NOT OK");
|
||||
response.bodyOutputStream.write(errorMessage, errorMessage.length);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
|
||||
try {
|
||||
let result = yield client.signIn('mé@example.com', 'bigsecret');
|
||||
} catch(result) {
|
||||
do_check_eq("Oops", result.error);
|
||||
do_check_eq(400, result.code);
|
||||
do_check_eq(99, result.errno);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
6
services/fxaccounts/tests/xpcshell/xpcshell.ini
Normal file
6
services/fxaccounts/tests/xpcshell/xpcshell.ini
Normal file
@ -0,0 +1,6 @@
|
||||
[DEFAULT]
|
||||
head = head.js ../../../common/tests/unit/head_helpers.js ../../../common/tests/unit/head_http.js
|
||||
tail =
|
||||
|
||||
[test_client.js]
|
||||
|
@ -7,6 +7,7 @@
|
||||
PARALLEL_DIRS += [
|
||||
'common',
|
||||
'crypto',
|
||||
'fxaccounts',
|
||||
]
|
||||
|
||||
if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
|
||||
|
Loading…
Reference in New Issue
Block a user