Bug 1665420 - Remove legacy BrowserID crypto code. r=markh

Differential Revision: https://phabricator.services.mozilla.com/D107985
This commit is contained in:
Ryan Kelly 2021-03-17 00:38:08 +00:00
parent b7f213243a
commit f6a46d059d
43 changed files with 325 additions and 3119 deletions

View File

@ -1579,12 +1579,6 @@ pref("identity.fxaccounts.remote.pairing.uri", "wss://channelserver.services.moz
// Token server used by the FxA Sync identity.
pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5");
// Fetch Sync tokens using the OAuth token function
pref("identity.sync.useOAuthForSyncToken", true);
// Using session tokens to fetch OAuth tokens
pref("identity.fxaccounts.useSessionTokensForOAuth", true);
// Auto-config URL for FxA self-hosters, makes an HTTP request to
// [identity.fxaccounts.autoconfig.uri]/.well-known/fxa-client-configuration
// This is now the prefered way of pointing to a custom FxA server, instead

View File

@ -695,7 +695,7 @@ RESTResponse.prototype = {
* (Object) An auth token of the form {id: (string), key: (string)}
* from which the MAC Authentication header for this request will be
* derived. A token as obtained from
* TokenServerClient.getTokenFromBrowserIDAssertion is accepted.
* TokenServerClient.getTokenUsingOAuth is accepted.
* @param extra
* (Object) Optional extra parameters. Valid keys are: nonce_bytes, ts,
* nonce, and ext. See CrytoUtils.computeHTTPMACSHA1 for information on

View File

@ -18,8 +18,6 @@ add_task(async function test_authenticated_request() {
let message = "Great Success!";
// TODO: We use a preset key here, but use getTokenFromBrowserIDAssertion()
// from TokenServerClient to get a real one when possible. (Bug 745800)
let id = "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x";
let key = "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=";
let method = "GET";

View File

@ -9,8 +9,8 @@ const {
initTestLogging("Trace");
add_task(async function test_working_bid_exchange() {
_("Ensure that working BrowserID token exchange works as expected.");
add_task(async function test_working_token_exchange() {
_("Ensure that working OAuth token exchange works as expected.");
let service = "http://example.com/foo";
let duration = 300;
@ -18,7 +18,6 @@ add_task(async function test_working_bid_exchange() {
let server = httpd_setup({
"/1.0/foo/1.0": function(request, response) {
Assert.ok(request.hasHeader("accept"));
Assert.ok(!request.hasHeader("x-conditions-accepted"));
Assert.equal("application/json", request.getHeader("accept"));
response.setStatusLine(request.httpVersion, 200, "OK");
@ -37,7 +36,7 @@ add_task(async function test_working_bid_exchange() {
let client = new TokenServerClient();
let url = server.baseURI + "/1.0/foo/1.0";
let result = await client.getTokenFromBrowserIDAssertion(url, "assertion");
let result = await client.getTokenUsingOAuth(url, "access_token");
Assert.equal("object", typeof result);
do_check_attribute_count(result, 7);
Assert.equal(service, result.endpoint);
@ -49,7 +48,7 @@ add_task(async function test_working_bid_exchange() {
await promiseStopServer(server);
});
add_task(async function test_working_bid_exchange_with_nodetype() {
add_task(async function test_working_token_exchange_with_nodetype() {
_("Ensure that a token response with a node type as expected.");
let service = "http://example.com/foo";
@ -59,7 +58,6 @@ add_task(async function test_working_bid_exchange_with_nodetype() {
let server = httpd_setup({
"/1.0/foo/1.0": function(request, response) {
Assert.ok(request.hasHeader("accept"));
Assert.ok(!request.hasHeader("x-conditions-accepted"));
Assert.equal("application/json", request.getHeader("accept"));
response.setStatusLine(request.httpVersion, 200, "OK");
@ -79,7 +77,7 @@ add_task(async function test_working_bid_exchange_with_nodetype() {
let client = new TokenServerClient();
let url = server.baseURI + "/1.0/foo/1.0";
let result = await client.getTokenFromBrowserIDAssertion(url, "assertion");
let result = await client.getTokenUsingOAuth(url, "access_token");
Assert.equal("object", typeof result);
do_check_attribute_count(result, 7);
Assert.equal(service, result.endpoint);
@ -95,60 +93,17 @@ add_task(async function test_invalid_arguments() {
_("Ensure invalid arguments to APIs are rejected.");
let args = [
[null, "assertion"],
[null, "access_token"],
["http://example.com/", null],
];
for (let arg of args) {
let client = new TokenServerClient();
await Assert.rejects(
client.getTokenFromBrowserIDAssertion(arg[0], arg[1]),
ex => {
Assert.ok(ex instanceof TokenServerClientError);
return true;
}
);
}
});
add_task(async function test_conditions_required_response_handling() {
_("Ensure that a conditions required response is handled properly.");
let description = "Need to accept conditions";
let tosURL = "http://example.com/tos";
let server = httpd_setup({
"/1.0/foo/1.0": function(request, response) {
Assert.ok(!request.hasHeader("x-conditions-accepted"));
response.setStatusLine(request.httpVersion, 403, "Forbidden");
response.setHeader("Content-Type", "application/json");
let body = JSON.stringify({
errors: [{ description, location: "body", name: "" }],
urls: { tos: tosURL },
});
response.bodyOutputStream.write(body, body.length);
},
});
let client = new TokenServerClient();
let url = server.baseURI + "/1.0/foo/1.0";
await Assert.rejects(
client.getTokenFromBrowserIDAssertion(url, "assertion"),
error => {
Assert.ok(error instanceof TokenServerClientServerError);
Assert.equal(error.cause, "conditions-required");
// Check a JSON.stringify works on our errors as our logging will try and use it.
Assert.ok(JSON.stringify(error), "JSON.stringify worked");
Assert.equal(error.urls.tos, tosURL);
await Assert.rejects(client.getTokenUsingOAuth(arg[0], arg[1]), ex => {
Assert.ok(ex instanceof TokenServerClientError);
return true;
}
);
await promiseStopServer(server);
});
}
});
add_task(async function test_invalid_403_no_content_type() {
@ -171,7 +126,7 @@ add_task(async function test_invalid_403_no_content_type() {
let url = server.baseURI + "/1.0/foo/1.0";
await Assert.rejects(
client.getTokenFromBrowserIDAssertion(url, "assertion"),
client.getTokenUsingOAuth(url, "access_token"),
error => {
Assert.ok(error instanceof TokenServerClientServerError);
Assert.equal(error.cause, "malformed-response");
@ -184,65 +139,6 @@ add_task(async function test_invalid_403_no_content_type() {
await promiseStopServer(server);
});
add_task(async function test_invalid_403_bad_json() {
_("Ensure that a 403 with JSON that isn't proper is handled properly.");
let server = httpd_setup({
"/1.0/foo/1.0": function(request, response) {
response.setStatusLine(request.httpVersion, 403, "Forbidden");
response.setHeader("Content-Type", "application/json; charset=utf-8");
let body = JSON.stringify({
foo: "bar",
});
response.bodyOutputStream.write(body, body.length);
},
});
let client = new TokenServerClient();
let url = server.baseURI + "/1.0/foo/1.0";
await Assert.rejects(
client.getTokenFromBrowserIDAssertion(url, "assertion"),
error => {
Assert.ok(error instanceof TokenServerClientServerError);
Assert.equal(error.cause, "malformed-response");
Assert.equal(null, error.urls);
return true;
}
);
await promiseStopServer(server);
});
add_task(async function test_403_no_urls() {
_("Ensure that a 403 without a urls field is handled properly.");
let server = httpd_setup({
"/1.0/foo/1.0": function(request, response) {
response.setStatusLine(request.httpVersion, 403, "Forbidden");
response.setHeader("Content-Type", "application/json; charset=utf-8");
let body = "{}";
response.bodyOutputStream.write(body, body.length);
},
});
let client = new TokenServerClient();
let url = server.baseURI + "/1.0/foo/1.0";
await Assert.rejects(
client.getTokenFromBrowserIDAssertion(url, "assertion"),
error => {
Assert.ok(error instanceof TokenServerClientServerError);
Assert.equal(error.cause, "malformed-response");
return true;
}
);
await promiseStopServer(server);
});
add_task(async function test_send_extra_headers() {
_("Ensures that the condition acceptance header is sent when asked.");
@ -277,7 +173,7 @@ add_task(async function test_send_extra_headers() {
"X-Bar": 17,
};
await client.getTokenFromBrowserIDAssertion(url, "assertion", extra);
await client.getTokenUsingOAuth(url, "access_token", extra);
// Other tests validate other things.
await promiseStopServer(server);
@ -292,7 +188,7 @@ add_task(async function test_error_404_empty() {
let url = server.baseURI + "/foo";
await Assert.rejects(
client.getTokenFromBrowserIDAssertion(url, "assertion"),
client.getTokenUsingOAuth(url, "access_token"),
error => {
Assert.ok(error instanceof TokenServerClientServerError);
Assert.equal(error.cause, "malformed-response");
@ -326,7 +222,7 @@ add_task(async function test_error_404_proper_response() {
let url = server.baseURI + "/1.0/foo/1.0";
await Assert.rejects(
client.getTokenFromBrowserIDAssertion(url, "assertion"),
client.getTokenUsingOAuth(url, "access_token"),
error => {
Assert.ok(error instanceof TokenServerClientServerError);
Assert.equal(error.cause, "unknown-service");
@ -354,7 +250,7 @@ add_task(async function test_bad_json() {
let url = server.baseURI + "/1.0/foo/1.0";
await Assert.rejects(
client.getTokenFromBrowserIDAssertion(url, "assertion"),
client.getTokenUsingOAuth(url, "access_token"),
error => {
Assert.notEqual(null, error);
Assert.equal("TokenServerClientServerError", error.name);
@ -384,7 +280,7 @@ add_task(async function test_400_response() {
let url = server.baseURI + "/1.0/foo/1.0";
await Assert.rejects(
client.getTokenFromBrowserIDAssertion(url, "assertion"),
client.getTokenUsingOAuth(url, "access_token"),
error => {
Assert.notEqual(null, error);
Assert.equal("TokenServerClientServerError", error.name);
@ -414,7 +310,7 @@ add_task(async function test_401_with_error_cause() {
let url = server.baseURI + "/1.0/foo/1.0";
await Assert.rejects(
client.getTokenFromBrowserIDAssertion(url, "assertion"),
client.getTokenUsingOAuth(url, "access_token"),
error => {
Assert.notEqual(null, error);
Assert.equal("TokenServerClientServerError", error.name);
@ -444,7 +340,7 @@ add_task(async function test_unhandled_media_type() {
let client = new TokenServerClient();
await Assert.rejects(
client.getTokenFromBrowserIDAssertion(url, "assertion"),
client.getTokenUsingOAuth(url, "access_token"),
error => {
Assert.notEqual(null, error);
Assert.equal("TokenServerClientServerError", error.name);
@ -479,6 +375,6 @@ add_task(async function test_rich_media_types() {
let url = server.baseURI + "/foo";
let client = new TokenServerClient();
await client.getTokenFromBrowserIDAssertion(url, "assertion");
await client.getTokenUsingOAuth(url, "access_token");
await promiseStopServer(server);
});

View File

@ -74,12 +74,6 @@ TokenServerClientNetworkError.prototype._toStringFields = function() {
* server. The type of error is strongly enumerated and is stored in the
* `cause` property. This property can have the following string values:
*
* conditions-required -- The server is requesting that the client
* agree to service conditions before it can obtain a token. The
* conditions that must be presented to the user and agreed to are in
* the `urls` mapping on the instance. Keys of this mapping are
* identifiers. Values are string URLs.
*
* invalid-credentials -- A token could not be obtained because
* the credentials presented by the client were invalid.
*
@ -126,15 +120,10 @@ TokenServerClientServerError.prototype._toStringFields = function() {
*
* http://docs.services.mozilla.com/token/index.html
*
* The Token Server supports obtaining tokens for arbitrary apps by
* constructing URI paths of the form <app>/<app_version>. However, the service
* discovery mechanism emphasizes the use of full URIs and tries to not force
* the client to manipulate URIs. This client currently enforces this practice
* by not implementing an API which would perform URI manipulation.
*
* If you are tempted to implement this API in the future, consider this your
* warning that you may be doing it wrong and that you should store full URIs
* instead.
* The Token Server was designed to support obtaining tokens for arbitrary apps by
* constructing URI paths of the form <app>/<app_version>. In practice this was
* never used and it only supports an <app> value of `sync`, and the API presented
* here reflects that.
*
* Areas to Improve:
*
@ -155,7 +144,7 @@ TokenServerClient.prototype = {
_log: null,
/**
* Obtain a token from a BrowserID assertion against a specific URL.
* Obtain a token from a provided OAuth token against a specific URL.
*
* This asynchronously obtains the token.
* It returns a Promise that resolves or rejects:
@ -179,66 +168,20 @@ TokenServerClient.prototype = {
* uid (string) user ID for requested service.
* duration (string) the validity duration of the issued token.
*
* Terms of Service Acceptance
* ---------------------------
*
* Some services require users to accept terms of service before they can
* obtain a token. If a service requires ToS acceptance, the error passed
* to the callback will be a `TokenServerClientServerError` with the
* `cause` property set to "conditions-required". The `urls` property of that
* instance will be a map of string keys to string URL values. The user-agent
* should prompt the user to accept the content at these URLs.
*
* Clients signify acceptance of the terms of service by sending a token
* request with additional metadata. This is controlled by the
* `conditionsAccepted` argument to this function. Clients only need to set
* this flag once per service and the server remembers acceptance. If
* the conditions for the service change, the server may request
* clients agree to terms again. Therefore, clients should always be
* prepared to handle a conditions required response.
*
* Clients should not blindly send acceptance to conditions. Instead, clients
* should set `conditionsAccepted` if and only if the server asks for
* acceptance, the conditions are displayed to the user, and the user agrees
* to them.
*
* Example Usage
* -------------
*
* let client = new TokenServerClient();
* let assertion = getBrowserIDAssertionFromSomewhere();
* let access_token = getOAuthAccessTokenFromSomewhere();
* let url = "https://token.services.mozilla.com/1.0/sync/2.0";
*
* try {
* const result = await client.getTokenFromBrowserIDAssertion(url, assertion);
* const result = await client.getTokenUsingOAuth(url, access_token);
* let {id, key, uid, endpoint, duration} = result;
* // Do stuff with data and carry on.
* } catch (error) {
* // Handle errors.
* }
*
* @param url
* (string) URL to fetch token from.
* @param assertion
* (string) BrowserID assertion to exchange token for.
* @param addHeaders
* (object) Extra headers for the request.
*/
async getTokenFromBrowserIDAssertion(url, assertion, addHeaders = {}) {
this._log.debug("Beginning BID assertion exchange: " + url);
if (!assertion) {
throw new TokenServerClientError("assertion argument is not valid.");
}
return this._tokenServerExchangeRequest(
url,
`BrowserID ${assertion}`,
addHeaders
);
},
/**
* Obtain a token from a provided OAuth token against a specific URL.
*
* @param url
@ -248,7 +191,7 @@ TokenServerClient.prototype = {
* @param addHeaders
* (object) Extra headers for the request.
*/
async getTokenFromOAuthToken(url, oauthToken, addHeaders = {}) {
async getTokenUsingOAuth(url, oauthToken, addHeaders = {}) {
this._log.debug("Beginning OAuth token exchange: " + url);
if (!oauthToken) {
@ -377,26 +320,6 @@ TokenServerClient.prototype = {
// invalid-generation.
error.message = "Authentication failed.";
error.cause = result.status;
} else if (response.status == 403) {
// 403 should represent a "condition acceptance needed" response.
//
// The extra validation of "urls" is important. We don't want to signal
// conditions required unless we are absolutely sure that is what the
// server is asking for.
if (!("urls" in result)) {
this._log.warn("403 response without proper fields!");
this._log.warn("Response body: " + response.body);
error.message = "Missing JSON fields.";
error.cause = "malformed-response";
} else if (typeof result.urls != "object") {
error.message = "urls field is not a map.";
error.cause = "malformed-response";
} else {
error.message = "Conditions must be accepted.";
error.cause = "conditions-required";
error.urls = result.urls;
}
} else if (response.status == 404) {
error.message = "Unknown service.";
error.cause = "unknown-service";

View File

@ -1,461 +0,0 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* 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/. */
#include "nsIIdentityCryptoService.h"
#include "nsServiceManagerUtils.h"
#include "nsIThread.h"
#include "nsThreadUtils.h"
#include "nsCOMPtr.h"
#include "nsProxyRelease.h"
#include "nsString.h"
#include "mozilla/ArrayUtils.h" // ArrayLength
#include "mozilla/Base64.h"
#include "mozilla/Components.h"
#include "ScopedNSSTypes.h"
#include "NSSErrorsService.h"
#include "nss.h"
#include "pk11pub.h"
#include "secmod.h"
#include "secerr.h"
#include "keyhi.h"
#include "cryptohi.h"
#include <limits.h>
using namespace mozilla;
namespace {
void HexEncode(const SECItem* it, nsACString& result) {
static const char digits[] = "0123456789ABCDEF";
result.SetLength(it->len * 2);
char* p = result.BeginWriting();
for (unsigned int i = 0; i < it->len; ++i) {
*p++ = digits[it->data[i] >> 4];
*p++ = digits[it->data[i] & 0x0f];
}
}
#define DSA_KEY_TYPE_STRING ("DS160"_ns)
#define RSA_KEY_TYPE_STRING ("RS256"_ns)
class KeyPair : public nsIIdentityKeyPair {
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIIDENTITYKEYPAIR
KeyPair(SECKEYPrivateKey* aPrivateKey, SECKEYPublicKey* aPublicKey,
nsIEventTarget* aOperationThread);
private:
virtual ~KeyPair() {
if (mPrivateKey) {
SECKEY_DestroyPrivateKey(mPrivateKey);
}
if (mPublicKey) {
SECKEY_DestroyPublicKey(mPublicKey);
}
}
SECKEYPrivateKey* mPrivateKey;
SECKEYPublicKey* mPublicKey;
nsCOMPtr<nsIEventTarget> mThread;
KeyPair(const KeyPair&) = delete;
void operator=(const KeyPair&) = delete;
};
NS_IMPL_ISUPPORTS(KeyPair, nsIIdentityKeyPair)
class KeyGenRunnable : public Runnable {
public:
NS_DECL_NSIRUNNABLE
KeyGenRunnable(KeyType keyType, nsIIdentityKeyGenCallback* aCallback,
nsIEventTarget* aOperationThread);
private:
const KeyType mKeyType; // in
nsMainThreadPtrHandle<nsIIdentityKeyGenCallback> mCallback; // in
nsresult mRv; // out
nsCOMPtr<nsIIdentityKeyPair> mKeyPair; // out
nsCOMPtr<nsIEventTarget> mThread;
KeyGenRunnable(const KeyGenRunnable&) = delete;
void operator=(const KeyGenRunnable&) = delete;
};
class SignRunnable : public Runnable {
public:
NS_DECL_NSIRUNNABLE
SignRunnable(const nsACString& textToSign, SECKEYPrivateKey* privateKey,
nsIIdentitySignCallback* aCallback);
private:
~SignRunnable() override {
if (mPrivateKey) {
SECKEY_DestroyPrivateKey(mPrivateKey);
}
}
const nsCString mTextToSign; // in
SECKEYPrivateKey* mPrivateKey; // in
nsMainThreadPtrHandle<nsIIdentitySignCallback> mCallback; // in
nsresult mRv; // out
nsCString mSignature; // out
SignRunnable(const SignRunnable&) = delete;
void operator=(const SignRunnable&) = delete;
};
class IdentityCryptoService final : public nsIIdentityCryptoService {
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIIDENTITYCRYPTOSERVICE
IdentityCryptoService() = default;
nsresult Init() {
nsresult rv;
nsCOMPtr<nsISupports> dummyUsedToEnsureNSSIsInitialized =
do_GetService("@mozilla.org/psm;1", &rv);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIThread> thread;
rv = NS_NewNamedThread("IdentityCrypto", getter_AddRefs(thread));
NS_ENSURE_SUCCESS(rv, rv);
mThread = std::move(thread);
return NS_OK;
}
private:
~IdentityCryptoService() = default;
IdentityCryptoService(const KeyPair&) = delete;
void operator=(const IdentityCryptoService&) = delete;
nsCOMPtr<nsIEventTarget> mThread;
};
NS_IMPL_ISUPPORTS(IdentityCryptoService, nsIIdentityCryptoService)
NS_IMETHODIMP
IdentityCryptoService::GenerateKeyPair(const nsACString& keyTypeString,
nsIIdentityKeyGenCallback* callback) {
KeyType keyType;
if (keyTypeString.Equals(RSA_KEY_TYPE_STRING)) {
keyType = rsaKey;
} else if (keyTypeString.Equals(DSA_KEY_TYPE_STRING)) {
keyType = dsaKey;
} else {
return NS_ERROR_UNEXPECTED;
}
nsCOMPtr<nsIRunnable> r = new KeyGenRunnable(keyType, callback, mThread);
nsresult rv = mThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
NS_IMETHODIMP
IdentityCryptoService::Base64UrlEncode(const nsACString& utf8Input,
nsACString& result) {
return Base64URLEncode(
utf8Input.Length(),
reinterpret_cast<const uint8_t*>(utf8Input.BeginReading()),
Base64URLEncodePaddingPolicy::Include, result);
}
KeyPair::KeyPair(SECKEYPrivateKey* privateKey, SECKEYPublicKey* publicKey,
nsIEventTarget* operationThread)
: mPrivateKey(privateKey), mPublicKey(publicKey), mThread(operationThread) {
MOZ_ASSERT(!NS_IsMainThread());
}
NS_IMETHODIMP
KeyPair::GetHexRSAPublicKeyExponent(nsACString& result) {
MOZ_ASSERT(NS_IsMainThread());
NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
NS_ENSURE_TRUE(mPublicKey->keyType == rsaKey, NS_ERROR_NOT_AVAILABLE);
HexEncode(&mPublicKey->u.rsa.publicExponent, result);
return NS_OK;
}
NS_IMETHODIMP
KeyPair::GetHexRSAPublicKeyModulus(nsACString& result) {
MOZ_ASSERT(NS_IsMainThread());
NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
NS_ENSURE_TRUE(mPublicKey->keyType == rsaKey, NS_ERROR_NOT_AVAILABLE);
HexEncode(&mPublicKey->u.rsa.modulus, result);
return NS_OK;
}
NS_IMETHODIMP
KeyPair::GetHexDSAPrime(nsACString& result) {
MOZ_ASSERT(NS_IsMainThread());
NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
NS_ENSURE_TRUE(mPublicKey->keyType == dsaKey, NS_ERROR_NOT_AVAILABLE);
HexEncode(&mPublicKey->u.dsa.params.prime, result);
return NS_OK;
}
NS_IMETHODIMP
KeyPair::GetHexDSASubPrime(nsACString& result) {
MOZ_ASSERT(NS_IsMainThread());
NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
NS_ENSURE_TRUE(mPublicKey->keyType == dsaKey, NS_ERROR_NOT_AVAILABLE);
HexEncode(&mPublicKey->u.dsa.params.subPrime, result);
return NS_OK;
}
NS_IMETHODIMP
KeyPair::GetHexDSAGenerator(nsACString& result) {
MOZ_ASSERT(NS_IsMainThread());
NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
NS_ENSURE_TRUE(mPublicKey->keyType == dsaKey, NS_ERROR_NOT_AVAILABLE);
HexEncode(&mPublicKey->u.dsa.params.base, result);
return NS_OK;
}
NS_IMETHODIMP
KeyPair::GetHexDSAPublicValue(nsACString& result) {
MOZ_ASSERT(NS_IsMainThread());
NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
NS_ENSURE_TRUE(mPublicKey->keyType == dsaKey, NS_ERROR_NOT_AVAILABLE);
HexEncode(&mPublicKey->u.dsa.publicValue, result);
return NS_OK;
}
NS_IMETHODIMP
KeyPair::GetKeyType(nsACString& result) {
MOZ_ASSERT(NS_IsMainThread());
NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
switch (mPublicKey->keyType) {
case rsaKey:
result = RSA_KEY_TYPE_STRING;
return NS_OK;
case dsaKey:
result = DSA_KEY_TYPE_STRING;
return NS_OK;
default:
return NS_ERROR_UNEXPECTED;
}
}
NS_IMETHODIMP
KeyPair::Sign(const nsACString& textToSign, nsIIdentitySignCallback* callback) {
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsIRunnable> r = new SignRunnable(textToSign, mPrivateKey, callback);
return mThread->Dispatch(r, NS_DISPATCH_NORMAL);
}
KeyGenRunnable::KeyGenRunnable(KeyType keyType,
nsIIdentityKeyGenCallback* callback,
nsIEventTarget* operationThread)
: mozilla::Runnable("KeyGenRunnable"),
mKeyType(keyType),
mCallback(new nsMainThreadPtrHolder<nsIIdentityKeyGenCallback>(
"KeyGenRunnable::mCallback", callback)),
mRv(NS_ERROR_NOT_INITIALIZED),
mThread(operationThread) {}
[[nodiscard]] nsresult GenerateKeyPair(PK11SlotInfo* slot,
SECKEYPrivateKey** privateKey,
SECKEYPublicKey** publicKey,
CK_MECHANISM_TYPE mechanism,
void* params) {
*publicKey = nullptr;
*privateKey = PK11_GenerateKeyPair(
slot, mechanism, params, publicKey, PR_FALSE /*isPerm*/,
PR_TRUE /*isSensitive*/, nullptr /*&pwdata*/);
if (!*privateKey) {
MOZ_ASSERT(!*publicKey);
return mozilla::psm::GetXPCOMFromNSSError(PR_GetError());
}
if (!*publicKey) {
SECKEY_DestroyPrivateKey(*privateKey);
*privateKey = nullptr;
MOZ_CRASH("PK11_GnerateKeyPair returned private key without public key");
}
return NS_OK;
}
[[nodiscard]] nsresult GenerateRSAKeyPair(PK11SlotInfo* slot,
SECKEYPrivateKey** privateKey,
SECKEYPublicKey** publicKey) {
MOZ_ASSERT(!NS_IsMainThread());
PK11RSAGenParams rsaParams;
rsaParams.keySizeInBits = 2048;
rsaParams.pe = 0x10001;
return GenerateKeyPair(slot, privateKey, publicKey, CKM_RSA_PKCS_KEY_PAIR_GEN,
&rsaParams);
}
[[nodiscard]] nsresult GenerateDSAKeyPair(PK11SlotInfo* slot,
SECKEYPrivateKey** privateKey,
SECKEYPublicKey** publicKey) {
MOZ_ASSERT(!NS_IsMainThread());
// XXX: These could probably be static const arrays, but this way we avoid
// compiler warnings and also we avoid having to worry much about whether the
// functions that take these inputs will (unexpectedly) modify them.
// Using NIST parameters. Some other BrowserID components require that these
// exact parameters are used.
uint8_t P[] = {
0xFF, 0x60, 0x04, 0x83, 0xDB, 0x6A, 0xBF, 0xC5, 0xB4, 0x5E, 0xAB, 0x78,
0x59, 0x4B, 0x35, 0x33, 0xD5, 0x50, 0xD9, 0xF1, 0xBF, 0x2A, 0x99, 0x2A,
0x7A, 0x8D, 0xAA, 0x6D, 0xC3, 0x4F, 0x80, 0x45, 0xAD, 0x4E, 0x6E, 0x0C,
0x42, 0x9D, 0x33, 0x4E, 0xEE, 0xAA, 0xEF, 0xD7, 0xE2, 0x3D, 0x48, 0x10,
0xBE, 0x00, 0xE4, 0xCC, 0x14, 0x92, 0xCB, 0xA3, 0x25, 0xBA, 0x81, 0xFF,
0x2D, 0x5A, 0x5B, 0x30, 0x5A, 0x8D, 0x17, 0xEB, 0x3B, 0xF4, 0xA0, 0x6A,
0x34, 0x9D, 0x39, 0x2E, 0x00, 0xD3, 0x29, 0x74, 0x4A, 0x51, 0x79, 0x38,
0x03, 0x44, 0xE8, 0x2A, 0x18, 0xC4, 0x79, 0x33, 0x43, 0x8F, 0x89, 0x1E,
0x22, 0xAE, 0xEF, 0x81, 0x2D, 0x69, 0xC8, 0xF7, 0x5E, 0x32, 0x6C, 0xB7,
0x0E, 0xA0, 0x00, 0xC3, 0xF7, 0x76, 0xDF, 0xDB, 0xD6, 0x04, 0x63, 0x8C,
0x2E, 0xF7, 0x17, 0xFC, 0x26, 0xD0, 0x2E, 0x17};
uint8_t Q[] = {0xE2, 0x1E, 0x04, 0xF9, 0x11, 0xD1, 0xED, 0x79, 0x91, 0x00,
0x8E, 0xCA, 0xAB, 0x3B, 0xF7, 0x75, 0x98, 0x43, 0x09, 0xC3};
uint8_t G[] = {
0xC5, 0x2A, 0x4A, 0x0F, 0xF3, 0xB7, 0xE6, 0x1F, 0xDF, 0x18, 0x67, 0xCE,
0x84, 0x13, 0x83, 0x69, 0xA6, 0x15, 0x4F, 0x4A, 0xFA, 0x92, 0x96, 0x6E,
0x3C, 0x82, 0x7E, 0x25, 0xCF, 0xA6, 0xCF, 0x50, 0x8B, 0x90, 0xE5, 0xDE,
0x41, 0x9E, 0x13, 0x37, 0xE0, 0x7A, 0x2E, 0x9E, 0x2A, 0x3C, 0xD5, 0xDE,
0xA7, 0x04, 0xD1, 0x75, 0xF8, 0xEB, 0xF6, 0xAF, 0x39, 0x7D, 0x69, 0xE1,
0x10, 0xB9, 0x6A, 0xFB, 0x17, 0xC7, 0xA0, 0x32, 0x59, 0x32, 0x9E, 0x48,
0x29, 0xB0, 0xD0, 0x3B, 0xBC, 0x78, 0x96, 0xB1, 0x5B, 0x4A, 0xDE, 0x53,
0xE1, 0x30, 0x85, 0x8C, 0xC3, 0x4D, 0x96, 0x26, 0x9A, 0xA8, 0x90, 0x41,
0xF4, 0x09, 0x13, 0x6C, 0x72, 0x42, 0xA3, 0x88, 0x95, 0xC9, 0xD5, 0xBC,
0xCA, 0xD4, 0xF3, 0x89, 0xAF, 0x1D, 0x7A, 0x4B, 0xD1, 0x39, 0x8B, 0xD0,
0x72, 0xDF, 0xFA, 0x89, 0x62, 0x33, 0x39, 0x7A};
static_assert(MOZ_ARRAY_LENGTH(P) == 1024 / CHAR_BIT, "bad DSA P");
static_assert(MOZ_ARRAY_LENGTH(Q) == 160 / CHAR_BIT, "bad DSA Q");
static_assert(MOZ_ARRAY_LENGTH(G) == 1024 / CHAR_BIT, "bad DSA G");
PQGParams pqgParams = {
nullptr /*arena*/,
{siBuffer, P, static_cast<unsigned int>(mozilla::ArrayLength(P))},
{siBuffer, Q, static_cast<unsigned int>(mozilla::ArrayLength(Q))},
{siBuffer, G, static_cast<unsigned int>(mozilla::ArrayLength(G))}};
return GenerateKeyPair(slot, privateKey, publicKey, CKM_DSA_KEY_PAIR_GEN,
&pqgParams);
}
NS_IMETHODIMP
KeyGenRunnable::Run() {
if (!NS_IsMainThread()) {
// We always want to use the internal slot for BrowserID; in particular,
// we want to avoid smartcard slots.
PK11SlotInfo* slot = PK11_GetInternalSlot();
if (!slot) {
mRv = NS_ERROR_UNEXPECTED;
} else {
SECKEYPrivateKey* privk = nullptr;
SECKEYPublicKey* pubk = nullptr;
switch (mKeyType) {
case rsaKey:
mRv = GenerateRSAKeyPair(slot, &privk, &pubk);
break;
case dsaKey:
mRv = GenerateDSAKeyPair(slot, &privk, &pubk);
break;
default:
MOZ_CRASH("unknown key type");
}
PK11_FreeSlot(slot);
if (NS_SUCCEEDED(mRv)) {
MOZ_ASSERT(privk);
MOZ_ASSERT(pubk);
// mKeyPair will take over ownership of privk and pubk
mKeyPair = new KeyPair(privk, pubk, mThread);
}
}
NS_DispatchToMainThread(this);
} else {
// Back on Main Thread
(void)mCallback->GenerateKeyPairFinished(mRv, mKeyPair);
}
return NS_OK;
}
SignRunnable::SignRunnable(const nsACString& aText,
SECKEYPrivateKey* privateKey,
nsIIdentitySignCallback* aCallback)
: mozilla::Runnable("SignRunnable"),
mTextToSign(aText),
mPrivateKey(SECKEY_CopyPrivateKey(privateKey)),
mCallback(new nsMainThreadPtrHolder<nsIIdentitySignCallback>(
"SignRunnable::mCallback", aCallback)),
mRv(NS_ERROR_NOT_INITIALIZED) {}
NS_IMETHODIMP
SignRunnable::Run() {
if (!NS_IsMainThread()) {
// We need the output in PKCS#11 format, not DER encoding, so we must use
// PK11_HashBuf and PK11_Sign instead of SEC_SignData.
SECItem sig = {siBuffer, nullptr, 0};
int sigLength = PK11_SignatureLen(mPrivateKey);
if (sigLength <= 0) {
mRv = mozilla::psm::GetXPCOMFromNSSError(PR_GetError());
} else if (!SECITEM_AllocItem(nullptr, &sig, sigLength)) {
mRv = mozilla::psm::GetXPCOMFromNSSError(PR_GetError());
} else {
uint8_t hash[32]; // big enough for SHA-1 or SHA-256
SECOidTag hashAlg =
mPrivateKey->keyType == dsaKey ? SEC_OID_SHA1 : SEC_OID_SHA256;
SECItem hashItem = {siBuffer, hash, hashAlg == SEC_OID_SHA1 ? 20u : 32u};
mRv = MapSECStatus(
PK11_HashBuf(hashAlg, hash,
const_cast<uint8_t*>(
reinterpret_cast<const uint8_t*>(mTextToSign.get())),
mTextToSign.Length()));
if (NS_SUCCEEDED(mRv)) {
mRv = MapSECStatus(PK11_Sign(mPrivateKey, &sig, &hashItem));
}
if (NS_SUCCEEDED(mRv)) {
mRv =
Base64URLEncode(sig.len, sig.data,
Base64URLEncodePaddingPolicy::Include, mSignature);
}
SECITEM_FreeItem(&sig, false);
}
NS_DispatchToMainThread(this);
} else {
// Back on Main Thread
(void)mCallback->SignFinished(mRv, mSignature);
}
return NS_OK;
}
} // unnamed namespace
// XPCOM module registration
NS_IMPL_COMPONENT_FACTORY(nsIIdentityCryptoService) {
auto inst = MakeRefPtr<IdentityCryptoService>();
if (NS_SUCCEEDED(inst->Init())) {
return inst.forget().downcast<nsIIdentityCryptoService>();
}
return nullptr;
}

View File

@ -1,13 +0,0 @@
# -*- Mode: python; 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/.
Classes = [
{
'cid': '{bea13a3a-44e8-4d7f-a0a2-2c67f84e3a97}',
'contract_ids': ['@mozilla.org/identity/crypto-service;1'],
'type': 'nsIIdentityCryptoService',
},
]

View File

@ -1,21 +0,0 @@
# -*- Mode: python; 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/.
XPIDL_SOURCES += [
"nsIIdentityCryptoService.idl",
]
XPIDL_MODULE = "services-crypto-component"
SOURCES += [
"IdentityCryptoService.cpp",
]
XPCOM_MANIFESTS += [
"components.conf",
]
FINAL_LIBRARY = "xul"

View File

@ -1,106 +0,0 @@
/* 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/. */
#include "nsISupports.idl"
interface nsIURI;
interface nsIIdentityKeyGenCallback;
interface nsIIdentitySignCallback;
/* Naming and calling conventions:
*
* A"hex" prefix means "hex-encoded string representation of a byte sequence"
* e.g. "ae34bcdf123"
*
* A "base64url" prefix means "base-64-URL-encoded string repressentation of a
* byte sequence.
* e.g. "eyJhbGciOiJSUzI1NiJ9"
* http://en.wikipedia.org/wiki/Base64#Variants_summary_table
* we use the padded approach to base64-url-encoding
*
* Callbacks take an "in nsresult rv" argument that indicates whether the async
* operation succeeded. On success, rv will be a success code
* (NS_SUCCEEDED(rv) / Components.isSuccessCode(rv)) and the remaining
* arguments are as defined in the documentation for the callback. When the
* operation fails, rv will be a failure code (NS_FAILED(rv) /
* !Components.isSuccessCode(rv)) and the values of the remaining arguments will
* be unspecified.
*
* Key Types:
*
* "RS256": RSA + SHA-256.
*
* "DS160": DSA with SHA-1. A 1024-bit prime and a 160-bit subprime with SHA-1.
*
* we use these abbreviated algorithm names as per the JWA spec
* http://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-02
*/
// "@mozilla.org/identity/crypto-service;1"
[scriptable, builtinclass, uuid(f087e6bc-dd33-4f6c-a106-dd786e052ee9)]
interface nsIIdentityCryptoService : nsISupports
{
void generateKeyPair(in AUTF8String algorithm,
in nsIIdentityKeyGenCallback callback);
ACString base64UrlEncode(in AUTF8String toEncode);
};
/**
* This interface provides a keypair and signing interface for Identity functionality
*/
[scriptable, uuid(73962dc7-8ee7-4346-a12b-b039e1d9b54d)]
interface nsIIdentityKeyPair : nsISupports
{
readonly attribute AUTF8String keyType;
// RSA properties, only accessible when keyType == "RS256"
readonly attribute AUTF8String hexRSAPublicKeyExponent;
readonly attribute AUTF8String hexRSAPublicKeyModulus;
// DSA properties, only accessible when keyType == "DS128"
readonly attribute AUTF8String hexDSAPrime; // p
readonly attribute AUTF8String hexDSASubPrime; // q
readonly attribute AUTF8String hexDSAGenerator; // g
readonly attribute AUTF8String hexDSAPublicValue; // y
void sign(in AUTF8String aText,
in nsIIdentitySignCallback callback);
// XXX implement verification bug 769856
// AUTF8String verify(in AUTF8String aSignature, in AUTF8String encodedPublicKey);
};
/**
* This interface provides a JavaScript callback object used to collect the
* nsIIdentityServeKeyPair when the keygen operation is complete
*
* though there is discussion as to whether we need the nsresult,
* we keep it so we can track deeper crypto errors.
*/
[scriptable, function, uuid(90f24ca2-2b05-4ca9-8aec-89d38e2f905a)]
interface nsIIdentityKeyGenCallback : nsISupports
{
void generateKeyPairFinished(in nsresult rv,
in nsIIdentityKeyPair keyPair);
};
/**
* This interface provides a JavaScript callback object used to collect the
* AUTF8String signature
*/
[scriptable, function, uuid(2d3e5036-374b-4b47-a430-1196b67b890f)]
interface nsIIdentitySignCallback : nsISupports
{
/** On success, base64urlSignature is the base-64-URL-encoded signature
*
* For RS256 signatures, XXX bug 769858
*
* For DSA128 signatures, the signature is the r value concatenated with the
* s value, each component padded with leading zeroes as necessary.
*/
void signFinished(in nsresult rv, in ACString base64urlSignature);
};

View File

@ -7,41 +7,11 @@
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
XPCOMUtils.defineLazyServiceGetter(
this,
"IdentityCryptoService",
"@mozilla.org/identity/crypto-service;1",
"nsIIdentityCryptoService"
);
XPCOMUtils.defineLazyGlobalGetters(this, ["crypto"]);
const EXPORTED_SYMBOLS = ["jwcrypto"];
const PREF_LOG_LEVEL = "services.crypto.jwcrypto.log.level";
XPCOMUtils.defineLazyGetter(this, "log", function() {
const log = Log.repository.getLogger("Services.Crypto.jwcrypto");
// Default log level is "Error", but consumers can change this with the pref
// "services.crypto.jwcrypto.log.level".
log.level = Log.Level.Error;
const appender = new Log.DumpAppender();
log.addAppender(appender);
try {
const level =
Services.prefs.getPrefType(PREF_LOG_LEVEL) ==
Ci.nsIPrefBranch.PREF_STRING &&
Services.prefs.getCharPref(PREF_LOG_LEVEL);
log.level = Log.Level[level] || Log.Level.Error;
} catch (e) {
log.error(e);
}
return log;
});
const ASSERTION_DEFAULT_DURATION_MS = 1000 * 60 * 2; // 2 minutes default assertion lifetime
const ECDH_PARAMS = {
name: "ECDH",
namedCurve: "P-256",
@ -200,125 +170,6 @@ class JWCrypto {
);
return new Uint8Array(decrypted);
}
generateKeyPair(aAlgorithmName, aCallback) {
log.debug("generating");
log.debug("Generate key pair; alg = " + aAlgorithmName);
IdentityCryptoService.generateKeyPair(aAlgorithmName, (rv, aKeyPair) => {
if (!Components.isSuccessCode(rv)) {
return aCallback("key generation failed");
}
let publicKey;
switch (aKeyPair.keyType) {
case "RS256":
publicKey = {
algorithm: "RS",
exponent: aKeyPair.hexRSAPublicKeyExponent,
modulus: aKeyPair.hexRSAPublicKeyModulus,
};
break;
case "DS160":
publicKey = {
algorithm: "DS",
y: aKeyPair.hexDSAPublicValue,
p: aKeyPair.hexDSAPrime,
q: aKeyPair.hexDSASubPrime,
g: aKeyPair.hexDSAGenerator,
};
break;
default:
return aCallback("unknown key type");
}
const keyWrapper = {
serializedPublicKey: JSON.stringify(publicKey),
_kp: aKeyPair,
};
return aCallback(null, keyWrapper);
});
}
/**
* Generate an assertion and return it through the provided callback.
*
* @param aCert
* Identity certificate
*
* @param aKeyPair
* KeyPair object
*
* @param aAudience
* Audience of the assertion
*
* @param aOptions (optional)
* Can include:
* {
* localtimeOffsetMsec: <clock offset in milliseconds>,
* now: <current date in milliseconds>
* duration: <validity duration for this assertion in milliseconds>
* }
*
* localtimeOffsetMsec is the number of milliseconds that need to be
* added to the local clock time to make it concur with the server.
* For example, if the local clock is two minutes fast, the offset in
* milliseconds would be -120000.
*
* @param aCallback
* Function to invoke with resulting assertion. Assertion
* will be string or null on failure.
*/
generateAssertion(aCert, aKeyPair, aAudience, aOptions, aCallback) {
if (typeof aOptions == "function") {
aCallback = aOptions;
aOptions = {};
}
// for now, we hack the algorithm name
// XXX bug 769851
const header = { alg: "DS128" };
const headerBytes = IdentityCryptoService.base64UrlEncode(
JSON.stringify(header)
);
function getExpiration(
duration = ASSERTION_DEFAULT_DURATION_MS,
localtimeOffsetMsec = 0,
now = Date.now()
) {
return now + localtimeOffsetMsec + duration;
}
const payload = {
exp: getExpiration(
aOptions.duration,
aOptions.localtimeOffsetMsec,
aOptions.now
),
aud: aAudience,
};
const payloadBytes = IdentityCryptoService.base64UrlEncode(
JSON.stringify(payload)
);
log.debug("payload", { payload, payloadBytes });
const message = headerBytes + "." + payloadBytes;
aKeyPair._kp.sign(message, (rv, signature) => {
if (!Components.isSuccessCode(rv)) {
log.error("signer.sign failed");
aCallback("Sign failed");
return;
}
log.debug("signer.sign: success");
const signedAssertion = message + "." + signature;
aCallback(null, aCert + "~" + signedAssertion);
});
}
}
/**

View File

@ -7,8 +7,6 @@
with Files("**"):
BUG_COMPONENT = ("Firefox", "Sync")
DIRS += ["component"]
XPCSHELL_TESTS_MANIFESTS += ["tests/unit/xpcshell.ini"]
EXTRA_JS_MODULES["services-crypto"] += [

View File

@ -1,133 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const idService = Cc["@mozilla.org/identity/crypto-service;1"].getService(
Ci.nsIIdentityCryptoService
);
const ALG_DSA = "DS160";
const ALG_RSA = "RS256";
const BASE64_URL_ENCODINGS = [
// The vectors from RFC 4648 are very silly, but we may as well include them.
["", ""],
["f", "Zg=="],
["fo", "Zm8="],
["foo", "Zm9v"],
["foob", "Zm9vYg=="],
["fooba", "Zm9vYmE="],
["foobar", "Zm9vYmFy"],
// It's quite likely you could get a string like this in an assertion audience
["i-like-pie.com", "aS1saWtlLXBpZS5jb20="],
// A few extra to be really sure
["andré@example.com", "YW5kcsOpQGV4YW1wbGUuY29t"],
[
"πόλλ' οἶδ' ἀλώπηξ, ἀλλ' ἐχῖνος ἓν μέγα",
"z4DPjM67zrsnIM6_4by2zrQnIOG8gM67z47PgM63zr4sIOG8gM67zrsnIOG8kM-H4b-Wzr3Ov8-CIOG8k869IM68zq3Os86x",
],
];
let log = Log.repository.getLogger("crypto.service.test");
(function() {
let appender = new Log.DumpAppender();
log.level = Log.Level.Debug;
log.addAppender(appender);
})();
// When the output of an operation is a
function do_check_eq_or_slightly_less(x, y) {
Assert.ok(x >= y - 3 * 8);
}
function test_base64_roundtrip() {
let message = "Attack at dawn!";
let encoded = idService.base64UrlEncode(message);
let decoded = base64UrlDecode(encoded);
Assert.notEqual(message, encoded);
Assert.equal(decoded, message);
run_next_test();
}
function test_dsa() {
idService.generateKeyPair(ALG_DSA, function(rv, keyPair) {
log.debug("DSA generateKeyPair finished " + rv);
Assert.ok(Components.isSuccessCode(rv));
Assert.equal(typeof keyPair.sign, "function");
Assert.equal(keyPair.keyType, ALG_DSA);
do_check_eq_or_slightly_less(
keyPair.hexDSAGenerator.length,
(1024 / 8) * 2
);
do_check_eq_or_slightly_less(keyPair.hexDSAPrime.length, (1024 / 8) * 2);
do_check_eq_or_slightly_less(keyPair.hexDSASubPrime.length, (160 / 8) * 2);
do_check_eq_or_slightly_less(
keyPair.hexDSAPublicValue.length,
(1024 / 8) * 2
);
// XXX: test that RSA parameters throw the correct error
log.debug("about to sign with DSA key");
keyPair.sign("foo", function(rv2, signature) {
log.debug("DSA sign finished " + rv2 + " " + signature);
Assert.ok(Components.isSuccessCode(rv2));
Assert.ok(signature.length > 1);
// TODO: verify the signature with the public key
run_next_test();
});
});
}
function test_rsa() {
idService.generateKeyPair(ALG_RSA, function(rv, keyPair) {
log.debug("RSA generateKeyPair finished " + rv);
Assert.ok(Components.isSuccessCode(rv));
Assert.equal(typeof keyPair.sign, "function");
Assert.equal(keyPair.keyType, ALG_RSA);
do_check_eq_or_slightly_less(
keyPair.hexRSAPublicKeyModulus.length,
2048 / 8
);
Assert.ok(keyPair.hexRSAPublicKeyExponent.length > 1);
log.debug("about to sign with RSA key");
keyPair.sign("foo", function(rv2, signature) {
log.debug("RSA sign finished " + rv2 + " " + signature);
Assert.ok(Components.isSuccessCode(rv2));
Assert.ok(signature.length > 1);
run_next_test();
});
});
}
function test_base64UrlEncode() {
for (let [source, target] of BASE64_URL_ENCODINGS) {
Assert.equal(target, idService.base64UrlEncode(source));
}
run_next_test();
}
function test_base64UrlDecode() {
let utf8Converter = Cc[
"@mozilla.org/intl/scriptableunicodeconverter"
].createInstance(Ci.nsIScriptableUnicodeConverter);
utf8Converter.charset = "UTF-8";
// We know the encoding of our inputs - on conversion back out again, make
// sure they're the same.
for (let [source, target] of BASE64_URL_ENCODINGS) {
let result = utf8Converter.ConvertToUnicode(base64UrlDecode(target));
result += utf8Converter.Finish();
Assert.equal(source, result);
}
run_next_test();
}
add_test(test_base64_roundtrip);
add_test(test_dsa);
add_test(test_rsa);
add_test(test_base64UrlEncode);
add_test(test_base64UrlDecode);

View File

@ -11,18 +11,8 @@ ChromeUtils.defineModuleGetter(
"resource://services-crypto/jwcrypto.jsm"
);
XPCOMUtils.defineLazyServiceGetter(
this,
"CryptoService",
"@mozilla.org/identity/crypto-service;1",
"nsIIdentityCryptoService"
);
Cu.importGlobalProperties(["crypto"]);
const RP_ORIGIN = "http://123done.org";
const INTERNAL_ORIGIN = "browserid://";
const SECOND_MS = 1000;
const MINUTE_MS = SECOND_MS * 60;
const HOUR_MS = MINUTE_MS * 60;
@ -30,18 +20,6 @@ const HOUR_MS = MINUTE_MS * 60;
// Enable logging from jwcrypto.jsm.
Services.prefs.setCharPref("services.crypto.jwcrypto.log.level", "Debug");
function promisify(fn) {
return (...args) => {
return new Promise((res, rej) => {
fn(...args, (err, result) => {
err ? rej(err) : res(result);
});
});
};
}
const generateKeyPair = promisify(jwcrypto.generateKeyPair);
const generateAssertion = promisify(jwcrypto.generateAssertion);
add_task(async function test_jwe_roundtrip_ecdh_es_encryption() {
const plaintext = crypto.getRandomValues(new Uint8Array(123));
const remoteKey = await crypto.subtle.generateKey(
@ -81,160 +59,3 @@ add_task(async function test_jwe_header_includes_key_id() {
);
Assert.equal(header.kid, "key identifier");
});
add_task(async function test_sanity() {
// Shouldn't reject.
await generateKeyPair("DS160");
});
add_task(async function test_generate() {
let kp = await generateKeyPair("DS160");
Assert.notEqual(kp, null);
});
add_task(async function test_get_assertion() {
let kp = await generateKeyPair("DS160");
let backedAssertion = await generateAssertion("fake-cert", kp, RP_ORIGIN);
Assert.equal(backedAssertion.split("~").length, 2);
Assert.equal(backedAssertion.split(".").length, 3);
});
add_task(async function test_rsa() {
let kpo = await generateKeyPair("RS256");
Assert.notEqual(kpo, undefined);
info(kpo.serializedPublicKey);
let pk = JSON.parse(kpo.serializedPublicKey);
Assert.equal(pk.algorithm, "RS");
/* TODO
do_check_neq(kpo.sign, null);
do_check_eq(typeof kpo.sign, "function");
do_check_neq(kpo.userID, null);
do_check_neq(kpo.url, null);
do_check_eq(kpo.url, INTERNAL_ORIGIN);
do_check_neq(kpo.exponent, null);
do_check_neq(kpo.modulus, null);
// TODO: should sign be async?
let sig = kpo.sign("This is a message to sign");
do_check_neq(sig, null);
do_check_eq(typeof sig, "string");
do_check_true(sig.length > 1);
*/
});
add_task(async function test_dsa() {
let kpo = await generateKeyPair("DS160");
info(kpo.serializedPublicKey);
let pk = JSON.parse(kpo.serializedPublicKey);
Assert.equal(pk.algorithm, "DS");
/* TODO
do_check_neq(kpo.sign, null);
do_check_eq(typeof kpo.sign, "function");
do_check_neq(kpo.userID, null);
do_check_neq(kpo.url, null);
do_check_eq(kpo.url, INTERNAL_ORIGIN);
do_check_neq(kpo.generator, null);
do_check_neq(kpo.prime, null);
do_check_neq(kpo.subPrime, null);
do_check_neq(kpo.publicValue, null);
let sig = kpo.sign("This is a message to sign");
do_check_neq(sig, null);
do_check_eq(typeof sig, "string");
do_check_true(sig.length > 1);
*/
});
add_task(async function test_get_assertion_with_offset() {
// Use an arbitrary date in the past to ensure we don't accidentally pass
// this test with current dates, missing offsets, etc.
let serverMsec = Date.parse("Tue Oct 31 2000 00:00:00 GMT-0800");
// local clock skew
// clock is 12 hours fast; -12 hours offset must be applied
let localtimeOffsetMsec = -1 * 12 * HOUR_MS;
let localMsec = serverMsec - localtimeOffsetMsec;
let kp = await generateKeyPair("DS160");
let backedAssertion = await generateAssertion("fake-cert", kp, RP_ORIGIN, {
duration: MINUTE_MS,
localtimeOffsetMsec,
now: localMsec,
});
// properly formed
let cert;
let assertion;
[cert, assertion] = backedAssertion.split("~");
Assert.equal(cert, "fake-cert");
Assert.equal(assertion.split(".").length, 3);
let components = extractComponents(assertion);
// Expiry is within two minutes, corrected for skew
let exp = parseInt(components.payload.exp, 10);
Assert.ok(exp - serverMsec === MINUTE_MS);
});
add_task(async function test_assertion_lifetime() {
let kp = await generateKeyPair("DS160");
let backedAssertion = await generateAssertion("fake-cert", kp, RP_ORIGIN, {
duration: MINUTE_MS,
});
// properly formed
let cert;
let assertion;
[cert, assertion] = backedAssertion.split("~");
Assert.equal(cert, "fake-cert");
Assert.equal(assertion.split(".").length, 3);
let components = extractComponents(assertion);
// Expiry is within one minute, as we specified above
let exp = parseInt(components.payload.exp, 10);
Assert.ok(Math.abs(Date.now() - exp) > 50 * SECOND_MS);
Assert.ok(Math.abs(Date.now() - exp) <= MINUTE_MS);
});
add_task(async function test_audience_encoding_bug972582() {
let audience = "i-like-pie.com";
let kp = await generateKeyPair("DS160");
let backedAssertion = await generateAssertion("fake-cert", kp, audience);
let [, /* cert */ assertion] = backedAssertion.split("~");
let components = extractComponents(assertion);
Assert.equal(components.payload.aud, audience);
});
function extractComponents(signedObject) {
if (typeof signedObject != "string") {
throw new Error("malformed signature " + typeof signedObject);
}
let parts = signedObject.split(".");
if (parts.length != 3) {
throw new Error(
"signed object must have three parts, this one has " + parts.length
);
}
let headerSegment = parts[0];
let payloadSegment = parts[1];
let cryptoSegment = parts[2];
let header = JSON.parse(base64UrlDecode(headerSegment));
let payload = JSON.parse(base64UrlDecode(payloadSegment));
// Ensure well-formed header
Assert.equal(Object.keys(header).length, 1);
Assert.ok(!!header.alg);
// Ensure well-formed payload
for (let field of ["exp", "aud"]) {
Assert.ok(!!payload[field]);
}
return { header, payload, headerSegment, payloadSegment, cryptoSegment };
}

View File

@ -8,7 +8,6 @@ support-files =
[test_crypto_crypt.js]
[test_crypto_random.js]
[test_crypto_service.js]
skip-if = appname == 'thunderbird'
[test_jwcrypto.js]
skip-if = appname == 'thunderbird'

View File

@ -20,24 +20,17 @@ const { FxAccountsStorageManager } = ChromeUtils.import(
"resource://gre/modules/FxAccountsStorage.jsm"
);
const {
ASSERTION_LIFETIME,
ASSERTION_USE_PERIOD,
CERT_LIFETIME,
ERRNO_INVALID_AUTH_TOKEN,
ERRNO_INVALID_FXA_ASSERTION,
ERROR_AUTH_ERROR,
ERROR_INVALID_PARAMETER,
ERROR_NO_ACCOUNT,
ERROR_OFFLINE,
ERROR_TO_GENERAL_ERROR_CLASS,
ERROR_UNKNOWN,
ERROR_UNVERIFIED_ACCOUNT,
FXA_PWDMGR_MEMORY_FIELDS,
FXA_PWDMGR_PLAINTEXT_FIELDS,
FXA_PWDMGR_REAUTH_WHITELIST,
FXA_PWDMGR_SECURE_FIELDS,
FX_OAUTH_CLIENT_ID,
KEY_LIFETIME,
ON_ACCOUNT_STATE_CHANGE_NOTIFICATION,
ONLOGIN_NOTIFICATION,
ONLOGOUT_NOTIFICATION,
@ -59,24 +52,12 @@ ChromeUtils.defineModuleGetter(
"resource://gre/modules/FxAccountsClient.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"FxAccountsOAuthGrantClient",
"resource://gre/modules/FxAccountsOAuthGrantClient.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"FxAccountsConfig",
"resource://gre/modules/FxAccountsConfig.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"jwcrypto",
"resource://services-crypto/jwcrypto.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"FxAccountsCommands",
@ -118,12 +99,6 @@ XPCOMUtils.defineLazyPreferenceGetter(
true
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"USE_SESSION_TOKENS_FOR_OAUTH",
"identity.fxaccounts.useSessionTokensForOAuth"
);
// An AccountState object holds all state related to one specific account.
// It is considered "private" to the FxAccounts modules.
// Only one AccountState is ever "current" in the FxAccountsInternal object -
@ -834,15 +809,6 @@ FxAccountsInternal.prototype = {
return this._fxAccountsClient;
},
get fxAccountsOAuthGrantClient() {
if (!this._fxAccountsOAuthGrantClient) {
this._fxAccountsOAuthGrantClient = new FxAccountsOAuthGrantClient({
client_id: FX_OAUTH_CLIENT_ID,
});
}
return this._fxAccountsOAuthGrantClient;
},
// The profile object used to fetch the actual user profile.
_profile: null,
get profile() {
@ -906,7 +872,7 @@ FxAccountsInternal.prototype = {
/**
* Return the current time in milliseconds as an integer. Allows tests to
* manipulate the date to simulate certificate expiration.
* manipulate the date to simulate token expiration.
*/
now() {
return this.fxAccountsClient.now();
@ -1027,31 +993,6 @@ FxAccountsInternal.prototype = {
});
},
/**
* returns a promise that fires with the assertion. Throws if there is no
* verified signed-in user or no local sessionToken.
*/
getAssertion: function getAssertion(audience) {
return this._getAssertion(audience);
},
// getAssertion() is "public" so screws with our mock story. This
// implementation method *can* be (and is) mocked by tests.
_getAssertion(audience) {
log.debug("enter getAssertion()");
return this.withSessionToken(async (_, currentState) => {
let { keyPair, certificate } = await this.getKeypairAndCertificate(
currentState
);
return this.getAssertionFromCert(
await currentState.getUserAccountData(),
keyPair,
certificate,
audience
);
});
},
/*
* Reset state such that any previous flow is canceled.
*/
@ -1172,152 +1113,6 @@ FxAccountsInternal.prototype = {
}
},
async getAssertionFromCert(data, keyPair, cert, audience) {
log.debug("getAssertionFromCert");
let options = {
duration: ASSERTION_LIFETIME,
localtimeOffsetMsec: this.localtimeOffsetMsec,
now: this.now(),
};
let currentState = this.currentAccountState;
// "audience" should look like "http://123done.org".
// The generated assertion will expire in two minutes.
let assertion = await new Promise((resolve, reject) => {
jwcrypto.generateAssertion(
cert,
keyPair,
audience,
options,
(err, signed) => {
if (err) {
log.error("getAssertionFromCert: " + err);
reject(err);
} else {
log.debug("getAssertionFromCert returning signed: " + !!signed);
if (logPII) {
log.debug("getAssertionFromCert returning signed: " + signed);
}
resolve(signed);
}
}
);
});
return currentState.resolve(assertion);
},
getCertificateSigned(sessionToken, serializedPublicKey, lifetime) {
log.debug(
"getCertificateSigned: " + !!sessionToken + " " + !!serializedPublicKey
);
if (logPII) {
log.debug(
"getCertificateSigned: " + sessionToken + " " + serializedPublicKey
);
}
return this.fxAccountsClient.signCertificate(
sessionToken,
JSON.parse(serializedPublicKey),
lifetime
);
},
/**
* returns a promise that fires with {keyPair, certificate}.
*/
async getKeypairAndCertificate(currentState) {
// If the debugging pref to ignore cached authentication credentials is set for Sync,
// then don't use any cached key pair/certificate, i.e., generate a new
// one and get it signed.
// The purpose of this pref is to expedite any auth errors as the result of a
// expired or revoked FxA session token, e.g., from resetting or changing the FxA
// password.
let ignoreCachedAuthCredentials = Services.prefs.getBoolPref(
"services.sync.debug.ignoreCachedAuthCredentials",
false
);
let mustBeValidUntil = this.now() + ASSERTION_USE_PERIOD;
let accountData = await currentState.getUserAccountData([
"cert",
"keyPair",
"sessionToken",
]);
let keyPairValid =
!ignoreCachedAuthCredentials &&
accountData.keyPair &&
accountData.keyPair.validUntil > mustBeValidUntil;
let certValid =
!ignoreCachedAuthCredentials &&
accountData.cert &&
accountData.cert.validUntil > mustBeValidUntil;
// TODO: get the lifetime from the cert's .exp field
if (keyPairValid && certValid) {
log.debug(
"getKeypairAndCertificate: already have keyPair and certificate"
);
return {
keyPair: accountData.keyPair.rawKeyPair,
certificate: accountData.cert.rawCert,
};
}
// We are definately going to generate a new cert, either because it has
// already expired, or the keyPair has - and a new keyPair means we must
// generate a new cert.
// A keyPair has a longer lifetime than a cert, so it's possible we will
// have a valid keypair but an expired cert, which means we can skip
// keypair generation.
// Either way, the cert will require hitting the network, so bail now if
// we know that's going to fail.
if (Services.io.offline) {
throw new Error(ERROR_OFFLINE);
}
let keyPair;
if (keyPairValid) {
keyPair = accountData.keyPair;
} else {
let keyWillBeValidUntil = this.now() + KEY_LIFETIME;
keyPair = await new Promise((resolve, reject) => {
jwcrypto.generateKeyPair("DS160", (err, kp) => {
if (err) {
reject(err);
return;
}
log.debug("got keyPair");
resolve({
rawKeyPair: kp,
validUntil: keyWillBeValidUntil,
});
});
});
}
// and generate the cert.
let certWillBeValidUntil = this.now() + CERT_LIFETIME;
let certificate = await this.getCertificateSigned(
accountData.sessionToken,
keyPair.rawKeyPair.serializedPublicKey,
CERT_LIFETIME
);
log.debug("getCertificate got a new one: " + !!certificate);
if (certificate) {
// Cache both keypair and cert.
let toUpdate = {
keyPair,
cert: {
rawCert: certificate,
validUntil: certWillBeValidUntil,
},
};
await currentState.updateUserAccountData(toUpdate);
}
return {
keyPair: keyPair.rawKeyPair,
certificate,
};
},
getUserAccountData(fieldNames = null) {
return this.currentAccountState.getUserAccountData(fieldNames);
},
@ -1510,67 +1305,26 @@ FxAccountsInternal.prototype = {
delete currentState.whenVerifiedDeferred;
},
/**
* Does the actual fetch of an oauth token for getOAuthToken()
* @param scopeString
* @param ttl
* @returns {Promise<string>}
* @private
*/
async _doTokenFetch(scopeString, ttl) {
// Ideally, we would auth this call directly with our `sessionToken`
// using the `_doTokenFetchWithSessionToken` method rather than going
// via a BrowserID assertion. Before we can do so we need to resolve some
// data-volume processing issues in the server-side FxA metrics pipeline.
let token;
let oAuthURL = this.fxAccountsOAuthGrantClient.serverURL.href;
let assertion = await this.getAssertion(oAuthURL);
try {
let result = await this.fxAccountsOAuthGrantClient.getTokenFromAssertion(
assertion,
scopeString,
ttl
);
token = result.access_token;
} catch (err) {
// If we get a 401 fetching the token it may be that our certificate
// needs to be regenerated.
if (err.code !== 401 || err.errno !== ERRNO_INVALID_FXA_ASSERTION) {
throw err;
}
log.warn(
"OAuth server returned 401, refreshing certificate and retrying token fetch"
);
await this.invalidateCertificate();
assertion = await this.getAssertion(oAuthURL);
let result = await this.fxAccountsOAuthGrantClient.getTokenFromAssertion(
assertion,
scopeString,
ttl
);
token = result.access_token;
}
return token;
},
/**
* Does the actual fetch of an oauth token for getOAuthToken()
* using the account session token.
*
* It's split out into a separate method so that we can easily
* stash in-flight calls in a cache.
*
* @param {String} scopeString
* @param {Number} ttl
* @returns {Promise<string>}
* @private
*/
async _doTokenFetchWithSessionToken(scopeString, ttl) {
return this.withSessionToken(async sessionToken => {
const result = await this.fxAccountsClient.accessTokenWithSessionToken(
sessionToken,
FX_OAUTH_CLIENT_ID,
scopeString,
ttl
);
return result.access_token;
});
async _doTokenFetchWithSessionToken(sessionToken, scopeString, ttl) {
const result = await this.fxAccountsClient.accessTokenWithSessionToken(
sessionToken,
FX_OAUTH_CLIENT_ID,
scopeString,
ttl
);
return result.access_token;
},
getOAuthToken(options = {}) {
@ -1589,7 +1343,7 @@ FxAccountsInternal.prototype = {
);
}
return this.withVerifiedAccountState(async currentState => {
return this.withSessionToken(async (sessionToken, currentState) => {
// Early exit for a cached token.
let cached = currentState.getCachedToken(scope);
if (cached) {
@ -1608,13 +1362,14 @@ FxAccountsInternal.prototype = {
log.debug("getOAuthToken has an in-flight request for this scope");
return maybeInFlight;
}
let fetchFunction = this._doTokenFetch.bind(this);
if (USE_SESSION_TOKENS_FOR_OAUTH) {
fetchFunction = this._doTokenFetchWithSessionToken.bind(this);
}
// We need to start a new fetch and stick the promise in our in-flight map
// and remove it when it resolves.
let promise = fetchFunction(scopeString, options.ttl)
let promise = this._doTokenFetchWithSessionToken(
sessionToken,
scopeString,
options.ttl
)
.then(token => {
// As a sanity check, ensure something else hasn't raced getting a token
// of the same scope. If something has we just make noise rather than
@ -1670,16 +1425,6 @@ FxAccountsInternal.prototype = {
});
},
/**
* Invalidate the FxA certificate, so that it will be refreshed from the server
* the next time it is needed.
*/
invalidateCertificate() {
return this.withCurrentAccountState(async currentState => {
await currentState.updateUserAccountData({ cert: null });
});
},
async _getVerifiedAccountOrReject() {
let data = await this.currentAccountState.getUserAccountData();
if (!data) {
@ -1807,7 +1552,6 @@ FxAccountsInternal.prototype = {
};
FXA_PWDMGR_PLAINTEXT_FIELDS.forEach(clearField);
FXA_PWDMGR_SECURE_FIELDS.forEach(clearField);
FXA_PWDMGR_MEMORY_FIELDS.forEach(clearField);
return state.updateUserAccountData(updateData);
},

View File

@ -24,7 +24,6 @@ const {
ERRNO_INVALID_AUTH_NONCE,
ERRNO_INVALID_AUTH_TIMESTAMP,
ERRNO_INVALID_AUTH_TOKEN,
FX_OAUTH_CLIENT_ID,
log,
} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
const { Credentials } = ChromeUtils.import(
@ -440,54 +439,6 @@ FxAccountsClient.prototype = {
};
},
/**
* Sends a public key to the FxA API server and returns a signed certificate
*
* @param sessionTokenHex
* The current session token encoded 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.
* @throws a new Error
* wrapping any of these HTTP code/errno pairs:
* https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-12
*/
async signCertificate(sessionTokenHex, serializedPublicKey, lifetime) {
let creds = await deriveHawkCredentials(sessionTokenHex, "sessionToken");
// The FxA server has various special-case behaviours for sync clients.
// As a terrible hack, check whether sync is enabled and adjust the
// `service` parameter appropriately. This can go away once we stop using
// BrowserID for OAuth requests, after which sync will be the only user
// of these signed certs.
let service = FX_OAUTH_CLIENT_ID;
if (Services.prefs.prefHasUserValue("services.sync.username")) {
service = "sync";
}
let body = { publicKey: serializedPublicKey, duration: lifetime };
return Promise.resolve()
.then(_ =>
this._request(
`/certificate/sign?service=${service}`,
"POST",
creds,
body
)
)
.then(
resp => resp.cert,
err => {
log.error("HAWK.signCertificate error", err);
throw err;
}
);
},
/**
* Obtain an OAuth access token by authenticating using a session token.
*

View File

@ -62,18 +62,7 @@ exports.FXACCOUNTS_PERMISSION = "firefox-accounts";
exports.DATA_FORMAT_VERSION = 1;
exports.DEFAULT_STORAGE_FILENAME = "signedInUser.json";
// Token life times.
// Having this parameter be short has limited security value and can cause
// spurious authentication values if the client's clock is skewed and
// we fail to adjust. See Bug 983256.
exports.ASSERTION_LIFETIME = 1000 * 3600 * 24 * 365 * 25; // 25 years
// This is a time period we want to guarantee that the assertion will be
// valid after we generate it (e.g., the signed cert won't expire in this
// period).
exports.ASSERTION_USE_PERIOD = 1000 * 60 * 5; // 5 minutes
exports.CERT_LIFETIME = 1000 * 3600 * 6; // 6 hours
exports.OAUTH_TOKEN_FOR_SYNC_LIFETIME_SECONDS = 3600 * 6; // 6 hours
exports.KEY_LIFETIME = 1000 * 3600 * 12; // 12 hours
// After we start polling for account verification, we stop polling when this
// many milliseconds have elapsed.
@ -309,13 +298,9 @@ exports.FXA_PWDMGR_SECURE_FIELDS = new Set([
...exports.LEGACY_DERIVED_KEYS_NAMES,
"keyFetchToken",
"unwrapBKey",
"assertion",
"scopedKeys",
]);
// Fields we keep in memory and don't persist anywhere.
exports.FXA_PWDMGR_MEMORY_FIELDS = new Set(["cert", "keyPair"]);
// A whitelist of fields that remain in storage when the user needs to
// reauthenticate. All other fields will be removed.
exports.FXA_PWDMGR_REAUTH_WHITELIST = new Set([

View File

@ -1,239 +0,0 @@
/* 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/. */
/**
* Firefox Accounts OAuth Grant Client allows clients to obtain
* an OAuth token from a BrowserID assertion. Only certain client
* IDs support this privilege.
*/
var EXPORTED_SYMBOLS = [
"FxAccountsOAuthGrantClient",
"FxAccountsOAuthGrantClientError",
];
const {
ERRNO_NETWORK,
ERRNO_PARSE,
ERRNO_UNKNOWN_ERROR,
ERROR_CODE_METHOD_NOT_ALLOWED,
ERROR_MSG_METHOD_NOT_ALLOWED,
ERROR_NETWORK,
ERROR_PARSE,
ERROR_UNKNOWN,
OAUTH_SERVER_ERRNO_OFFSET,
log,
} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
const { RESTRequest } = ChromeUtils.import(
"resource://services-common/rest.js"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
const AUTH_ENDPOINT = "/authorization";
const HOST_PREF = "identity.fxaccounts.remote.oauth.uri";
// This is the same pref that's used by FxAccounts.jsm.
const ALLOW_HTTP_PREF = "identity.fxaccounts.allowHttp";
/**
* Create a new FxAccountsOAuthClient for the browser.
*
* @param {Object} options Options
* @param {String} options.client_id
* OAuth client id under which this client is registered
* @param {String} options.parameters.serverURL
* The FxA OAuth server URL
* @constructor
*/
var FxAccountsOAuthGrantClient = function(options = {}) {
this.parameters = { ...options };
if (!this.parameters.serverURL) {
this.parameters.serverURL = Services.prefs.getCharPref(HOST_PREF);
}
this._validateOptions(this.parameters);
try {
this.serverURL = new URL(this.parameters.serverURL);
} catch (e) {
throw new Error("Invalid 'serverURL'");
}
let forceHTTPS = !Services.prefs.getBoolPref(ALLOW_HTTP_PREF, false);
if (forceHTTPS && this.serverURL.protocol != "https:") {
throw new Error("'serverURL' must be HTTPS");
}
log.debug("FxAccountsOAuthGrantClient Initialized");
};
FxAccountsOAuthGrantClient.prototype = {
/**
* Retrieves an OAuth access token for the signed in user
*
* @param {Object} assertion BrowserID assertion
* @param {String} scope OAuth scope
* @param {Number} ttl token time to live
* @return Promise
* Resolves: {Object} Object with access_token property
*/
getTokenFromAssertion(assertion, scope, ttl) {
if (!assertion) {
throw new Error("Missing 'assertion' parameter");
}
if (!scope) {
throw new Error("Missing 'scope' parameter");
}
let params = {
scope,
client_id: this.parameters.client_id,
assertion,
response_type: "token",
ttl,
};
return this._createRequest(AUTH_ENDPOINT, "POST", params);
},
/**
* Validates the required FxA OAuth parameters
*
* @param options {Object}
* OAuth client options
* @private
*/
_validateOptions(options) {
if (!options) {
throw new Error("Missing configuration options");
}
["serverURL", "client_id"].forEach(option => {
if (!options[option]) {
throw new Error("Missing '" + option + "' parameter");
}
});
},
/**
* Interface for making remote requests.
*/
_Request: RESTRequest,
/**
* Remote request helper
*
* @param {String} path
* OAuth server path, i.e "/token".
* @param {String} [method]
* Type of request, i.e "GET".
* @return Promise
* Resolves: {Object} Successful response from the Oauth server.
* Rejects: {FxAccountsOAuthGrantClientError} OAuth client error.
* @private
*/
async _createRequest(path, method = "POST", params) {
let requestUrl = this.serverURL + path;
let request = new this._Request(requestUrl);
method = method.toUpperCase();
request.setHeader("Accept", "application/json");
request.setHeader("Content-Type", "application/json");
if (method != "POST") {
throw new FxAccountsOAuthGrantClientError({
error: ERROR_NETWORK,
errno: ERRNO_NETWORK,
code: ERROR_CODE_METHOD_NOT_ALLOWED,
message: ERROR_MSG_METHOD_NOT_ALLOWED,
});
}
try {
await request.post(params);
} catch (error) {
throw new FxAccountsOAuthGrantClientError({
error: ERROR_NETWORK,
errno: ERRNO_NETWORK,
message: error.toString(),
});
}
let body = null;
try {
body = JSON.parse(request.response.body);
} catch (e) {
throw new FxAccountsOAuthGrantClientError({
error: ERROR_PARSE,
errno: ERRNO_PARSE,
code: request.response.status,
message: request.response.body,
});
}
if (request.response.success) {
return body;
}
if (typeof body.errno === "number") {
// Offset oauth server errnos to avoid conflict with other FxA server errnos
body.errno += OAUTH_SERVER_ERRNO_OFFSET;
} else if (body.errno) {
body.errno = ERRNO_UNKNOWN_ERROR;
}
throw new FxAccountsOAuthGrantClientError(body);
},
};
/**
* Normalized oauth client errors
* @param {Object} [details]
* Error details object
* @param {number} [details.code]
* Error code
* @param {number} [details.errno]
* Error number
* @param {String} [details.error]
* Error description
* @param {String|null} [details.message]
* Error message
* @constructor
*/
var FxAccountsOAuthGrantClientError = function(details) {
details = details || {};
this.name = "FxAccountsOAuthGrantClientError";
this.code = details.code || null;
this.errno = details.errno || ERRNO_UNKNOWN_ERROR;
this.error = details.error || ERROR_UNKNOWN;
this.message = details.message || null;
};
/**
* Returns error object properties
*
* @returns {{name: *, code: *, errno: *, error: *, message: *}}
* @private
*/
FxAccountsOAuthGrantClientError.prototype._toStringFields = function() {
return {
name: this.name,
code: this.code,
errno: this.errno,
error: this.error,
message: this.message,
};
};
/**
* String representation of a oauth grant client error
*
* @returns {String}
*/
FxAccountsOAuthGrantClientError.prototype.toString = function() {
return this.name + "(" + JSON.stringify(this._toStringFields()) + ")";
};

View File

@ -13,7 +13,6 @@ const {
DATA_FORMAT_VERSION,
DEFAULT_STORAGE_FILENAME,
FXA_PWDMGR_HOST,
FXA_PWDMGR_MEMORY_FIELDS,
FXA_PWDMGR_PLAINTEXT_FIELDS,
FXA_PWDMGR_REALM,
FXA_PWDMGR_SECURE_FIELDS,
@ -27,7 +26,6 @@ const { CommonUtils } = ChromeUtils.import(
// the storage manager without having a reference to a manager instance.
function FxAccountsStorageManagerCanStoreField(fieldName) {
return (
FXA_PWDMGR_MEMORY_FIELDS.has(fieldName) ||
FXA_PWDMGR_PLAINTEXT_FIELDS.has(fieldName) ||
FXA_PWDMGR_SECURE_FIELDS.has(fieldName)
);
@ -85,16 +83,12 @@ FxAccountsStorageManager.prototype = {
} else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) {
this.cachedSecure[name] = val;
} else {
// Hopefully it's an "in memory" field. If it's not we log a warning
// but still treat it as such (so it will still be available in this
// session but isn't persisted anywhere.)
if (!FXA_PWDMGR_MEMORY_FIELDS.has(name)) {
log.warn(
"Unknown FxA field name in user data, treating as in-memory",
name
);
}
this.cachedMemory[name] = val;
// Unknown fields are silently discarded, because there is no way
// for them to be read back later.
log.error(
"Unknown FxA field name in user data, it will be ignored",
name
);
}
}
// write it out and we are done.
@ -179,8 +173,6 @@ FxAccountsStorageManager.prototype = {
for (let [name, value] of Object.entries(this.cachedSecure)) {
result[name] = value;
}
// Note we don't return cachedMemory fields here - they must be explicitly
// requested.
return result;
}
// The new explicit way of getting attributes.
@ -189,11 +181,7 @@ FxAccountsStorageManager.prototype = {
}
let checkedSecure = false;
for (let fieldName of fieldNames) {
if (FXA_PWDMGR_MEMORY_FIELDS.has(fieldName)) {
if (this.cachedMemory[fieldName] !== undefined) {
result[fieldName] = this.cachedMemory[fieldName];
}
} else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(fieldName)) {
if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(fieldName)) {
if (this.cachedPlain[fieldName] !== undefined) {
result[fieldName] = this.cachedPlain[fieldName];
}
@ -229,14 +217,11 @@ FxAccountsStorageManager.prototype = {
// work out what bucket.
for (let [name, value] of Object.entries(newFields)) {
if (value == null) {
delete this.cachedMemory[name];
delete this.cachedPlain[name];
// no need to do the "delete on null" thing for this.cachedSecure -
// we need to keep it until we have managed to read so we can nuke
// it on write.
this.cachedSecure[name] = null;
} else if (FXA_PWDMGR_MEMORY_FIELDS.has(name)) {
this.cachedMemory[name] = value;
} else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) {
this.cachedPlain[name] = value;
} else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) {
@ -258,7 +243,6 @@ FxAccountsStorageManager.prototype = {
},
_clearCachedData() {
this.cachedMemory = {};
this.cachedPlain = {};
// If we don't have secure storage available we have cachedPlain and
// cachedSecure be the same object.

View File

@ -22,7 +22,6 @@ EXTRA_JS_MODULES += [
"FxAccountsConfig.jsm",
"FxAccountsDevice.jsm",
"FxAccountsKeys.jsm",
"FxAccountsOAuthGrantClient.jsm",
"FxAccountsPairing.jsm",
"FxAccountsPairingChannel.js",
"FxAccountsProfile.jsm",

View File

@ -10,13 +10,10 @@ const { FxAccountsClient } = ChromeUtils.import(
"resource://gre/modules/FxAccountsClient.jsm"
);
const {
ASSERTION_LIFETIME,
CERT_LIFETIME,
ERRNO_INVALID_AUTH_TOKEN,
ERROR_NETWORK,
ERROR_NO_ACCOUNT,
FX_OAUTH_CLIENT_ID,
KEY_LIFETIME,
ONLOGIN_NOTIFICATION,
ONLOGOUT_NOTIFICATION,
ONVERIFIED_NOTIFICATION,
@ -177,10 +174,6 @@ function MockFxAccountsClient() {
return Promise.resolve(sessionToken);
};
this.signCertificate = function() {
throw new Error("no");
};
this.signOut = () => Promise.resolve();
FxAccountsClient.apply(this);
@ -211,14 +204,6 @@ function MockFxAccounts(credentials = null) {
storage.initialize(newCredentials);
return new AccountState(storage);
},
getCertificateSigned(sessionToken, serializedPublicKey) {
_("mock getCertificateSigned\n");
this._getCertificateSigned_calls.push([
sessionToken,
serializedPublicKey,
]);
return this._d_signCertificate.promise;
},
fxAccountsClient: new MockFxAccountsClient(),
observerPreloads: [],
device: {
@ -280,7 +265,6 @@ add_task(async function test_get_signed_in_user_initially_unset() {
let credentials = {
email: "foo@example.com",
uid: "1234567890abcdef1234567890abcdef",
assertion: "foobar",
sessionToken: "dead",
verified: true,
...MOCK_ACCOUNT_KEYS,
@ -293,7 +277,6 @@ add_task(async function test_get_signed_in_user_initially_unset() {
// getSignedInUser only returns a subset.
result = await account.getSignedInUser();
Assert.deepEqual(result.email, credentials.email);
Assert.deepEqual(result.assertion, undefined);
Assert.deepEqual(result.scopedKeys, undefined);
Assert.deepEqual(result.kSync, undefined);
Assert.deepEqual(result.kXCS, undefined);
@ -302,7 +285,6 @@ add_task(async function test_get_signed_in_user_initially_unset() {
// for the sake of testing, use the low-level function to check it's all there
result = await account._internal.currentAccountState.getUserAccountData();
Assert.deepEqual(result.email, credentials.email);
Assert.deepEqual(result.assertion, credentials.assertion);
Assert.deepEqual(result.scopedKeys, credentials.scopedKeys);
Assert.ok(result.kSync);
Assert.ok(result.kXCS);
@ -324,7 +306,6 @@ add_task(async function test_set_signed_in_user_signs_out_previous_account() {
let credentials = {
email: "foo@example.com",
uid: "1234567890abcdef1234567890abcdef",
assertion: "foobar",
sessionToken: "dead",
verified: true,
...MOCK_ACCOUNT_KEYS,
@ -345,7 +326,6 @@ add_task(async function test_update_account_data() {
let credentials = {
email: "foo@example.com",
uid: "1234567890abcdef1234567890abcdef",
assertion: "foobar",
sessionToken: "dead",
verified: true,
...MOCK_ACCOUNT_KEYS,
@ -355,12 +335,12 @@ add_task(async function test_update_account_data() {
let newCreds = {
email: credentials.email,
uid: credentials.uid,
assertion: "new_assertion",
sessionToken: "alive",
};
await account._internal.updateUserAccountData(newCreds);
Assert.equal(
(await account._internal.getUserAccountData()).assertion,
"new_assertion",
(await account._internal.getUserAccountData()).sessionToken,
"alive",
"new field value was saved"
);
@ -368,7 +348,7 @@ add_task(async function test_update_account_data() {
newCreds = {
email: credentials.email,
uid: "11111111111111111111222222222222",
assertion: "new_assertion",
sessionToken: "alive",
};
await Assert.rejects(
account._internal.updateUserAccountData(newCreds),
@ -377,7 +357,7 @@ add_task(async function test_update_account_data() {
// should fail without the uid.
newCreds = {
assertion: "new_assertion",
sessionToken: "alive",
};
await Assert.rejects(
account._internal.updateUserAccountData(newCreds),
@ -396,133 +376,6 @@ add_task(async function test_update_account_data() {
);
});
add_task(async function test_getCertificateOffline() {
_("getCertificateOffline()");
let credentials = {
email: "foo@example.com",
uid: "1234567890abcdef1234567890abcdef",
sessionToken: "dead",
verified: true,
};
let fxa = await MakeFxAccounts({ credentials });
// Test that an expired cert throws if we're offline.
let offline = Services.io.offline;
Services.io.offline = true;
await fxa._internal
.getKeypairAndCertificate(fxa._internal.currentAccountState)
.then(
result => {
Services.io.offline = offline;
do_throw("Unexpected success");
},
err => {
Services.io.offline = offline;
// ... so we have to check the error string.
Assert.equal(err, "Error: OFFLINE");
}
);
await fxa.signOut(/* localOnly = */ true);
});
add_task(async function test_getCertificateCached() {
_("getCertificateCached()");
let credentials = {
email: "foo@example.com",
uid: "1234567890abcdef1234567890abcdef",
sessionToken: "dead",
verified: true,
// A cached keypair and cert that remain valid.
keyPair: {
validUntil: Date.now() + KEY_LIFETIME + 10000,
rawKeyPair: "good-keypair",
},
cert: {
validUntil: Date.now() + CERT_LIFETIME + 10000,
rawCert: "good-cert",
},
};
let fxa = await MakeFxAccounts({ credentials });
let { keyPair, certificate } = await fxa._internal.getKeypairAndCertificate(
fxa._internal.currentAccountState
);
// should have the same keypair and cert.
Assert.equal(keyPair, credentials.keyPair.rawKeyPair);
Assert.equal(certificate, credentials.cert.rawCert);
await fxa.signOut(/* localOnly = */ true);
});
add_task(async function test_getCertificateExpiredCert() {
_("getCertificateExpiredCert()");
let credentials = {
email: "foo@example.com",
uid: "1234567890abcdef1234567890abcdef",
sessionToken: "dead",
verified: true,
// A cached keypair that remains valid.
keyPair: {
validUntil: Date.now() + KEY_LIFETIME + 10000,
rawKeyPair: "good-keypair",
},
// A cached certificate which has expired.
cert: {
validUntil: Date.parse("Mon, 13 Jan 2000 21:45:06 GMT"),
rawCert: "expired-cert",
},
};
let fxa = await MakeFxAccounts({
internal: {
getCertificateSigned() {
return "new cert";
},
},
credentials,
});
let { keyPair, certificate } = await fxa._internal.getKeypairAndCertificate(
fxa._internal.currentAccountState
);
// should have the same keypair but a new cert.
Assert.equal(keyPair, credentials.keyPair.rawKeyPair);
Assert.notEqual(certificate, credentials.cert.rawCert);
await fxa.signOut(/* localOnly = */ true);
});
add_task(async function test_getCertificateExpiredKeypair() {
_("getCertificateExpiredKeypair()");
let credentials = {
email: "foo@example.com",
uid: "1234567890abcdef",
sessionToken: "dead",
verified: true,
// A cached keypair that has expired.
keyPair: {
validUntil: Date.now() - 1000,
rawKeyPair: "expired-keypair",
},
// A cached certificate which remains valid.
cert: {
validUntil: Date.now() + CERT_LIFETIME + 10000,
rawCert: "expired-cert",
},
};
let fxa = await MakeFxAccounts({
internal: {
getCertificateSigned() {
return "new cert";
},
},
credentials,
});
let { keyPair, certificate } = await fxa._internal.getKeypairAndCertificate(
fxa._internal.currentAccountState
);
// even though the cert was valid, the fact the keypair was not means we
// should have fetched both.
Assert.notEqual(keyPair, credentials.keyPair.rawKeyPair);
Assert.notEqual(certificate, credentials.cert.rawCert);
await fxa.signOut(/* localOnly = */ true);
});
// Sanity-check that our mocked client is working correctly
add_test(function test_client_mock() {
let fxa = new MockFxAccounts();
@ -1156,157 +1009,6 @@ add_test(function test_overlapping_signins() {
});
});
add_task(async function test_getAssertion_invalid_token() {
let fxa = new MockFxAccounts();
let client = fxa._internal.fxAccountsClient;
client.accountStatus = () => Promise.resolve(true);
client.sessionStatus = () => Promise.resolve(false);
let creds = {
sessionToken: "sessionToken",
verified: true,
email: "sonia@example.com",
...MOCK_ACCOUNT_KEYS,
};
await fxa.setSignedInUser(creds);
// we have what we still believe to be a valid session token, so we should
// consider that we have a local session.
Assert.ok(await fxa.hasLocalSession());
try {
let promiseAssertion = fxa._internal.getAssertion("audience.example.com");
fxa._internal._d_signCertificate.reject({
code: 401,
errno: ERRNO_INVALID_AUTH_TOKEN,
});
await promiseAssertion;
Assert.ok(false, "getAssertion should reject invalid session token");
} catch (err) {
Assert.equal(err.code, 401);
Assert.equal(err.errno, ERRNO_INVALID_AUTH_TOKEN);
}
let user = await fxa._internal.getUserAccountData();
Assert.equal(user.email, creds.email);
Assert.equal(user.sessionToken, null);
Assert.ok(!(await fxa.hasLocalSession()));
});
add_task(async function test_getAssertion() {
let fxa = new MockFxAccounts();
let creds = {
sessionToken: "sessionToken",
verified: true,
...MOCK_ACCOUNT_KEYS,
};
// By putting scopedKeys in "creds", we skip ahead to the "we're ready" stage.
await fxa.setSignedInUser(creds);
_("== ready to go\n");
// Start with a nice arbitrary but realistic date. Here we use a nice RFC
// 1123 date string like we would get from an HTTP header. Over the course of
// the test, we will update 'now', but leave 'start' where it is.
let now = Date.parse("Mon, 13 Jan 2014 21:45:06 GMT");
let start = now;
fxa._internal._now_is = now;
let d = fxa._internal.getAssertion("audience.example.com");
// At this point, a thread has been spawned to generate the keys.
_("-- back from fxa.getAssertion\n");
fxa._internal._d_signCertificate.resolve("cert1");
let assertion = await d;
Assert.equal(fxa._internal._getCertificateSigned_calls.length, 1);
Assert.equal(fxa._internal._getCertificateSigned_calls[0][0], "sessionToken");
Assert.notEqual(assertion, null);
_("ASSERTION: " + assertion + "\n");
let pieces = assertion.split("~");
Assert.equal(pieces[0], "cert1");
let userData = await fxa._internal.getUserAccountData();
let keyPair = userData.keyPair;
let cert = userData.cert;
Assert.notEqual(keyPair, undefined);
_(keyPair.validUntil + "\n");
let p2 = pieces[1].split(".");
let header = JSON.parse(atob(p2[0]));
_("HEADER: " + JSON.stringify(header) + "\n");
Assert.equal(header.alg, "DS128");
let payload = JSON.parse(atob(p2[1]));
_("PAYLOAD: " + JSON.stringify(payload) + "\n");
Assert.equal(payload.aud, "audience.example.com");
Assert.equal(keyPair.validUntil, start + KEY_LIFETIME);
Assert.equal(cert.validUntil, start + CERT_LIFETIME);
_("delta: " + Date.parse(payload.exp - start) + "\n");
let exp = Number(payload.exp);
Assert.equal(exp, now + ASSERTION_LIFETIME);
// Reset for next call.
fxa._internal._d_signCertificate = PromiseUtils.defer();
// Getting a new assertion "soon" (i.e., w/o incrementing "now"), even for
// a new audience, should not provoke key generation or a signing request.
assertion = await fxa._internal.getAssertion("other.example.com");
// There were no additional calls - same number of getcert calls as before
Assert.equal(fxa._internal._getCertificateSigned_calls.length, 1);
// Wait an hour; assertion use period expires, but not the certificate
now += ONE_HOUR_MS;
fxa._internal._now_is = now;
// This won't block on anything - will make an assertion, but not get a
// new certificate.
assertion = await fxa._internal.getAssertion("third.example.com");
// Test will time out if that failed (i.e., if that had to go get a new cert)
pieces = assertion.split("~");
Assert.equal(pieces[0], "cert1");
p2 = pieces[1].split(".");
header = JSON.parse(atob(p2[0]));
payload = JSON.parse(atob(p2[1]));
Assert.equal(payload.aud, "third.example.com");
// The keypair and cert should have the same validity as before, but the
// expiration time of the assertion should be different. We compare this to
// the initial start time, to which they are relative, not the current value
// of "now".
userData = await fxa._internal.getUserAccountData();
keyPair = userData.keyPair;
cert = userData.cert;
Assert.equal(keyPair.validUntil, start + KEY_LIFETIME);
Assert.equal(cert.validUntil, start + CERT_LIFETIME);
exp = Number(payload.exp);
Assert.equal(exp, now + ASSERTION_LIFETIME);
// Now we wait even longer, and expect both assertion and cert to expire. So
// we will have to get a new keypair and cert.
now += ONE_DAY_MS;
fxa._internal._now_is = now;
d = fxa._internal.getAssertion("fourth.example.com");
fxa._internal._d_signCertificate.resolve("cert2");
assertion = await d;
Assert.equal(fxa._internal._getCertificateSigned_calls.length, 2);
Assert.equal(fxa._internal._getCertificateSigned_calls[1][0], "sessionToken");
pieces = assertion.split("~");
Assert.equal(pieces[0], "cert2");
p2 = pieces[1].split(".");
header = JSON.parse(atob(p2[0]));
payload = JSON.parse(atob(p2[1]));
Assert.equal(payload.aud, "fourth.example.com");
userData = await fxa._internal.getUserAccountData();
keyPair = userData.keyPair;
cert = userData.cert;
Assert.equal(keyPair.validUntil, now + KEY_LIFETIME);
Assert.equal(cert.validUntil, now + CERT_LIFETIME);
exp = Number(payload.exp);
Assert.equal(exp, now + ASSERTION_LIFETIME);
_("----- DONE ----\n");
});
add_task(async function test_resend_email_not_signed_in() {
let fxa = new MockFxAccounts();
@ -1429,29 +1131,6 @@ Services.prefs.setCharPref(
"https://example.com/v1"
);
add_test(function test_getOAuthToken() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
let oauthTokenCalled = false;
fxa._internal._d_signCertificate.resolve("cert1");
let client = fxa._internal.fxAccountsOAuthGrantClient;
client.getTokenFromAssertion = () => {
oauthTokenCalled = true;
return Promise.resolve({ access_token: "token" });
};
fxa.setSignedInUser(alice).then(() => {
fxa.getOAuthToken({ scope: "profile" }).then(result => {
Assert.ok(oauthTokenCalled);
Assert.equal(result, "token");
run_next_test();
});
});
});
add_test(async function test_getOAuthTokenWithSessionToken() {
Services.prefs.setBoolPref(
"identity.fxaccounts.useSessionTokensForOAuth",
@ -1541,66 +1220,33 @@ add_task(async function test_getOAuthTokenCachedWithSessionToken() {
);
});
add_test(function test_getOAuthTokenScoped() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
let oauthTokenCalled = false;
fxa._internal._d_signCertificate.resolve("cert1");
let client = fxa._internal.fxAccountsOAuthGrantClient;
client.getTokenFromAssertion = (_assertion, scopeString) => {
equal(scopeString, "bar foo"); // scopes are sorted locally before request.
oauthTokenCalled = true;
return Promise.resolve({ access_token: "token" });
};
fxa.setSignedInUser(alice).then(() => {
fxa.getOAuthToken({ scope: ["foo", "bar"] }).then(result => {
Assert.ok(oauthTokenCalled);
Assert.equal(result, "token");
run_next_test();
});
});
});
add_task(async function test_getOAuthTokenCached() {
add_test(function test_getOAuthTokenScopedWithSessionToken() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
let numOauthTokenCalls = 0;
fxa._internal._d_signCertificate.resolve("cert1");
let client = fxa._internal.fxAccountsOAuthGrantClient;
client.getTokenFromAssertion = () => {
numOauthTokenCalls += 1;
return Promise.resolve({ access_token: "token" });
let client = fxa._internal.fxAccountsClient;
client.accessTokenWithSessionToken = async (
_sessionTokenHex,
_clientId,
scopeString
) => {
equal(scopeString, "bar foo"); // scopes are sorted locally before request.
numOauthTokenCalls++;
return MOCK_TOKEN_RESPONSE;
};
await fxa.setSignedInUser(alice);
let result = await fxa.getOAuthToken({
scope: "profile",
service: "test-service",
fxa.setSignedInUser(alice).then(() => {
fxa.getOAuthToken({ scope: ["foo", "bar"] }).then(result => {
Assert.equal(numOauthTokenCalls, 1);
Assert.equal(
result,
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
);
run_next_test();
});
});
Assert.equal(numOauthTokenCalls, 1);
Assert.equal(result, "token");
// requesting it again should not re-fetch the token.
result = await fxa.getOAuthToken({
scope: "profile",
service: "test-service",
});
Assert.equal(numOauthTokenCalls, 1);
Assert.equal(result, "token");
// But requesting the same service and a different scope *will* get a new one.
result = await fxa.getOAuthToken({
scope: "something-else",
service: "test-service",
});
Assert.equal(numOauthTokenCalls, 2);
Assert.equal(result, "token");
});
add_task(async function test_getOAuthTokenCachedScopeNormalization() {
@ -1609,12 +1255,14 @@ add_task(async function test_getOAuthTokenCachedScopeNormalization() {
alice.verified = true;
let numOAuthTokenCalls = 0;
fxa._internal._d_signCertificate.resolve("cert1");
let client = fxa._internal.fxAccountsOAuthGrantClient;
client.getTokenFromAssertion = () => {
numOAuthTokenCalls += 1;
return Promise.resolve({ access_token: "token" });
let client = fxa._internal.fxAccountsClient;
client.accessTokenWithSessionToken = async (
_sessionTokenHex,
_clientId,
scopeString
) => {
numOAuthTokenCalls++;
return MOCK_TOKEN_RESPONSE;
};
await fxa.setSignedInUser(alice);
@ -1623,29 +1271,41 @@ add_task(async function test_getOAuthTokenCachedScopeNormalization() {
service: "test-service",
});
Assert.equal(numOAuthTokenCalls, 1);
Assert.equal(result, "token");
Assert.equal(
result,
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
);
// requesting it again with the scope array in a different order not re-fetch the token.
// requesting it again with the scope array in a different order should not re-fetch the token.
result = await fxa.getOAuthToken({
scope: ["bar", "foo"],
service: "test-service",
});
Assert.equal(numOAuthTokenCalls, 1);
Assert.equal(result, "token");
// requesting it again with the scope array in different case not re-fetch the token.
Assert.equal(
result,
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
);
// requesting it again with the scope array in different case should not re-fetch the token.
result = await fxa.getOAuthToken({
scope: ["Bar", "Foo"],
service: "test-service",
});
Assert.equal(numOAuthTokenCalls, 1);
Assert.equal(result, "token");
Assert.equal(
result,
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
);
// But requesting with a new entry in the array does fetch one.
result = await fxa.getOAuthToken({
scope: ["foo", "bar", "etc"],
service: "test-service",
});
Assert.equal(numOAuthTokenCalls, 2);
Assert.equal(result, "token");
Assert.equal(
result,
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
);
});
add_test(function test_getOAuthToken_invalid_param() {
@ -1715,54 +1375,19 @@ add_test(function test_getOAuthToken_error() {
let alice = getTestUser("alice");
alice.verified = true;
fxa._internal._d_signCertificate.resolve("cert1");
let client = fxa._internal.fxAccountsOAuthGrantClient;
client.getTokenFromAssertion = () => {
let client = fxa._internal.fxAccountsClient;
client.accessTokenWithSessionToken = () => {
return Promise.reject("boom");
};
fxa.setSignedInUser(alice).then(() => {
fxa.getOAuthToken({ scope: "profile" }).catch(err => {
equal(err.details, "boom");
run_next_test();
});
});
});
add_task(async function test_getOAuthToken_authErrorRefreshesCertificate() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
fxa._internal._d_signCertificate.resolve("cert1");
let client = fxa._internal.fxAccountsOAuthGrantClient;
let numTokenCalls = 0;
client.getTokenFromAssertion = () => {
numTokenCalls++;
// First time around, reject with a 401.
if (numTokenCalls == 1) {
return Promise.reject({
code: 401,
errno: 1104,
});
}
// Second time around, succeed.
if (numTokenCalls == 2) {
return Promise.resolve({ access_token: "token" });
}
throw new Error("too many token calls");
};
await fxa.setSignedInUser(alice);
let result = await fxa.getOAuthToken({ scope: "profile" });
Assert.equal(result, "token");
Assert.equal(numTokenCalls, 2);
Assert.equal(fxa._internal._getCertificateSigned_calls.length, 2);
});
add_task(async function test_listAttachedOAuthClients() {
const ONE_HOUR = 60 * 60 * 1000;
const ONE_DAY = 24 * ONE_HOUR;

View File

@ -613,65 +613,6 @@ add_task(async function test_accountKeys() {
await promiseStopServer(server);
});
add_task(async function test_signCertificate() {
let certSignMessage = JSON.stringify({ cert: { bar: "baz" } });
let errorMessage = JSON.stringify({
code: 400,
errno: 102,
error: "doesn't exist",
});
let tries = 0;
let server = httpd_setup({
"/certificate/sign": function(request, response) {
Assert.ok(request.hasHeader("Authorization"));
Assert.ok(request.queryString.startsWith("service="));
if (tries === 0) {
tries += 1;
let body = CommonUtils.readBytesFromInputStream(
request.bodyInputStream
);
let jsonBody = JSON.parse(body);
Assert.equal(JSON.parse(jsonBody.publicKey).foo, "bar");
Assert.equal(jsonBody.duration, 600);
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(
certSignMessage,
certSignMessage.length
);
return;
}
// Second attempt, trigger error
response.setStatusLine(request.httpVersion, 400, "Bad request");
response.bodyOutputStream.write(errorMessage, errorMessage.length);
},
});
let client = new FxAccountsClient(server.baseURI);
let result = await client.signCertificate(
FAKE_SESSION_TOKEN,
JSON.stringify({ foo: "bar" }),
600
);
Assert.equal("baz", result.bar);
// Account doesn't exist
try {
result = await client.signCertificate(
"bogus",
JSON.stringify({ foo: "bar" }),
600
);
do_throw("Expected to catch an exception");
} catch (expectedError) {
Assert.equal(102, expectedError.errno);
}
await promiseStopServer(server);
});
add_task(async function test_accessTokenWithSessionToken() {
let server = httpd_setup({
"/oauth/token": function(request, response) {

View File

@ -74,7 +74,6 @@ add_task(async function test_reset() {
let credentials = {
email: "foo@example.com",
uid: "1234@lcip.org",
assertion: "foobar",
sessionToken: "dead",
verified: true,
...MOCK_ACCOUNT_KEYS,

View File

@ -1,308 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const {
ERRNO_INVALID_FXA_ASSERTION,
ERRNO_NETWORK,
ERRNO_PARSE,
ERRNO_UNKNOWN_ERROR,
ERROR_CODE_METHOD_NOT_ALLOWED,
ERROR_MSG_METHOD_NOT_ALLOWED,
ERROR_NETWORK,
ERROR_PARSE,
ERROR_UNKNOWN,
} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
const {
FxAccountsOAuthGrantClient,
FxAccountsOAuthGrantClientError,
} = ChromeUtils.import("resource://gre/modules/FxAccountsOAuthGrantClient.jsm");
const CLIENT_OPTIONS = {
serverURL: "https://127.0.0.1:9010/v1",
client_id: "abc123",
};
const STATUS_SUCCESS = 200;
/**
* Mock request responder
* @param {String} response
* Mocked raw response from the server
* @returns {Function}
*/
var mockResponse = function(response) {
return function() {
return {
setHeader() {},
async post() {
this.response = response;
return response;
},
};
};
};
/**
* Mock request error responder
* @param {Error} error
* Error object
* @returns {Function}
*/
var mockResponseError = function(error) {
return function() {
return {
setHeader() {},
async post() {
throw error;
},
};
};
};
add_test(function missingParams() {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
try {
client.getTokenFromAssertion();
} catch (e) {
Assert.equal(e.message, "Missing 'assertion' parameter");
}
try {
client.getTokenFromAssertion("assertion");
} catch (e) {
Assert.equal(e.message, "Missing 'scope' parameter");
}
run_next_test();
});
add_test(function successfulResponse() {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
let response = {
success: true,
status: STATUS_SUCCESS,
body: JSON.stringify({
access_token: "http://example.com/image.jpeg",
id: "0d5c1a89b8c54580b8e3e8adadae864a",
}),
};
client._Request = new mockResponse(response);
client.getTokenFromAssertion("assertion", "scope").then(function(result) {
Assert.equal(result.access_token, "http://example.com/image.jpeg");
run_next_test();
});
});
add_test(function parseErrorResponse() {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
let response = {
success: true,
status: STATUS_SUCCESS,
body: "unexpected",
};
client._Request = new mockResponse(response);
client.getTokenFromAssertion("assertion", "scope").catch(function(e) {
Assert.equal(e.name, "FxAccountsOAuthGrantClientError");
Assert.equal(e.code, STATUS_SUCCESS);
Assert.equal(e.errno, ERRNO_PARSE);
Assert.equal(e.error, ERROR_PARSE);
Assert.equal(e.message, "unexpected");
run_next_test();
});
});
add_task(async function serverErrorResponse() {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
let response = {
status: 400,
body: JSON.stringify({
code: 400,
errno: 104,
error: "Bad Request",
message: "Unauthorized",
reason: "Invalid fxa assertion",
}),
};
client._Request = new mockResponse(response);
client.getTokenFromAssertion("blah", "scope").catch(function(e) {
Assert.equal(e.name, "FxAccountsOAuthGrantClientError");
Assert.equal(e.code, 400);
Assert.equal(e.errno, ERRNO_INVALID_FXA_ASSERTION);
Assert.equal(e.error, "Bad Request");
Assert.equal(e.message, "Unauthorized");
run_next_test();
});
});
add_task(async function networkErrorResponse() {
let client = new FxAccountsOAuthGrantClient({
serverURL: "https://domain.dummy",
client_id: "abc123",
});
client.getTokenFromAssertion("assertion", "scope").catch(function(e) {
Assert.equal(e.name, "FxAccountsOAuthGrantClientError");
Assert.equal(e.code, null);
Assert.equal(e.errno, ERRNO_NETWORK);
Assert.equal(e.error, ERROR_NETWORK);
run_next_test();
});
});
add_test(function unsupportedMethod() {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
return client._createRequest("/", "PUT").catch(function(e) {
Assert.equal(e.name, "FxAccountsOAuthGrantClientError");
Assert.equal(e.code, ERROR_CODE_METHOD_NOT_ALLOWED);
Assert.equal(e.errno, ERRNO_NETWORK);
Assert.equal(e.error, ERROR_NETWORK);
Assert.equal(e.message, ERROR_MSG_METHOD_NOT_ALLOWED);
run_next_test();
});
});
add_test(function onCompleteRequestError() {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
client._Request = new mockResponseError(new Error("onComplete error"));
client.getTokenFromAssertion("assertion", "scope").catch(function(e) {
Assert.equal(e.name, "FxAccountsOAuthGrantClientError");
Assert.equal(e.code, null);
Assert.equal(e.errno, ERRNO_NETWORK);
Assert.equal(e.error, ERROR_NETWORK);
Assert.equal(e.message, "Error: onComplete error");
run_next_test();
});
});
add_test(function incorrectErrno() {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
let response = {
status: 400,
body: JSON.stringify({
code: 400,
errno: "bad errno",
error: "Bad Request",
message: "Unauthorized",
reason: "Invalid fxa assertion",
}),
};
client._Request = new mockResponse(response);
client.getTokenFromAssertion("blah", "scope").catch(function(e) {
Assert.equal(e.name, "FxAccountsOAuthGrantClientError");
Assert.equal(e.code, 400);
Assert.equal(e.errno, ERRNO_UNKNOWN_ERROR);
Assert.equal(e.error, "Bad Request");
Assert.equal(e.message, "Unauthorized");
run_next_test();
});
});
add_test(function constructorTests() {
try {
Services.prefs.setCharPref(
"identity.fxaccounts.remote.oauth.uri",
"https://example.com/v1"
);
validationHelper({}, "Error: Missing 'client_id' parameter");
} finally {
Services.prefs.clearUserPref("identity.fxaccounts.remote.oauth.uri");
}
validationHelper(
{ serverURL: "https://example.com" },
"Error: Missing 'client_id' parameter"
);
validationHelper(
{ serverURL: "https://example.com" },
"Error: Missing 'client_id' parameter"
);
validationHelper(
{ client_id: "123ABC", serverURL: "http://example.com" },
"Error: 'serverURL' must be HTTPS"
);
try {
Services.prefs.setBoolPref("identity.fxaccounts.allowHttp", true);
validationHelper(
{ client_id: "123ABC", serverURL: "http://example.com" },
null
);
} finally {
Services.prefs.clearUserPref("identity.fxaccounts.allowHttp");
}
run_next_test();
});
add_test(function errorTests() {
let error1 = new FxAccountsOAuthGrantClientError();
Assert.equal(error1.name, "FxAccountsOAuthGrantClientError");
Assert.equal(error1.code, null);
Assert.equal(error1.errno, ERRNO_UNKNOWN_ERROR);
Assert.equal(error1.error, ERROR_UNKNOWN);
Assert.equal(error1.message, null);
let error2 = new FxAccountsOAuthGrantClientError({
code: STATUS_SUCCESS,
errno: 1,
error: "Error",
message: "Something",
});
let fields2 = error2._toStringFields();
let statusCode = 1;
Assert.equal(error2.name, "FxAccountsOAuthGrantClientError");
Assert.equal(error2.code, STATUS_SUCCESS);
Assert.equal(error2.errno, statusCode);
Assert.equal(error2.error, "Error");
Assert.equal(error2.message, "Something");
Assert.equal(fields2.name, "FxAccountsOAuthGrantClientError");
Assert.equal(fields2.code, STATUS_SUCCESS);
Assert.equal(fields2.errno, statusCode);
Assert.equal(fields2.error, "Error");
Assert.equal(fields2.message, "Something");
Assert.ok(error2.toString().includes("Something"));
run_next_test();
});
add_test(function networkErrorResponse() {
let client = new FxAccountsOAuthGrantClient({
serverURL: "https://domain.dummy",
client_id: "abc123",
});
client.getTokenFromAssertion("assertion", "scope").catch(function(e) {
Assert.equal(e.name, "FxAccountsOAuthGrantClientError");
Assert.equal(e.code, null);
Assert.equal(e.errno, ERRNO_NETWORK);
Assert.equal(e.error, ERROR_NETWORK);
run_next_test();
});
});
/**
* Quick way to test the "FxAccountsOAuthGrantClient" constructor.
*
* @param {Object} options
* FxAccountsOAuthGrantClient constructor options
* @param {String} expected
* Expected error message, or null if it's expected to pass.
* @returns {*}
*/
function validationHelper(options, expected) {
try {
new FxAccountsOAuthGrantClient(options);
} catch (e) {
return Assert.equal(e.toString(), expected);
}
return Assert.equal(expected, null);
}

View File

@ -1,96 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// A test of FxAccountsOAuthGrantClient but using a real server it can
// hit.
"use strict";
const { FxAccountsOAuthGrantClient } = ChromeUtils.import(
"resource://gre/modules/FxAccountsOAuthGrantClient.jsm"
);
// handlers for our server.
var numTokenFetches;
var authorizeError;
function authorize(request, response) {
if (authorizeError) {
response.setStatusLine(
"1.1",
authorizeError.status,
authorizeError.description
);
response.write(JSON.stringify(authorizeError.body));
} else {
response.setStatusLine("1.1", 200, "OK");
let token = "token" + numTokenFetches;
numTokenFetches += 1;
response.write(JSON.stringify({ access_token: token }));
}
}
function startServer() {
numTokenFetches = 0;
authorizeError = null;
let srv = new HttpServer();
srv.registerPathHandler("/v1/authorization", authorize);
srv.start(-1);
return srv;
}
async function runWithServer(cb) {
Services.prefs.setBoolPref("identity.fxaccounts.allowHttp", true);
let server = startServer();
try {
return await cb(server);
} finally {
await promiseStopServer(server);
Services.prefs.clearUserPref("identity.fxaccounts.allowHttp");
}
}
add_task(async function test_getToken() {
await runWithServer(async server => {
let clientOptions = {
serverURL: "http://localhost:" + server.identity.primaryPort + "/v1",
client_id: "abc123",
};
let client = new FxAccountsOAuthGrantClient(clientOptions);
let result = await client.getTokenFromAssertion("assertion", "scope");
equal(result.access_token, "token0");
equal(numTokenFetches, 1, "we hit the server to fetch a token");
});
});
add_task(async function test_authError() {
await runWithServer(async server => {
let clientOptions = {
serverURL: "http://localhost:" + server.identity.primaryPort + "/v1",
client_id: "abc123",
};
authorizeError = {
status: 401,
description: "Unauthorized",
body: {
code: 401,
errno: 101,
message: "fake error",
},
};
let client = new FxAccountsOAuthGrantClient(clientOptions);
try {
await client.getTokenFromAssertion("assertion", "scope");
do_throw("Expected to catch an exception");
} catch (e) {
equal(e.name, "FxAccountsOAuthGrantClientError");
equal(e.code, 401);
// The errno is offset by 1000 to distinguish from auth-server errnos.
equal(e.errno, 1101);
equal(e.error, "UNKNOWN_ERROR");
equal(e.message, "fake error");
}
});
});

View File

@ -122,7 +122,6 @@ async function createMockFxA() {
let credentials = {
email: "foo@example.com",
uid: "1234@lcip.org",
assertion: "foobar",
sessionToken: "dead",
kSync: "beef",
kXCS: "cafe",

View File

@ -82,6 +82,21 @@ function MockFxAccountsClient(activeTokens) {
this.getDeviceList = function() {
return Promise.resolve();
};
this.accessTokenWithSessionToken = function(
sessionTokenHex,
clientId,
scope,
ttl
) {
let token = `token${this.numTokenFetches}`;
if (ttl) {
token += `-ttl-${ttl}`;
}
this.numTokenFetches += 1;
this.activeTokens.add(token);
print("accessTokenWithSessionToken returning token", token);
return Promise.resolve({ access_token: token, ttl });
};
this.oauthDestroy = sinon.stub().callsFake((_clientId, token) => {
this.activeTokens.delete(token);
return Promise.resolve();
@ -89,6 +104,7 @@ function MockFxAccountsClient(activeTokens) {
// Test only stuff.
this.activeTokens = activeTokens;
this.numTokenFetches = 0;
FxAccountsClient.apply(this);
}
@ -97,36 +113,12 @@ MockFxAccountsClient.prototype = {
__proto__: FxAccountsClient.prototype,
};
function MockFxAccountsOAuthGrantClient(activeTokens) {
// Test only stuff.
this.numTokenFetches = 0;
this.activeTokens = activeTokens;
}
MockFxAccountsOAuthGrantClient.prototype = {
serverURL: { href: "http://localhost" },
getTokenFromAssertion(assertion, scope, ttl) {
let token = `token${this.numTokenFetches}`;
if (ttl) {
token += `-ttl-${ttl}`;
}
this.numTokenFetches += 1;
this.activeTokens.add(token);
print("getTokenFromAssertion returning token", token);
return Promise.resolve({ access_token: token, ttl });
},
};
function MockFxAccounts() {
// The FxA "auth" and "oauth" servers both share the same db of tokens,
// so we need to simulate the same here in the tests.
const activeTokens = new Set();
return new FxAccounts({
fxAccountsClient: new MockFxAccountsClient(activeTokens),
fxAccountsOAuthGrantClient: new MockFxAccountsOAuthGrantClient(
activeTokens
),
getAssertion: () => Promise.resolve("assertion"),
newAccountState(credentials) {
// we use a real accountState but mocked storage.
let storage = new MockStorageManager();
@ -153,7 +145,6 @@ async function createMockFxA() {
let credentials = {
email: "foo@example.com",
uid: "1234@lcip.org",
assertion: "foobar",
sessionToken: "dead",
kSync: "beef",
kXCS: "cafe",
@ -172,12 +163,11 @@ add_task(async function testRevoke() {
let tokenOptions = { scope: "test-scope" };
let fxa = await createMockFxA();
let client = fxa._internal.fxAccountsClient;
let oauthClient = fxa._internal.fxAccountsOAuthGrantClient;
// get our first token and check we hit the mock.
let token1 = await fxa.getOAuthToken(tokenOptions);
equal(oauthClient.numTokenFetches, 1);
equal(oauthClient.activeTokens.size, 1);
equal(client.numTokenFetches, 1);
equal(client.activeTokens.size, 1);
ok(token1, "got a token");
equal(token1, "token0");
@ -186,11 +176,11 @@ add_task(async function testRevoke() {
ok(client.oauthDestroy.calledOnce);
// the revoke should have been successful.
equal(oauthClient.activeTokens.size, 0);
equal(client.activeTokens.size, 0);
// fetching it again hits the server.
let token2 = await fxa.getOAuthToken(tokenOptions);
equal(oauthClient.numTokenFetches, 2);
equal(oauthClient.activeTokens.size, 1);
equal(client.numTokenFetches, 2);
equal(client.activeTokens.size, 1);
ok(token2, "got a token");
notEqual(token1, token2, "got a different token");
});
@ -198,18 +188,17 @@ add_task(async function testRevoke() {
add_task(async function testSignOutDestroysTokens() {
let fxa = await createMockFxA();
let client = fxa._internal.fxAccountsClient;
let oauthClient = fxa._internal.fxAccountsOAuthGrantClient;
// get our first token and check we hit the mock.
let token1 = await fxa.getOAuthToken({ scope: "test-scope" });
equal(oauthClient.numTokenFetches, 1);
equal(oauthClient.activeTokens.size, 1);
equal(client.numTokenFetches, 1);
equal(client.activeTokens.size, 1);
ok(token1, "got a token");
// get another
let token2 = await fxa.getOAuthToken({ scope: "test-scope-2" });
equal(oauthClient.numTokenFetches, 2);
equal(oauthClient.activeTokens.size, 2);
equal(client.numTokenFetches, 2);
equal(client.activeTokens.size, 2);
ok(token2, "got a token");
notEqual(token1, token2, "got a different token");
@ -220,7 +209,7 @@ add_task(async function testSignOutDestroysTokens() {
await signoutComplete;
ok(client.oauthDestroy.calledTwice);
// No active tokens left.
equal(oauthClient.activeTokens.size, 0);
equal(client.activeTokens.size, 0);
});
add_task(async function testTokenRaces() {
@ -231,7 +220,6 @@ add_task(async function testTokenRaces() {
// fetches, but each of the 4 calls should resolve with the correct values.
let fxa = await createMockFxA();
let client = fxa._internal.fxAccountsClient;
let oauthClient = fxa._internal.fxAccountsOAuthGrantClient;
let results = await Promise.all([
fxa.getOAuthToken({ scope: "test-scope" }),
@ -240,18 +228,18 @@ add_task(async function testTokenRaces() {
fxa.getOAuthToken({ scope: "test-scope-2" }),
]);
equal(oauthClient.numTokenFetches, 2, "should have fetched 2 tokens.");
equal(client.numTokenFetches, 2, "should have fetched 2 tokens.");
// Should have 2 unique tokens
results.sort();
equal(results[0], results[1]);
equal(results[2], results[3]);
// should be 2 active.
equal(oauthClient.activeTokens.size, 2);
equal(client.activeTokens.size, 2);
await fxa.removeCachedOAuthToken({ token: results[0] });
equal(oauthClient.activeTokens.size, 1);
equal(client.activeTokens.size, 1);
await fxa.removeCachedOAuthToken({ token: results[2] });
equal(oauthClient.activeTokens.size, 0);
equal(client.activeTokens.size, 0);
ok(client.oauthDestroy.calledTwice);
});

View File

@ -370,26 +370,6 @@ add_task(async function checkEverythingReadSecure() {
Assert.equal(accountData.kXCS, "kXCS");
});
add_task(async function checkMemoryFieldsNotReturnedByDefault() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({
uid: "uid",
email: "someone@somewhere.com",
});
sm.secureStorage = new MockedSecureStorage({ kXCS: "kXCS" });
await sm.initialize();
// keyPair is a memory field.
await sm.updateAccountData({ keyPair: "the keypair value" });
let accountData = await sm.getAccountData();
// Requesting everything should *not* return in memory fields.
Assert.strictEqual(accountData.keyPair, undefined);
// But requesting them specifically does get them.
accountData = await sm.getAccountData("keyPair");
Assert.strictEqual(accountData.keyPair, "the keypair value");
});
add_task(async function checkExplicitGet() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({

View File

@ -14,8 +14,6 @@ support-files =
[test_device.js]
[test_keys.js]
[test_loginmgr_storage.js]
[test_oauth_grant_client.js]
[test_oauth_grant_client_server.js]
[test_oauth_tokens.js]
[test_oauth_token_storage.js]
[test_pairing.js]

View File

@ -8,8 +8,8 @@ var EXPORTED_SYMBOLS = ["initializeIdentityWithTokenServerResponse"];
const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
const { Weave } = ChromeUtils.import("resource://services-sync/main.js");
const { BrowserIDManager } = ChromeUtils.import(
"resource://services-sync/browserid_identity.js"
const { SyncAuthManager } = ChromeUtils.import(
"resource://services-sync/sync_auth.js"
);
const { TokenServerClient } = ChromeUtils.import(
"resource://services-common/tokenserverclient.js"
@ -18,7 +18,7 @@ const { configureFxAccountIdentity } = ChromeUtils.import(
"resource://testing-common/services/sync/utils.js"
);
// Create a new browserid_identity object and initialize it with a
// Create a new sync_auth object and initialize it with a
// mocked TokenServerClient which always receives the specified response.
var initializeIdentityWithTokenServerResponse = function(response) {
// First create a mock "request" object that well' hack into the token server.
@ -47,17 +47,17 @@ var initializeIdentityWithTokenServerResponse = function(response) {
MockTSC.prototype.newRESTRequest = function(url) {
return new MockRESTRequest(url);
};
// Arrange for the same observerPrefix as browserid_identity uses.
// Arrange for the same observerPrefix as sync_auth uses.
MockTSC.prototype.observerPrefix = "weave:service";
// tie it all together.
Weave.Status.__authManager = Weave.Service.identity = new BrowserIDManager();
let browseridManager = Weave.Service.identity;
Weave.Status.__authManager = Weave.Service.identity = new SyncAuthManager();
let syncAuthManager = Weave.Service.identity;
// a sanity check
if (!(browseridManager instanceof BrowserIDManager)) {
throw new Error("sync isn't configured for browserid_identity");
if (!(syncAuthManager instanceof SyncAuthManager)) {
throw new Error("sync isn't configured to use sync_auth");
}
let mockTSC = new MockTSC();
configureFxAccountIdentity(browseridManager);
browseridManager._tokenServerClient = mockTSC;
configureFxAccountIdentity(syncAuthManager);
syncAuthManager._tokenServerClient = mockTSC;
};

View File

@ -150,7 +150,6 @@ var makeIdentityConfig = function(overrides) {
// fxaccount specific credentials.
fxaccount: {
user: {
assertion: "assertion",
email: "foo",
kSync: "a".repeat(128),
kXCS: "b".repeat(32),
@ -214,10 +213,8 @@ var makeFxAccountsInternalMock = function(config) {
let accountState = new AccountState(storageManager);
return accountState;
},
_getAssertion(audience) {
return Promise.resolve(config.fxaccount.user.assertion);
},
getOAuthToken: () => Promise.resolve("some-access-token"),
_destroyOAuthToken: () => Promise.resolve(),
keys: {
getScopedKeys: () =>
Promise.resolve({
@ -264,16 +261,7 @@ var configureFxAccountIdentity = function(
let mockTSC = {
// TokenServerClient
async getTokenFromBrowserIDAssertion(uri, assertion) {
Assert.equal(
uri,
Services.prefs.getStringPref("identity.sync.tokenserver.uri")
);
Assert.equal(assertion, config.fxaccount.user.assertion);
config.fxaccount.token.uid = config.username;
return config.fxaccount.token;
},
async getTokenFromOAuthToken(url, oauthToken) {
async getTokenUsingOAuth(url, oauthToken) {
Assert.equal(
url,
Services.prefs.getStringPref("identity.sync.tokenserver.uri")
@ -285,7 +273,7 @@ var configureFxAccountIdentity = function(
};
authService._fxaService = fxa;
authService._tokenServerClient = mockTSC;
// Set the "account" of the browserId manager to be the "email" of the
// Set the "account" of the sync auth manager to be the "email" of the
// logged in user of the mockFXA service.
authService._signedInUser = config.fxaccount.user;
authService._account = config.fxaccount.user.email;

View File

@ -17,8 +17,8 @@ const {
SYNC_SUCCEEDED,
} = ChromeUtils.import("resource://services-sync/constants.js");
const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
const { BrowserIDManager } = ChromeUtils.import(
"resource://services-sync/browserid_identity.js"
const { SyncAuthManager } = ChromeUtils.import(
"resource://services-sync/sync_auth.js"
);
var Status = {
@ -30,7 +30,7 @@ var Status = {
if (this.__authManager) {
return this.__authManager;
}
this.__authManager = new BrowserIDManager();
this.__authManager = new SyncAuthManager();
return this.__authManager;
},

View File

@ -4,7 +4,7 @@
"use strict";
var EXPORTED_SYMBOLS = ["BrowserIDManager", "AuthenticationError"];
var EXPORTED_SYMBOLS = ["SyncAuthManager", "AuthenticationError"];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
@ -57,7 +57,7 @@ ChromeUtils.defineModuleGetter(
);
XPCOMUtils.defineLazyGetter(this, "log", function() {
let log = Log.repository.getLogger("Sync.BrowserIDManager");
let log = Log.repository.getLogger("Sync.SyncAuthManager");
log.manageLevelFromPref("services.sync.log.logger.identity");
return log;
});
@ -68,12 +68,6 @@ XPCOMUtils.defineLazyPreferenceGetter(
"services.sync.debug.ignoreCachedAuthCredentials"
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"USE_OAUTH_FOR_SYNC_TOKEN",
"identity.sync.useOAuthForSyncToken"
);
// FxAccountsCommon.js doesn't use a "namespace", so create one here.
var fxAccountsCommon = {};
ChromeUtils.import(
@ -109,7 +103,13 @@ AuthenticationError.prototype = {
},
};
function BrowserIDManager() {
// The `SyncAuthManager` coordinates access authorization to the Sync server.
// Its job is essentially to get us from having a signed-in Firefox Accounts user,
// to knowing the user's sync storage node and having the necessary short-lived
// credentials in order to access it.
//
function SyncAuthManager() {
// NOTE: _fxaService and _tokenServerClient are replaced with mocks by
// the test suite.
this._fxaService = fxAccounts;
@ -128,7 +128,7 @@ function BrowserIDManager() {
}
}
this.BrowserIDManager.prototype = {
this.SyncAuthManager.prototype = {
_fxaService: null,
_tokenServerClient: null,
// https://docs.services.mozilla.com/token/apis.html
@ -408,15 +408,12 @@ this.BrowserIDManager.prototype = {
throw new Error("Can't fetch a token as we can't get keys");
}
// Do the assertion/certificate/token dance, with a retry.
// Do the token dance, with a retry in case of transient auth failure.
// We need to prove that we know the sync key in order to get a token
// from the tokenserver.
let getToken = async key => {
this._log.info("Getting a sync token from", this._tokenServerUrl);
let token;
if (USE_OAUTH_FOR_SYNC_TOKEN) {
token = await this._fetchTokenUsingOAuth(key);
} else {
token = await this._fetchTokenUsingBrowserID(key);
}
let token = await this._fetchTokenUsingOAuth(key);
this._log.trace("Successfully got a token");
return token;
};
@ -455,6 +452,7 @@ this.BrowserIDManager.prototype = {
return token;
} catch (caughtErr) {
let err = caughtErr; // The error we will rethrow.
// TODO: unify these errors - we need to handle errors thrown by
// both tokenserverclient and hawkclient.
// A tokenserver error thrown based on a bad response.
@ -469,8 +467,8 @@ this.BrowserIDManager.prototype = {
}
// TODO: write tests to make sure that different auth error cases are handled here
// properly: auth error getting assertion, auth error getting token (invalid generation
// and client-state error)
// properly: auth error getting oauth token, auth error getting sync token (invalid
// generation or client-state error)
if (err instanceof AuthenticationError) {
this._log.error("Authentication error in _fetchTokenForUser", err);
// set it to the "fatal" LOGIN_FAILED_LOGIN_REJECTED reason.
@ -486,7 +484,7 @@ this.BrowserIDManager.prototype = {
},
/**
* Fetches an OAuth token using the OLD_SYNC scope and exchanges it
* Generates an OAuth access_token using the OLD_SYNC scope and exchanges it
* for a TokenServer token.
*
* @returns {Promise}
@ -502,13 +500,12 @@ this.BrowserIDManager.prototype = {
};
return this._tokenServerClient
.getTokenFromOAuthToken(this._tokenServerUrl, accessToken, headers)
.getTokenUsingOAuth(this._tokenServerUrl, accessToken, headers)
.catch(async err => {
if (err.response && err.response.status === 401) {
// remove the cached token if we cannot authorize with it.
// we have to do this here because we know which `token` to remove
// from cache.
console.log("REMOVE CACHED", accessToken);
await fxa.removeCachedOAuthToken({ token: accessToken });
}
@ -517,38 +514,6 @@ this.BrowserIDManager.prototype = {
});
},
/**
* Exchanges a BrowserID assertion for a TokenServer token.
*
* This is a legacy access method that we're in the process of deprecating;
* if you have a choice you should use `_fetchTokenUsingOAuth` above.
*
* @returns {Promise}
* @private
*/
async _fetchTokenUsingBrowserID(key) {
this._log.debug("Getting a token using BrowserID");
const fxa = this._fxaService;
const audience = Services.io.newURI(this._tokenServerUrl).prePath;
const assertion = await fxa._internal.getAssertion(audience);
const headers = {
"X-Client-State": fxa._internal.keys.kidAsHex(key),
};
return this._tokenServerClient
.getTokenFromBrowserIDAssertion(this._tokenServerUrl, assertion, headers)
.catch(async err => {
if (err.response && err.response.status === 401) {
this._log.warn(
"Token server returned 401, refreshing certificate and retrying token fetch"
);
await fxa._internal.invalidateCertificate();
}
// continue the error chain, so other handlers can deal with the error.
throw err;
});
},
// Returns a promise that is resolved with a valid token for the current
// user, or rejects if one can't be obtained.
// NOTE: This does all the authentication for Sync - it both sets the

View File

@ -22,7 +22,7 @@ const { XPCOMUtils } = ChromeUtils.import(
XPCOMUtils.defineLazyModuleGetters(this, {
Async: "resource://services-common/async.js",
AuthenticationError: "resource://services-sync/browserid_identity.js",
AuthenticationError: "resource://services-sync/sync_auth.js",
fxAccounts: "resource://gre/modules/FxAccounts.jsm",
FxAccounts: "resource://gre/modules/FxAccounts.jsm",
Log: "resource://gre/modules/Log.jsm",

View File

@ -20,7 +20,6 @@ EXTRA_JS_MODULES["services-sync"] += [
"modules/addonutils.js",
"modules/bookmark_validator.js",
"modules/bridged_engine.js",
"modules/browserid_identity.js",
"modules/collection_validator.js",
"modules/constants.js",
"modules/doctor.js",
@ -32,6 +31,7 @@ EXTRA_JS_MODULES["services-sync"] += [
"modules/resource.js",
"modules/service.js",
"modules/status.js",
"modules/sync_auth.js",
"modules/SyncDisconnect.jsm",
"modules/SyncedTabs.jsm",
"modules/telemetry.js",

View File

@ -12,8 +12,8 @@ const { RESTRequest } = ChromeUtils.import(
);
const { Service } = ChromeUtils.import("resource://services-sync/service.js");
const { Status } = ChromeUtils.import("resource://services-sync/status.js");
const { BrowserIDManager } = ChromeUtils.import(
"resource://services-sync/browserid_identity.js"
const { SyncAuthManager } = ChromeUtils.import(
"resource://services-sync/sync_auth.js"
);
const { PromiseUtils } = ChromeUtils.import(
"resource://gre/modules/PromiseUtils.jsm"
@ -24,8 +24,8 @@ add_task(async function setup() {
// add-ons engine.
await Service.engineManager.clear();
// Setup the FxA identity manager and cluster manager.
Status.__authManager = Service.identity = new BrowserIDManager();
// Setup the sync auth manager.
Status.__authManager = Service.identity = new SyncAuthManager();
});
// API-compatible with SyncServer handler. Bind `handler` to something to use
@ -66,25 +66,7 @@ function prepareServer(cbAfterTokenFetch) {
let numReassigns = 0;
return configureIdentity(config).then(() => {
Service.identity._tokenServerClient = {
getTokenFromBrowserIDAssertion(uri, assertion) {
return new Promise(res => {
// Build a new URL with trailing zeros for the SYNC_VERSION part - this
// will still be seen as equivalent by the test server, but different
// by sync itself.
numReassigns += 1;
let trailingZeros = new Array(numReassigns + 1).join("0");
let token = config.fxaccount.token;
token.endpoint = server.baseURI + "1.1" + trailingZeros + "/johndoe";
token.uid = config.username;
_(`test server saw token fetch - endpoint now ${token.endpoint}`);
numTokenRequests += 1;
res(token);
if (cbAfterTokenFetch) {
cbAfterTokenFetch();
}
});
},
getTokenFromOAuthToken() {
getTokenUsingOAuth() {
return new Promise(res => {
// Build a new URL with trailing zeros for the SYNC_VERSION part - this
// will still be seen as equivalent by the test server, but different

View File

@ -4,7 +4,6 @@
const modules = [
"addonutils.js",
"addonsreconciler.js",
"browserid_identity.js",
"constants.js",
"engines/addons.js",
"engines/bookmarks.js",
@ -25,6 +24,7 @@ const modules = [
"stages/declined.js",
"stages/enginesync.js",
"status.js",
"sync_auth.js",
"util.js",
];

View File

@ -78,11 +78,7 @@ async function syncAndExpectNodeReassignment(
let getTokenCount = 0;
let mockTSC = {
// TokenServerClient
async getTokenFromBrowserIDAssertion(uri, assertion) {
getTokenCount++;
return { endpoint: server.baseURI + "1.1/johndoe/" };
},
async getTokenFromOAuthToken() {
async getTokenUsingOAuth() {
getTokenCount++;
return { endpoint: server.baseURI + "1.1/johndoe/" };
},
@ -294,11 +290,7 @@ add_task(async function test_loop_avoidance_storage() {
let getTokenCount = 0;
let mockTSC = {
// TokenServerClient
async getTokenFromBrowserIDAssertion(uri, assertion) {
getTokenCount++;
return { endpoint: server.baseURI + "1.1/johndoe/" };
},
async getTokenFromOAuthToken() {
async getTokenUsingOAuth() {
getTokenCount++;
return { endpoint: server.baseURI + "1.1/johndoe/" };
},
@ -396,11 +388,7 @@ add_task(async function test_loop_avoidance_engine() {
let getTokenCount = 0;
let mockTSC = {
// TokenServerClient
getTokenFromBrowserIDAssertion(uri, assertion) {
getTokenCount++;
return { endpoint: server.baseURI + "1.1/johndoe/" };
},
async getTokenFromOAuthToken() {
async getTokenUsingOAuth() {
getTokenCount++;
return { endpoint: server.baseURI + "1.1/johndoe/" };
},

View File

@ -5,8 +5,8 @@ const { Observers } = ChromeUtils.import(
"resource://services-common/observers.js"
);
const { Resource } = ChromeUtils.import("resource://services-sync/resource.js");
const { BrowserIDManager } = ChromeUtils.import(
"resource://services-sync/browserid_identity.js"
const { SyncAuthManager } = ChromeUtils.import(
"resource://services-sync/sync_auth.js"
);
var logger;
@ -328,9 +328,9 @@ add_task(async function test_get_protected_fail() {
add_task(async function test_get_protected_success() {
_("GET a password protected resource");
let identityConfig = makeIdentityConfig();
let browseridManager = new BrowserIDManager();
configureFxAccountIdentity(browseridManager, identityConfig);
let auth = browseridManager.getResourceAuthenticator();
let syncAuthManager = new SyncAuthManager();
configureFxAccountIdentity(syncAuthManager, identityConfig);
let auth = syncAuthManager.getResourceAuthenticator();
let res3 = new Resource(server.baseURI + "/protected");
res3.authenticator = auth;
Assert.equal(res3.authenticator, auth);

View File

@ -1,8 +1,8 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const { AuthenticationError, BrowserIDManager } = ChromeUtils.import(
"resource://services-sync/browserid_identity.js"
const { AuthenticationError, SyncAuthManager } = ChromeUtils.import(
"resource://services-sync/sync_auth.js"
);
const { Resource } = ChromeUtils.import("resource://services-sync/resource.js");
const { initializeIdentityWithTokenServerResponse } = ChromeUtils.import(
@ -18,7 +18,6 @@ const { FxAccountsClient } = ChromeUtils.import(
"resource://gre/modules/FxAccountsClient.jsm"
);
const {
CERT_LIFETIME,
ERRNO_INVALID_AUTH_TOKEN,
ONLOGIN_NOTIFICATION,
ONVERIFIED_NOTIFICATION,
@ -45,12 +44,12 @@ const MOCK_ACCESS_TOKEN =
"e3c5caf17f27a0d9e351926a928938b3737df43e91d4992a5a5fca9a7bdef8ba";
var globalIdentityConfig = makeIdentityConfig();
var globalBrowseridManager = new BrowserIDManager();
configureFxAccountIdentity(globalBrowseridManager, globalIdentityConfig);
var globalSyncAuthManager = new SyncAuthManager();
configureFxAccountIdentity(globalSyncAuthManager, globalIdentityConfig);
/**
* Mock client clock and skew vs server in FxAccounts signed-in user module and
* API client. browserid_identity.js queries these values to construct HAWK
* API client. sync_auth.js queries these values to construct HAWK
* headers. We will use this to test clock skew compensation in these headers
* below.
*/
@ -76,46 +75,27 @@ MockFxAccountsClient.prototype = {
add_test(function test_initial_state() {
_("Verify initial state");
Assert.ok(!globalBrowseridManager._token);
Assert.ok(!globalBrowseridManager._hasValidToken());
Assert.ok(!globalSyncAuthManager._token);
Assert.ok(!globalSyncAuthManager._hasValidToken());
run_next_test();
});
add_task(async function test_initialialize() {
_("Verify start after fetching token");
await globalBrowseridManager._ensureValidToken();
Assert.ok(!!globalBrowseridManager._token);
Assert.ok(globalBrowseridManager._hasValidToken());
});
add_task(async function test_initialialize_via_oauth_token() {
_("Verify start after fetching token using the oauth token flow");
Services.prefs.setBoolPref("identity.sync.useOAuthForSyncToken", true);
let browseridManager = new BrowserIDManager();
let identityConfig = makeIdentityConfig();
let fxaInternal = makeFxAccountsInternalMock(identityConfig);
configureFxAccountIdentity(browseridManager, identityConfig, fxaInternal);
browseridManager._fxaService._internal.initialize();
browseridManager._fxaService.getOAuthToken = () =>
Promise.resolve(MOCK_ACCESS_TOKEN);
await browseridManager._ensureValidToken();
Assert.ok(!!browseridManager._token);
Assert.ok(browseridManager._hasValidToken());
Services.prefs.setBoolPref("identity.sync.useOAuthForSyncToken", false);
await globalSyncAuthManager._ensureValidToken();
Assert.ok(!!globalSyncAuthManager._token);
Assert.ok(globalSyncAuthManager._hasValidToken());
});
add_task(async function test_refreshOAuthTokenOn401() {
_("Refreshes the FXA OAuth token after a 401.");
Services.prefs.setBoolPref("identity.sync.useOAuthForSyncToken", true);
let getTokenCount = 0;
let browseridManager = new BrowserIDManager();
let syncAuthManager = new SyncAuthManager();
let identityConfig = makeIdentityConfig();
let fxaInternal = makeFxAccountsInternalMock(identityConfig);
configureFxAccountIdentity(browseridManager, identityConfig, fxaInternal);
browseridManager._fxaService._internal.initialize();
browseridManager._fxaService.getOAuthToken = () => {
configureFxAccountIdentity(syncAuthManager, identityConfig, fxaInternal);
syncAuthManager._fxaService._internal.initialize();
syncAuthManager._fxaService.getOAuthToken = () => {
++getTokenCount;
return Promise.resolve(MOCK_ACCESS_TOKEN);
};
@ -145,33 +125,32 @@ add_task(async function test_refreshOAuthTokenOn401() {
};
});
browseridManager._tokenServerClient = mockTSC;
syncAuthManager._tokenServerClient = mockTSC;
await browseridManager._ensureValidToken();
await syncAuthManager._ensureValidToken();
Assert.equal(getTokenCount, 2);
Assert.ok(didReturn401);
Assert.ok(didReturn200);
Assert.ok(browseridManager._token);
Assert.ok(browseridManager._hasValidToken());
Services.prefs.setBoolPref("identity.sync.useOAuthForSyncToken", false);
Assert.ok(syncAuthManager._token);
Assert.ok(syncAuthManager._hasValidToken());
});
add_task(async function test_initialializeWithAuthErrorAndDeletedAccount() {
_("Verify sync state with auth error + account deleted");
var identityConfig = makeIdentityConfig();
var browseridManager = new BrowserIDManager();
var syncAuthManager = new SyncAuthManager();
// Use the real `_getAssertion` method that calls
// `mockFxAClient.signCertificate`.
// Use the real `getOAuthToken` method that calls
// `mockFxAClient.accessTokenWithSessionToken`.
let fxaInternal = makeFxAccountsInternalMock(identityConfig);
delete fxaInternal._getAssertion;
delete fxaInternal.getOAuthToken;
configureFxAccountIdentity(browseridManager, identityConfig, fxaInternal);
browseridManager._fxaService._internal.initialize();
configureFxAccountIdentity(syncAuthManager, identityConfig, fxaInternal);
syncAuthManager._fxaService._internal.initialize();
let signCertificateCalled = false;
let accessTokenWithSessionTokenCalled = false;
let accountStatusCalled = false;
let sessionStatusCalled = false;
@ -180,8 +159,8 @@ add_task(async function test_initialializeWithAuthErrorAndDeletedAccount() {
};
AuthErrorMockFxAClient.prototype = {
__proto__: FxAccountsClient.prototype,
signCertificate() {
signCertificateCalled = true;
accessTokenWithSessionToken() {
accessTokenWithSessionTokenCalled = true;
return Promise.reject({
code: 401,
errno: ERRNO_INVALID_AUTH_TOKEN,
@ -198,27 +177,27 @@ add_task(async function test_initialializeWithAuthErrorAndDeletedAccount() {
};
let mockFxAClient = new AuthErrorMockFxAClient();
browseridManager._fxaService._internal._fxAccountsClient = mockFxAClient;
syncAuthManager._fxaService._internal._fxAccountsClient = mockFxAClient;
await Assert.rejects(
browseridManager._ensureValidToken(),
syncAuthManager._ensureValidToken(),
AuthenticationError,
"should reject due to an auth error"
);
Assert.ok(signCertificateCalled);
Assert.ok(accessTokenWithSessionTokenCalled);
Assert.ok(sessionStatusCalled);
Assert.ok(accountStatusCalled);
Assert.ok(!browseridManager._token);
Assert.ok(!browseridManager._hasValidToken());
Assert.ok(!syncAuthManager._token);
Assert.ok(!syncAuthManager._hasValidToken());
});
add_task(async function test_getResourceAuthenticator() {
_(
"BrowserIDManager supplies a Resource Authenticator callback which returns a Hawk header."
"SyncAuthManager supplies a Resource Authenticator callback which returns a Hawk header."
);
configureFxAccountIdentity(globalBrowseridManager);
let authenticator = globalBrowseridManager.getResourceAuthenticator();
configureFxAccountIdentity(globalSyncAuthManager);
let authenticator = globalSyncAuthManager.getResourceAuthenticator();
Assert.ok(!!authenticator);
let req = {
uri: CommonUtils.makeURI("https://example.net/somewhere/over/the/rainbow"),
@ -230,14 +209,14 @@ add_task(async function test_getResourceAuthenticator() {
Assert.ok(output.headers.authorization.startsWith("Hawk"));
_("Expected internal state after successful call.");
Assert.equal(
globalBrowseridManager._token.uid,
globalSyncAuthManager._token.uid,
globalIdentityConfig.fxaccount.token.uid
);
});
add_task(async function test_resourceAuthenticatorSkew() {
_(
"BrowserIDManager Resource Authenticator compensates for clock skew in Hawk header."
"SyncAuthManager Resource Authenticator compensates for clock skew in Hawk header."
);
// Clock is skewed 12 hours into the future
@ -245,7 +224,7 @@ add_task(async function test_resourceAuthenticatorSkew() {
// uses new Date() instead of our given date.
let now =
new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS;
let browseridManager = new BrowserIDManager();
let syncAuthManager = new SyncAuthManager();
let hawkClient = new HawkClient("https://example.net/v1", "/foo");
// mock fxa hawk client skew
@ -276,25 +255,25 @@ add_task(async function test_resourceAuthenticatorSkew() {
// Mocks within mocks...
configureFxAccountIdentity(
browseridManager,
syncAuthManager,
globalIdentityConfig,
fxaInternal
);
Assert.equal(browseridManager._fxaService._internal.now(), now);
Assert.equal(syncAuthManager._fxaService._internal.now(), now);
Assert.equal(
browseridManager._fxaService._internal.localtimeOffsetMsec,
syncAuthManager._fxaService._internal.localtimeOffsetMsec,
localtimeOffsetMsec
);
Assert.equal(browseridManager._fxaService._internal.now(), now);
Assert.equal(syncAuthManager._fxaService._internal.now(), now);
Assert.equal(
browseridManager._fxaService._internal.localtimeOffsetMsec,
syncAuthManager._fxaService._internal.localtimeOffsetMsec,
localtimeOffsetMsec
);
let request = new Resource("https://example.net/i/like/pie/");
let authenticator = browseridManager.getResourceAuthenticator();
let authenticator = syncAuthManager.getResourceAuthenticator();
let output = await authenticator(request, "GET");
dump("output" + JSON.stringify(output));
let authHeader = output.headers.authorization;
@ -308,13 +287,13 @@ add_task(async function test_resourceAuthenticatorSkew() {
add_task(async function test_RESTResourceAuthenticatorSkew() {
_(
"BrowserIDManager REST Resource Authenticator compensates for clock skew in Hawk header."
"SyncAuthManager REST Resource Authenticator compensates for clock skew in Hawk header."
);
// Clock is skewed 12 hours into the future from our arbitary date
let now =
new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS;
let browseridManager = new BrowserIDManager();
let syncAuthManager = new SyncAuthManager();
let hawkClient = new HawkClient("https://example.net/v1", "/foo");
// mock fxa hawk client skew
@ -334,15 +313,15 @@ add_task(async function test_RESTResourceAuthenticatorSkew() {
fxaInternal.fxAccountsClient = fxaClient;
configureFxAccountIdentity(
browseridManager,
syncAuthManager,
globalIdentityConfig,
fxaInternal
);
Assert.equal(browseridManager._fxaService._internal.now(), now);
Assert.equal(syncAuthManager._fxaService._internal.now(), now);
let request = new Resource("https://example.net/i/like/pie/");
let authenticator = browseridManager.getResourceAuthenticator();
let authenticator = syncAuthManager.getResourceAuthenticator();
let output = await authenticator(request, "GET");
dump("output" + JSON.stringify(output));
let authHeader = output.headers.authorization;
@ -355,25 +334,25 @@ add_task(async function test_RESTResourceAuthenticatorSkew() {
});
add_task(async function test_ensureLoggedIn() {
configureFxAccountIdentity(globalBrowseridManager);
await globalBrowseridManager._ensureValidToken();
configureFxAccountIdentity(globalSyncAuthManager);
await globalSyncAuthManager._ensureValidToken();
Assert.equal(Status.login, LOGIN_SUCCEEDED, "original initialize worked");
Assert.ok(globalBrowseridManager._token);
Assert.ok(globalSyncAuthManager._token);
// arrange for no logged in user.
let fxa = globalBrowseridManager._fxaService;
let fxa = globalSyncAuthManager._fxaService;
let signedInUser =
fxa._internal.currentAccountState.storageManager.accountData;
fxa._internal.currentAccountState.storageManager.accountData = null;
await Assert.rejects(
globalBrowseridManager._ensureValidToken(true),
globalSyncAuthManager._ensureValidToken(true),
/no user is logged in/,
"expecting rejection due to no user"
);
// Restore the logged in user to what it was.
fxa._internal.currentAccountState.storageManager.accountData = signedInUser;
Status.login = LOGIN_FAILED_LOGIN_REJECTED;
await globalBrowseridManager._ensureValidToken(true);
await globalSyncAuthManager._ensureValidToken(true);
Assert.equal(Status.login, LOGIN_SUCCEEDED, "final ensureLoggedIn worked");
});
@ -383,18 +362,18 @@ add_task(async function test_syncState() {
let fxaInternal = makeFxAccountsInternalMock(identityConfig);
fxaInternal.startVerifiedCheck = () => {};
configureFxAccountIdentity(
globalBrowseridManager,
globalSyncAuthManager,
globalIdentityConfig,
fxaInternal
);
// arrange for no logged in user.
let fxa = globalBrowseridManager._fxaService;
let fxa = globalSyncAuthManager._fxaService;
let signedInUser =
fxa._internal.currentAccountState.storageManager.accountData;
fxa._internal.currentAccountState.storageManager.accountData = null;
await Assert.rejects(
globalBrowseridManager._ensureValidToken(true),
globalSyncAuthManager._ensureValidToken(true),
/no user is logged in/,
"expecting rejection due to no user"
);
@ -403,8 +382,8 @@ add_task(async function test_syncState() {
signedInUser.verified = false;
fxa._internal.currentAccountState.storageManager.accountData = signedInUser;
Status.login = LOGIN_FAILED_LOGIN_REJECTED;
// The browserid_identity observers are async, so call them directly.
await globalBrowseridManager.observe(null, ONLOGIN_NOTIFICATION, "");
// The sync_auth observers are async, so call them directly.
await globalSyncAuthManager.observe(null, ONLOGIN_NOTIFICATION, "");
Assert.equal(
Status.login,
LOGIN_FAILED_LOGIN_REJECTED,
@ -413,7 +392,7 @@ add_task(async function test_syncState() {
// now pretend the user because verified.
signedInUser.verified = true;
await globalBrowseridManager.observe(null, ONVERIFIED_NOTIFICATION, "");
await globalSyncAuthManager.observe(null, ONVERIFIED_NOTIFICATION, "");
Assert.equal(
Status.login,
LOGIN_SUCCEEDED,
@ -422,8 +401,8 @@ add_task(async function test_syncState() {
});
add_task(async function test_tokenExpiration() {
_("BrowserIDManager notices token expiration:");
let bimExp = new BrowserIDManager();
_("SyncAuthManager notices token expiration:");
let bimExp = new SyncAuthManager();
configureFxAccountIdentity(bimExp, globalIdentityConfig);
let authenticator = bimExp.getResourceAuthenticator();
@ -443,12 +422,12 @@ add_task(async function test_tokenExpiration() {
writable: true,
});
Assert.ok(bimExp._token.expiration < bimExp._now());
_("... means BrowserIDManager knows to re-fetch it on the next call.");
_("... means SyncAuthManager knows to re-fetch it on the next call.");
Assert.ok(!bimExp._hasValidToken());
});
add_task(async function test_getTokenErrors() {
_("BrowserIDManager correctly handles various failures to get a token.");
_("SyncAuthManager correctly handles various failures to get a token.");
_("Arrange for a 401 - Sync should reflect an auth error.");
initializeIdentityWithTokenServerResponse({
@ -456,10 +435,10 @@ add_task(async function test_getTokenErrors() {
headers: { "content-type": "application/json" },
body: JSON.stringify({}),
});
let browseridManager = Service.identity;
let syncAuthManager = Service.identity;
await Assert.rejects(
browseridManager._ensureValidToken(),
syncAuthManager._ensureValidToken(),
AuthenticationError,
"should reject due to 401"
);
@ -477,9 +456,9 @@ add_task(async function test_getTokenErrors() {
headers: [],
body: "",
});
browseridManager = Service.identity;
syncAuthManager = Service.identity;
await Assert.rejects(
browseridManager._ensureValidToken(),
syncAuthManager._ensureValidToken(),
TokenServerClientServerError,
"should reject due to non-JSON response"
);
@ -490,36 +469,37 @@ add_task(async function test_getTokenErrors() {
);
});
add_task(async function test_refreshCertificateOn401() {
_("BrowserIDManager refreshes the FXA certificate after a 401.");
add_task(async function test_refreshAccessTokenOn401() {
_("SyncAuthManager refreshes the FXA OAuth access token after a 401.");
var identityConfig = makeIdentityConfig();
var browseridManager = new BrowserIDManager();
// Use the real `_getAssertion` method that calls
// `mockFxAClient.signCertificate`.
var syncAuthManager = new SyncAuthManager();
// Use the real `getOAuthToken` method that calls
// `mockFxAClient.accessTokenWithSessionToken`.
let fxaInternal = makeFxAccountsInternalMock(identityConfig);
delete fxaInternal._getAssertion;
configureFxAccountIdentity(browseridManager, identityConfig, fxaInternal);
browseridManager._fxaService._internal.initialize();
delete fxaInternal.getOAuthToken;
configureFxAccountIdentity(syncAuthManager, identityConfig, fxaInternal);
syncAuthManager._fxaService._internal.initialize();
let getCertCount = 0;
let getTokenCount = 0;
let CheckSignMockFxAClient = function() {
FxAccountsClient.apply(this);
};
CheckSignMockFxAClient.prototype = {
__proto__: FxAccountsClient.prototype,
signCertificate() {
++getCertCount;
accessTokenWithSessionToken() {
++getTokenCount;
return Promise.resolve({ access_token: "token" });
},
};
let mockFxAClient = new CheckSignMockFxAClient();
browseridManager._fxaService._internal._fxAccountsClient = mockFxAClient;
syncAuthManager._fxaService._internal._fxAccountsClient = mockFxAClient;
let didReturn401 = false;
let didReturn200 = false;
let mockTSC = mockTokenServer(() => {
if (getCertCount <= 1) {
if (getTokenCount <= 1) {
didReturn401 = true;
return {
status: 401,
@ -541,15 +521,15 @@ add_task(async function test_refreshCertificateOn401() {
};
});
browseridManager._tokenServerClient = mockTSC;
syncAuthManager._tokenServerClient = mockTSC;
await browseridManager._ensureValidToken();
await syncAuthManager._ensureValidToken();
Assert.equal(getCertCount, 2);
Assert.equal(getTokenCount, 2);
Assert.ok(didReturn401);
Assert.ok(didReturn200);
Assert.ok(browseridManager._token);
Assert.ok(browseridManager._hasValidToken());
Assert.ok(syncAuthManager._token);
Assert.ok(syncAuthManager._hasValidToken());
});
add_task(async function test_getTokenErrorWithRetry() {
@ -564,10 +544,10 @@ add_task(async function test_getTokenErrorWithRetry() {
headers: { "content-type": "application/json", "retry-after": "100" },
body: JSON.stringify({}),
});
let browseridManager = Service.identity;
let syncAuthManager = Service.identity;
await Assert.rejects(
browseridManager._ensureValidToken(),
syncAuthManager._ensureValidToken(),
TokenServerClientServerError,
"should reject due to 503"
);
@ -584,10 +564,10 @@ add_task(async function test_getTokenErrorWithRetry() {
headers: { "content-type": "application/json", "x-backoff": "200" },
body: JSON.stringify({}),
});
browseridManager = Service.identity;
syncAuthManager = Service.identity;
await Assert.rejects(
browseridManager._ensureValidToken(),
syncAuthManager._ensureValidToken(),
TokenServerClientServerError,
"should reject due to no token in response"
);
@ -628,9 +608,9 @@ add_task(async function test_getKeysErrorWithBackoff() {
};
});
let browseridManager = Service.identity;
let syncAuthManager = Service.identity;
await Assert.rejects(
browseridManager._ensureValidToken(),
syncAuthManager._ensureValidToken(),
TokenServerClientServerError,
"should reject due to 503"
);
@ -671,9 +651,9 @@ add_task(async function test_getKeysErrorWithRetry() {
};
});
let browseridManager = Service.identity;
let syncAuthManager = Service.identity;
await Assert.rejects(
browseridManager._ensureValidToken(),
syncAuthManager._ensureValidToken(),
TokenServerClientServerError,
"should reject due to 503"
);
@ -685,7 +665,7 @@ add_task(async function test_getKeysErrorWithRetry() {
});
add_task(async function test_getHAWKErrors() {
_("BrowserIDManager correctly handles various HAWK failures.");
_("SyncAuthManager correctly handles various HAWK failures.");
_("Arrange for a 401 - Sync should reflect an auth error.");
let config = makeIdentityConfig();
@ -694,10 +674,17 @@ add_task(async function test_getHAWKErrors() {
data,
uri
) {
Assert.equal(method, "post");
Assert.equal(uri, "http://mockedserver:9999/certificate/sign?service=sync");
if (uri == "http://mockedserver:9999/oauth/token") {
Assert.equal(method, "post");
return {
status: 401,
headers: { "content-type": "application/json" },
body: JSON.stringify({ code: 401, errno: 110, error: "invalid token" }),
};
}
// For any follow-up requests that check account status.
return {
status: 401,
status: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({}),
};
@ -717,7 +704,7 @@ add_task(async function test_getHAWKErrors() {
uri
) {
Assert.equal(method, "post");
Assert.equal(uri, "http://mockedserver:9999/certificate/sign?service=sync");
Assert.equal(uri, "http://mockedserver:9999/oauth/token");
return {
status: 200,
headers: [],
@ -732,7 +719,7 @@ add_task(async function test_getHAWKErrors() {
});
add_task(async function test_getGetKeysFailing401() {
_("BrowserIDManager correctly handles 401 responses fetching keys.");
_("SyncAuthManager correctly handles 401 responses fetching keys.");
_("Arrange for a 401 - Sync should reflect an auth error.");
let config = makeIdentityConfig();
@ -760,7 +747,7 @@ add_task(async function test_getGetKeysFailing401() {
});
add_task(async function test_getGetKeysFailing503() {
_("BrowserIDManager correctly handles 5XX responses fetching keys.");
_("SyncAuthManager correctly handles 5XX responses fetching keys.");
_("Arrange for a 503 - Sync should reflect a network error.");
let config = makeIdentityConfig();
@ -793,10 +780,10 @@ add_task(async function test_getGetKeysFailing503() {
add_task(async function test_getKeysMissing() {
_(
"BrowserIDManager correctly handles getKeyForScope succeeding but not returning the key."
"SyncAuthManager correctly handles getKeyForScope succeeding but not returning the key."
);
let browseridManager = new BrowserIDManager();
let syncAuthManager = new SyncAuthManager();
let identityConfig = makeIdentityConfig();
// our mock identity config already has kSync, kXCS, kExtSync and kExtKbHash - remove them or we never
// try and fetch them.
@ -807,7 +794,7 @@ add_task(async function test_getKeysMissing() {
delete identityConfig.fxaccount.user.kExtKbHash;
identityConfig.fxaccount.user.keyFetchToken = "keyFetchToken";
configureFxAccountIdentity(browseridManager, identityConfig);
configureFxAccountIdentity(syncAuthManager, identityConfig);
// Mock a fxAccounts object
let fxa = new FxAccounts({
@ -830,20 +817,20 @@ add_task(async function test_getKeysMissing() {
},
});
browseridManager._fxaService = fxa;
syncAuthManager._fxaService = fxa;
await Assert.rejects(
browseridManager._ensureValidToken(),
syncAuthManager._ensureValidToken(),
/browser does not have the sync key, cannot sync/
);
});
add_task(async function test_getKeysUnexpecedError() {
_(
"BrowserIDManager correctly handles getKeyForScope throwing an unexpected error."
"SyncAuthManager correctly handles getKeyForScope throwing an unexpected error."
);
let browseridManager = new BrowserIDManager();
let syncAuthManager = new SyncAuthManager();
let identityConfig = makeIdentityConfig();
// our mock identity config already has kSync, kXCS, kExtSync and kExtKbHash - remove them or we never
// try and fetch them.
@ -854,7 +841,7 @@ add_task(async function test_getKeysUnexpecedError() {
delete identityConfig.fxaccount.user.kExtKbHash;
identityConfig.fxaccount.user.keyFetchToken = "keyFetchToken";
configureFxAccountIdentity(browseridManager, identityConfig);
configureFxAccountIdentity(syncAuthManager, identityConfig);
// Mock a fxAccounts object
let fxa = new FxAccounts({
@ -877,20 +864,20 @@ add_task(async function test_getKeysUnexpecedError() {
},
});
browseridManager._fxaService = fxa;
syncAuthManager._fxaService = fxa;
await Assert.rejects(
browseridManager._ensureValidToken(),
syncAuthManager._ensureValidToken(),
/well that was unexpected/
);
});
add_task(async function test_signedInUserMissing() {
_(
"BrowserIDManager detects getSignedInUser returning incomplete account data"
"SyncAuthManager detects getSignedInUser returning incomplete account data"
);
let browseridManager = new BrowserIDManager();
let syncAuthManager = new SyncAuthManager();
// Delete stored keys and the key fetch token.
delete globalIdentityConfig.fxaccount.user.scopedKeys;
delete globalIdentityConfig.fxaccount.user.kSync;
@ -899,7 +886,7 @@ add_task(async function test_signedInUserMissing() {
delete globalIdentityConfig.fxaccount.user.kExtKbHash;
delete globalIdentityConfig.fxaccount.user.keyFetchToken;
configureFxAccountIdentity(browseridManager, globalIdentityConfig);
configureFxAccountIdentity(syncAuthManager, globalIdentityConfig);
let fxa = new FxAccounts({
fetchAndUnwrapKeys() {
@ -918,16 +905,16 @@ add_task(async function test_signedInUserMissing() {
},
});
browseridManager._fxaService = fxa;
syncAuthManager._fxaService = fxa;
let status = await browseridManager.unlockAndVerifyAuthState();
let status = await syncAuthManager.unlockAndVerifyAuthState();
Assert.equal(status, LOGIN_FAILED_LOGIN_REJECTED);
});
// End of tests
// Utility functions follow
// Create a new browserid_identity object and initialize it with a
// Create a new sync_auth object and initialize it with a
// hawk mock that simulates HTTP responses.
// The callback function will be called each time the mocked hawk server wants
// to make a request. The result of the callback should be the mock response
@ -957,7 +944,7 @@ async function initializeIdentityWithHAWKResponseFactory(
return this.response;
},
async get() {
// Skip /status requests (browserid_identity checks if the account still
// Skip /status requests (sync_auth checks if the account still
// exists after an auth error)
if (this._uri.startsWith("http://mockedserver:9999/account/status")) {
this.response = {
@ -1010,9 +997,9 @@ async function initializeIdentityWithHAWKResponseFactory(
};
let fxa = new FxAccounts(internal);
globalBrowseridManager._fxaService = fxa;
globalSyncAuthManager._fxaService = fxa;
await Assert.rejects(
globalBrowseridManager._ensureValidToken(true),
globalSyncAuthManager._ensureValidToken(true),
// TODO: Ideally this should have a specific check for an error.
() => true,
"expecting rejection due to hawk error"
@ -1050,7 +1037,7 @@ function mockTokenServer(func) {
MockTSC.prototype.newRESTRequest = function(url) {
return new MockRESTRequest(url);
};
// Arrange for the same observerPrefix as browserid_identity uses.
// Arrange for the same observerPrefix as sync_auth uses.
MockTSC.prototype.observerPrefix = "weave:service";
return new MockTSC();
}

View File

@ -4,8 +4,8 @@
const { FxAccounts } = ChromeUtils.import(
"resource://gre/modules/FxAccounts.jsm"
);
const { BrowserIDManager } = ChromeUtils.import(
"resource://services-sync/browserid_identity.js"
const { SyncAuthManager } = ChromeUtils.import(
"resource://services-sync/sync_auth.js"
);
const { SyncScheduler } = ChromeUtils.import(
"resource://services-sync/policies.js"
@ -666,7 +666,7 @@ add_task(async function test_no_autoconnect_during_wizard() {
add_task(async function test_no_autoconnect_status_not_ok() {
let server = await sync_httpd_setup();
Status.__authManager = Service.identity = new BrowserIDManager();
Status.__authManager = Service.identity = new SyncAuthManager();
// Ensure we don't actually try to sync (or log in for that matter).
function onLoginStart() {

View File

@ -43,12 +43,12 @@ tags = addons
[test_resource_ua.js]
# Generic Sync types.
[test_browserid_identity.js]
[test_collection_getBatched.js]
[test_collections_recovery.js]
[test_keys.js]
[test_records_crypto.js]
[test_records_wbo.js]
[test_sync_auth_manager.js]
# Engine APIs.
[test_engine.js]

View File

@ -25,7 +25,7 @@
"bookmark_validator.js": ["BookmarkValidator", "BookmarkProblemData"],
"browser-loader.js": ["BrowserLoader"],
"browser.js": ["browser", "Context", "WindowState"],
"browserid_identity.js": ["BrowserIDManager", "AuthenticationError"],
"sync_auth.js": ["SyncAuthManager", "AuthenticationError"],
"capabilities.js": ["Capabilities", "PageLoadStrategy", "Proxy", "Timeouts", "UnhandledPromptBehavior"],
"cert.js": ["allowAllCerts"],
"clients.js": ["ClientEngine", "ClientsRec"],
@ -79,7 +79,7 @@
"fxaccounts.jsm": ["Authentication"],
"FxAccounts.jsm": ["fxAccounts", "FxAccounts"],
"FxAccountsCommands.js": ["SendTab", "FxAccountsCommands"],
"FxAccountsCommon.js": ["log", "logPII", "FXACCOUNTS_PERMISSION", "DATA_FORMAT_VERSION", "DEFAULT_STORAGE_FILENAME", "ASSERTION_LIFETIME", "ASSERTION_USE_PERIOD", "CERT_LIFETIME", "KEY_LIFETIME", "POLL_SESSION", "ONLOGIN_NOTIFICATION", "ONVERIFIED_NOTIFICATION", "ONLOGOUT_NOTIFICATION", "ON_COMMAND_RECEIVED_NOTIFICATION", "ON_DEVICE_CONNECTED_NOTIFICATION", "ON_DEVICE_DISCONNECTED_NOTIFICATION", "ON_PROFILE_UPDATED_NOTIFICATION", "ON_PASSWORD_CHANGED_NOTIFICATION", "ON_PASSWORD_RESET_NOTIFICATION", "ON_VERIFY_LOGIN_NOTIFICATION", "ON_ACCOUNT_DESTROYED_NOTIFICATION", "ON_COLLECTION_CHANGED_NOTIFICATION", "FXA_PUSH_SCOPE_ACCOUNT_UPDATE", "ON_PROFILE_CHANGE_NOTIFICATION", "ON_ACCOUNT_STATE_CHANGE_NOTIFICATION", "ON_NEW_DEVICE_ID", "COMMAND_SENDTAB", "SCOPE_PROFILE", "SCOPE_OLD_SYNC", "UI_REQUEST_SIGN_IN_FLOW", "UI_REQUEST_REFRESH_AUTH", "FX_OAUTH_CLIENT_ID", "WEBCHANNEL_ID", "COMMAND_PAIR_HEARTBEAT", "COMMAND_PAIR_SUPP_METADATA", "COMMAND_PAIR_AUTHORIZE", "COMMAND_PAIR_DECLINE", "COMMAND_PAIR_COMPLETE", "COMMAND_PAIR_PREFERENCES", "COMMAND_PROFILE_CHANGE", "COMMAND_CAN_LINK_ACCOUNT", "COMMAND_LOGIN", "COMMAND_LOGOUT", "COMMAND_DELETE", "COMMAND_SYNC_PREFERENCES", "COMMAND_CHANGE_PASSWORD", "COMMAND_FXA_STATUS", "PREF_LAST_FXA_USER", "PREF_REMOTE_PAIRING_URI", "ERRNO_ACCOUNT_ALREADY_EXISTS", "ERRNO_ACCOUNT_DOES_NOT_EXIST", "ERRNO_INCORRECT_PASSWORD", "ERRNO_UNVERIFIED_ACCOUNT", "ERRNO_INVALID_VERIFICATION_CODE", "ERRNO_NOT_VALID_JSON_BODY", "ERRNO_INVALID_BODY_PARAMETERS", "ERRNO_MISSING_BODY_PARAMETERS", "ERRNO_INVALID_REQUEST_SIGNATURE", "ERRNO_INVALID_AUTH_TOKEN", "ERRNO_INVALID_AUTH_TIMESTAMP", "ERRNO_MISSING_CONTENT_LENGTH", "ERRNO_REQUEST_BODY_TOO_LARGE", "ERRNO_TOO_MANY_CLIENT_REQUESTS", "ERRNO_INVALID_AUTH_NONCE", "ERRNO_ENDPOINT_NO_LONGER_SUPPORTED", "ERRNO_INCORRECT_LOGIN_METHOD", "ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD", "ERRNO_INCORRECT_API_VERSION", "ERRNO_INCORRECT_EMAIL_CASE", "ERRNO_ACCOUNT_LOCKED", "ERRNO_ACCOUNT_UNLOCKED", "ERRNO_UNKNOWN_DEVICE", "ERRNO_DEVICE_SESSION_CONFLICT", "ERRNO_SERVICE_TEMP_UNAVAILABLE", "ERRNO_PARSE", "ERRNO_NETWORK", "ERRNO_UNKNOWN_ERROR", "OAUTH_SERVER_ERRNO_OFFSET", "ERRNO_UNKNOWN_CLIENT_ID", "ERRNO_INCORRECT_CLIENT_SECRET", "ERRNO_INCORRECT_REDIRECT_URI", "ERRNO_INVALID_FXA_ASSERTION", "ERRNO_UNKNOWN_CODE", "ERRNO_INCORRECT_CODE", "ERRNO_EXPIRED_CODE", "ERRNO_OAUTH_INVALID_TOKEN", "ERRNO_INVALID_REQUEST_PARAM", "ERRNO_INVALID_RESPONSE_TYPE", "ERRNO_UNAUTHORIZED", "ERRNO_FORBIDDEN", "ERRNO_INVALID_CONTENT_TYPE", "ERROR_ACCOUNT_ALREADY_EXISTS", "ERROR_ACCOUNT_DOES_NOT_EXIST", "ERROR_ACCOUNT_LOCKED", "ERROR_ACCOUNT_UNLOCKED", "ERROR_ALREADY_SIGNED_IN_USER", "ERROR_DEVICE_SESSION_CONFLICT", "ERROR_ENDPOINT_NO_LONGER_SUPPORTED", "ERROR_INCORRECT_API_VERSION", "ERROR_INCORRECT_EMAIL_CASE", "ERROR_INCORRECT_KEY_RETRIEVAL_METHOD", "ERROR_INCORRECT_LOGIN_METHOD", "ERROR_INVALID_EMAIL", "ERROR_INVALID_AUDIENCE", "ERROR_INVALID_AUTH_TOKEN", "ERROR_INVALID_AUTH_TIMESTAMP", "ERROR_INVALID_AUTH_NONCE", "ERROR_INVALID_BODY_PARAMETERS", "ERROR_INVALID_PASSWORD", "ERROR_INVALID_VERIFICATION_CODE", "ERROR_INVALID_REFRESH_AUTH_VALUE", "ERROR_INVALID_REQUEST_SIGNATURE", "ERROR_INTERNAL_INVALID_USER", "ERROR_MISSING_BODY_PARAMETERS", "ERROR_MISSING_CONTENT_LENGTH", "ERROR_NO_TOKEN_SESSION", "ERROR_NO_SILENT_REFRESH_AUTH", "ERROR_NOT_VALID_JSON_BODY", "ERROR_OFFLINE", "ERROR_PERMISSION_DENIED", "ERROR_REQUEST_BODY_TOO_LARGE", "ERROR_SERVER_ERROR", "ERROR_SYNC_DISABLED", "ERROR_TOO_MANY_CLIENT_REQUESTS", "ERROR_SERVICE_TEMP_UNAVAILABLE", "ERROR_UI_ERROR", "ERROR_UI_REQUEST", "ERROR_PARSE", "ERROR_NETWORK", "ERROR_UNKNOWN", "ERROR_UNKNOWN_DEVICE", "ERROR_UNVERIFIED_ACCOUNT", "ERROR_UNKNOWN_CLIENT_ID", "ERROR_INCORRECT_CLIENT_SECRET", "ERROR_INCORRECT_REDIRECT_URI", "ERROR_INVALID_FXA_ASSERTION", "ERROR_UNKNOWN_CODE", "ERROR_INCORRECT_CODE", "ERROR_EXPIRED_CODE", "ERROR_OAUTH_INVALID_TOKEN", "ERROR_INVALID_REQUEST_PARAM", "ERROR_INVALID_RESPONSE_TYPE", "ERROR_UNAUTHORIZED", "ERROR_FORBIDDEN", "ERROR_INVALID_CONTENT_TYPE", "ERROR_NO_ACCOUNT", "ERROR_AUTH_ERROR", "ERROR_INVALID_PARAMETER", "ERROR_CODE_METHOD_NOT_ALLOWED", "ERROR_MSG_METHOD_NOT_ALLOWED", "DERIVED_KEYS_NAMES", "FXA_PWDMGR_PLAINTEXT_FIELDS", "FXA_PWDMGR_SECURE_FIELDS", "FXA_PWDMGR_MEMORY_FIELDS", "FXA_PWDMGR_REAUTH_WHITELIST", "FXA_PWDMGR_HOST", "FXA_PWDMGR_REALM", "SERVER_ERRNO_TO_ERROR", "ERROR_TO_GENERAL_ERROR_CLASS"],
"FxAccountsCommon.js": ["log", "logPII", "FXACCOUNTS_PERMISSION", "DATA_FORMAT_VERSION", "DEFAULT_STORAGE_FILENAME", "ASSERTION_LIFETIME", "ASSERTION_USE_PERIOD", "CERT_LIFETIME", "KEY_LIFETIME", "POLL_SESSION", "ONLOGIN_NOTIFICATION", "ONVERIFIED_NOTIFICATION", "ONLOGOUT_NOTIFICATION", "ON_COMMAND_RECEIVED_NOTIFICATION", "ON_DEVICE_CONNECTED_NOTIFICATION", "ON_DEVICE_DISCONNECTED_NOTIFICATION", "ON_PROFILE_UPDATED_NOTIFICATION", "ON_PASSWORD_CHANGED_NOTIFICATION", "ON_PASSWORD_RESET_NOTIFICATION", "ON_VERIFY_LOGIN_NOTIFICATION", "ON_ACCOUNT_DESTROYED_NOTIFICATION", "ON_COLLECTION_CHANGED_NOTIFICATION", "FXA_PUSH_SCOPE_ACCOUNT_UPDATE", "ON_PROFILE_CHANGE_NOTIFICATION", "ON_ACCOUNT_STATE_CHANGE_NOTIFICATION", "ON_NEW_DEVICE_ID", "COMMAND_SENDTAB", "SCOPE_PROFILE", "SCOPE_OLD_SYNC", "UI_REQUEST_SIGN_IN_FLOW", "UI_REQUEST_REFRESH_AUTH", "FX_OAUTH_CLIENT_ID", "WEBCHANNEL_ID", "COMMAND_PAIR_HEARTBEAT", "COMMAND_PAIR_SUPP_METADATA", "COMMAND_PAIR_AUTHORIZE", "COMMAND_PAIR_DECLINE", "COMMAND_PAIR_COMPLETE", "COMMAND_PAIR_PREFERENCES", "COMMAND_PROFILE_CHANGE", "COMMAND_CAN_LINK_ACCOUNT", "COMMAND_LOGIN", "COMMAND_LOGOUT", "COMMAND_DELETE", "COMMAND_SYNC_PREFERENCES", "COMMAND_CHANGE_PASSWORD", "COMMAND_FXA_STATUS", "PREF_LAST_FXA_USER", "PREF_REMOTE_PAIRING_URI", "ERRNO_ACCOUNT_ALREADY_EXISTS", "ERRNO_ACCOUNT_DOES_NOT_EXIST", "ERRNO_INCORRECT_PASSWORD", "ERRNO_UNVERIFIED_ACCOUNT", "ERRNO_INVALID_VERIFICATION_CODE", "ERRNO_NOT_VALID_JSON_BODY", "ERRNO_INVALID_BODY_PARAMETERS", "ERRNO_MISSING_BODY_PARAMETERS", "ERRNO_INVALID_REQUEST_SIGNATURE", "ERRNO_INVALID_AUTH_TOKEN", "ERRNO_INVALID_AUTH_TIMESTAMP", "ERRNO_MISSING_CONTENT_LENGTH", "ERRNO_REQUEST_BODY_TOO_LARGE", "ERRNO_TOO_MANY_CLIENT_REQUESTS", "ERRNO_INVALID_AUTH_NONCE", "ERRNO_ENDPOINT_NO_LONGER_SUPPORTED", "ERRNO_INCORRECT_LOGIN_METHOD", "ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD", "ERRNO_INCORRECT_API_VERSION", "ERRNO_INCORRECT_EMAIL_CASE", "ERRNO_ACCOUNT_LOCKED", "ERRNO_ACCOUNT_UNLOCKED", "ERRNO_UNKNOWN_DEVICE", "ERRNO_DEVICE_SESSION_CONFLICT", "ERRNO_SERVICE_TEMP_UNAVAILABLE", "ERRNO_PARSE", "ERRNO_NETWORK", "ERRNO_UNKNOWN_ERROR", "OAUTH_SERVER_ERRNO_OFFSET", "ERRNO_UNKNOWN_CLIENT_ID", "ERRNO_INCORRECT_CLIENT_SECRET", "ERRNO_INCORRECT_REDIRECT_URI", "ERRNO_INVALID_FXA_ASSERTION", "ERRNO_UNKNOWN_CODE", "ERRNO_INCORRECT_CODE", "ERRNO_EXPIRED_CODE", "ERRNO_OAUTH_INVALID_TOKEN", "ERRNO_INVALID_REQUEST_PARAM", "ERRNO_INVALID_RESPONSE_TYPE", "ERRNO_UNAUTHORIZED", "ERRNO_FORBIDDEN", "ERRNO_INVALID_CONTENT_TYPE", "ERROR_ACCOUNT_ALREADY_EXISTS", "ERROR_ACCOUNT_DOES_NOT_EXIST", "ERROR_ACCOUNT_LOCKED", "ERROR_ACCOUNT_UNLOCKED", "ERROR_ALREADY_SIGNED_IN_USER", "ERROR_DEVICE_SESSION_CONFLICT", "ERROR_ENDPOINT_NO_LONGER_SUPPORTED", "ERROR_INCORRECT_API_VERSION", "ERROR_INCORRECT_EMAIL_CASE", "ERROR_INCORRECT_KEY_RETRIEVAL_METHOD", "ERROR_INCORRECT_LOGIN_METHOD", "ERROR_INVALID_EMAIL", "ERROR_INVALID_AUDIENCE", "ERROR_INVALID_AUTH_TOKEN", "ERROR_INVALID_AUTH_TIMESTAMP", "ERROR_INVALID_AUTH_NONCE", "ERROR_INVALID_BODY_PARAMETERS", "ERROR_INVALID_PASSWORD", "ERROR_INVALID_VERIFICATION_CODE", "ERROR_INVALID_REFRESH_AUTH_VALUE", "ERROR_INVALID_REQUEST_SIGNATURE", "ERROR_INTERNAL_INVALID_USER", "ERROR_MISSING_BODY_PARAMETERS", "ERROR_MISSING_CONTENT_LENGTH", "ERROR_NO_TOKEN_SESSION", "ERROR_NO_SILENT_REFRESH_AUTH", "ERROR_NOT_VALID_JSON_BODY", "ERROR_OFFLINE", "ERROR_PERMISSION_DENIED", "ERROR_REQUEST_BODY_TOO_LARGE", "ERROR_SERVER_ERROR", "ERROR_SYNC_DISABLED", "ERROR_TOO_MANY_CLIENT_REQUESTS", "ERROR_SERVICE_TEMP_UNAVAILABLE", "ERROR_UI_ERROR", "ERROR_UI_REQUEST", "ERROR_PARSE", "ERROR_NETWORK", "ERROR_UNKNOWN", "ERROR_UNKNOWN_DEVICE", "ERROR_UNVERIFIED_ACCOUNT", "ERROR_UNKNOWN_CLIENT_ID", "ERROR_INCORRECT_CLIENT_SECRET", "ERROR_INCORRECT_REDIRECT_URI", "ERROR_INVALID_FXA_ASSERTION", "ERROR_UNKNOWN_CODE", "ERROR_INCORRECT_CODE", "ERROR_EXPIRED_CODE", "ERROR_OAUTH_INVALID_TOKEN", "ERROR_INVALID_REQUEST_PARAM", "ERROR_INVALID_RESPONSE_TYPE", "ERROR_UNAUTHORIZED", "ERROR_FORBIDDEN", "ERROR_INVALID_CONTENT_TYPE", "ERROR_NO_ACCOUNT", "ERROR_AUTH_ERROR", "ERROR_INVALID_PARAMETER", "ERROR_CODE_METHOD_NOT_ALLOWED", "ERROR_MSG_METHOD_NOT_ALLOWED", "DERIVED_KEYS_NAMES", "FXA_PWDMGR_PLAINTEXT_FIELDS", "FXA_PWDMGR_SECURE_FIELDS", "FXA_PWDMGR_REAUTH_WHITELIST", "FXA_PWDMGR_HOST", "FXA_PWDMGR_REALM", "SERVER_ERRNO_TO_ERROR", "ERROR_TO_GENERAL_ERROR_CLASS"],
"FxAccountsOAuthGrantClient.jsm": ["FxAccountsOAuthGrantClient", "FxAccountsOAuthGrantClientError"],
"FxAccountsProfileClient.jsm": ["FxAccountsProfileClient", "FxAccountsProfileClientError"],
"FxAccountsPush.js": ["FxAccountsPushService"],