mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-07 09:54:42 +00:00
Bug 1665420 - Remove legacy BrowserID crypto code. r=markh
Differential Revision: https://phabricator.services.mozilla.com/D107985
This commit is contained in:
parent
b7f213243a
commit
f6a46d059d
@ -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
|
||||
|
@ -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
|
||||
|
@ -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";
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
}
|
@ -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',
|
||||
},
|
||||
]
|
@ -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"
|
@ -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);
|
||||
};
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -7,8 +7,6 @@
|
||||
with Files("**"):
|
||||
BUG_COMPONENT = ("Firefox", "Sync")
|
||||
|
||||
DIRS += ["component"]
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += ["tests/unit/xpcshell.ini"]
|
||||
|
||||
EXTRA_JS_MODULES["services-crypto"] += [
|
||||
|
@ -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);
|
@ -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 };
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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([
|
||||
|
@ -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()) + ")";
|
||||
};
|
@ -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.
|
||||
|
@ -22,7 +22,6 @@ EXTRA_JS_MODULES += [
|
||||
"FxAccountsConfig.jsm",
|
||||
"FxAccountsDevice.jsm",
|
||||
"FxAccountsKeys.jsm",
|
||||
"FxAccountsOAuthGrantClient.jsm",
|
||||
"FxAccountsPairing.jsm",
|
||||
"FxAccountsPairingChannel.js",
|
||||
"FxAccountsProfile.jsm",
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
@ -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");
|
||||
}
|
||||
});
|
||||
});
|
@ -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",
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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]
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
},
|
||||
|
||||
|
@ -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
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
];
|
||||
|
||||
|
@ -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/" };
|
||||
},
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
@ -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() {
|
||||
|
@ -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]
|
||||
|
@ -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"],
|
||||
|
Loading…
Reference in New Issue
Block a user