This commit is contained in:
Shane Tomlinson 2018-09-24 13:15:17 +01:00 коммит произвёл vladikoff
Родитель 091f6070ff
Коммит 6625d04be3
36 изменённых файлов: 1170 добавлений и 134 удалений

Просмотреть файл

@ -149,6 +149,39 @@ jobs:
command: |
./retry -n 1 -- npm run test-circle -- --firefoxBinary=./firefox/firefox
test-pairing:
working_directory: ~/fxa
docker:
- image: circleci/node:8-stretch-browsers
steps:
- attach_workspace:
at: ~/
- run:
name: Run fxa-content-server
command: |
CONFIG_FILES=server/config/local.json,server/config/production.json,tests/ci/config_circleci.json node_modules/.bin/grunt serverproc:dist
background: true
- run:
name: Check server
command: |
sleep 10
curl http://127.0.0.1:3030
- run:
name: Run tests
shell: /bin/bash --login
# Download the Nightly pairing build
# From: https://treeherder.mozilla.org/#/jobs?repo=try&revision=7f10c7614e9fa46d6679a0ad4f0a1e02985e5425
command: |
wget https://s3-us-west-2.amazonaws.com/fxa-dev-bucket/fenix-pair/desktop/7f10c7614e9fa46-target.tar.bz2
sudo apt-get install -y python-setuptools python-dev build-essential
sudo easy_install pip
sudo pip install mozinstall
mozinstall 7f10c7614e9fa46-target.tar.bz2
./retry -n 1 -- npm run test-pairing-circle -- --firefoxBinary=./firefox/firefox
dockerpush:
docker:
- image: circleci/node:8-stretch-browsers
@ -216,6 +249,9 @@ workflows:
- test4:
requires:
- build
- test-pairing:
requires:
- build
- dockerpush:
filters:
tags:

Просмотреть файл

@ -28,6 +28,7 @@ define(function (require, exports, module) {
AuthErrors.is(error, 'MISSING_PARAMETER') ||
OAuthErrors.is(error, 'INCORRECT_REDIRECT') ||
OAuthErrors.is(error, 'INVALID_PARAMETER') ||
OAuthErrors.is(error, 'INVALID_PAIRING_CLIENT') ||
OAuthErrors.is(error, 'MISSING_PARAMETER') ||
OAuthErrors.is(error, 'UNKNOWN_CLIENT')) {
return FourHundredTemplate;

Просмотреть файл

@ -0,0 +1,7 @@
/* 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/. */
export default function importFxaPairingChannel() {
return import(/* webpackChunkName: "fxaPairingChannel" */ 'fxaPairingChannel');
}

Просмотреть файл

@ -130,7 +130,11 @@ define(function (require, exports, module) {
MISSING_PARAMETER: {
errno: 1005,
message: t('Missing OAuth parameter: %(param)s')
}
},
INVALID_PAIRING_CLIENT: {
errno: 1006,
message: 'Invalid pairing client'
},
};
/*eslint-enable sorting/sort-object-props*/

Просмотреть файл

@ -6,6 +6,7 @@ import PairingChannelClientErrors from './pairing-channel-client-errors';
import { Model } from 'backbone';
import { pick } from 'underscore';
import { base64urlToUint8Array } from './crypto/util';
import importFxaPairingChannel from './fxa-pairing-channel';
import Raven from 'raven';
import Vat from 'lib/vat';
@ -35,7 +36,7 @@ export default class PairingChannelClient extends Model {
super(attrs, options);
this.sentryMetrics = options.sentryMetrics || Raven;
this._importPairingChannel = options.importPairingChannel;
this._importPairingChannel = options.importPairingChannel || importFxaPairingChannel;
}
/**

Просмотреть файл

@ -7,6 +7,7 @@
import AuthorityStateMachine from '../../pairing/authority-state-machine';
import BaseAuthenticationBroker from '../base';
import OAuthErrors from '../../../lib/oauth-errors';
import setRemoteMetaData from './remote-metadata';
const PAIR_HEARTBEAT_INTERVAL = 1000;
@ -17,7 +18,12 @@ export default class AuthorityBroker extends BaseAuthenticationBroker {
initialize (options) {
super.initialize(options);
const { notifier } = options;
const { notifier, config } = options;
if (! config.pairingClients.includes(this.relier.get('clientId'))) {
// only approved clients may pair
throw OAuthErrors.toError('INVALID_PAIRING_CLIENT');
}
// The AuthorityStateMachine is responsible for driving the next steps of the pairing process.
// It transitions between various pairing views.

Просмотреть файл

@ -2,6 +2,7 @@
* 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/. */
import OAuthErrors from '../../../lib/oauth-errors';
import OAuthRedirectBroker from '../oauth-redirect';
import PairingChannelClient from '../../../lib/pairing-channel-client';
import setRemoteMetaData from './remote-metadata';
@ -15,6 +16,12 @@ export default class SupplicantBroker extends OAuthRedirectBroker {
super.initialize(options);
const { config, notifier, relier } = options;
if (! config.pairingClients.includes(relier.get('clientId'))) {
// only approved clients may pair
throw OAuthErrors.toError('INVALID_PAIRING_CLIENT');
}
const channelServerUri = config.pairingChannelServerUri;
const { channelId, channelKey } = relier.toJSON();
if (channelId && channelKey && channelServerUri) {

Просмотреть файл

@ -14,6 +14,7 @@ const CHANNEL_ID = 'channelId';
describe('models/auth_brokers/pairing/authority', function () {
let broker;
let config;
let relier;
let notifier;
let notificationChannel;
@ -49,13 +50,18 @@ describe('models/auth_brokers/pairing/authority', function () {
return Promise.resolve(response);
});
config = {
pairingClients: ['3c49430b43dfba77'],
};
relier = new Relier();
relier.set({
channelId: CHANNEL_ID
channelId: CHANNEL_ID,
clientId: '3c49430b43dfba77',
});
notifier = new Notifier();
broker = new AuthorityBroker({
config,
notificationChannel,
notifier,
relier: relier,
@ -69,6 +75,24 @@ describe('models/auth_brokers/pairing/authority', function () {
assert.ok(broker.stateMachine);
});
describe('initialize', () => {
it('validates the client id', () => {
relier.set({
clientId: 'c6d74070a481bc10',
});
assert.throws(() => {
broker = new AuthorityBroker({
config,
notificationChannel,
notifier,
relier: relier,
window: windowMock,
});
}, 'Invalid pairing client');
});
});
describe('fetch', () => {
it('gets metadata and starts heartbeat', () => {
return broker.fetch().then(() => {

Просмотреть файл

@ -16,12 +16,14 @@ describe('models/auth_brokers/pairing/remote-metadata', function () {
beforeEach(function () {
config = {
pairingChannelServerUri: 'ws://test'
pairingChannelServerUri: 'ws://test',
pairingClients: ['3c49430b43dfba77'],
};
relier = new Relier();
relier.set({
channelId: '1',
channelKey: 'dGVzdA==',
clientId: '3c49430b43dfba77',
redirectUri: 'https://example.com?code=1&state=2'
});
notifier = new Notifier();

Просмотреть файл

@ -18,12 +18,14 @@ describe('models/auth_brokers/pairing/supplicant', function () {
beforeEach(function () {
config = {
pairingChannelServerUri: 'ws://test'
pairingChannelServerUri: 'ws://test',
pairingClients: ['3c49430b43dfba77'],
};
relier = new Relier();
relier.set({
channelId: '1',
channelKey: 'dGVzdA==',
clientId: '3c49430b43dfba77',
redirectUri: 'https://example.com?code=1&state=2'
});
notifier = new Notifier();
@ -36,7 +38,6 @@ describe('models/auth_brokers/pairing/supplicant', function () {
});
});
describe('initialize', () => {
it('creates a pairing channel and a state machine', () => {
assert.ok(broker.pairingChannelClient);
@ -57,6 +58,22 @@ describe('models/auth_brokers/pairing/supplicant', function () {
});
}, 'Failed to initialize supplicant');
});
it('throws on bad clientId', () => {
relier.set({
clientId: 'c6d74070a481bc10',
});
assert.throws(() => {
broker = new SupplicantBroker({
config,
importPairingChannel: mockPairingChannel,
notifier,
relier,
});
}, 'Invalid pairing client');
});
});
describe('afterSupplicantApprove', () => {

Просмотреть файл

@ -30,7 +30,8 @@ describe('models/auth_brokers/pairing/supplicant-state-machine', function () {
let broker;
let mockChannelClient;
const config = {
pairingChannelServerUri: 'ws://test'
pairingChannelServerUri: 'ws://test',
pairingClients: ['3c49430b43dfba77'],
};
beforeEach(() => {
@ -38,6 +39,7 @@ describe('models/auth_brokers/pairing/supplicant-state-machine', function () {
relier.set({
channelId: '1',
channelKey: 'dGVzdA==',
clientId: '3c49430b43dfba77',
redirectUri: 'https://example.com?code=1&state=2',
state: 'state'
});

Просмотреть файл

@ -5,9 +5,10 @@
import $ from 'jquery';
import { assert } from 'chai';
import AuthorityBroker from 'models/auth_brokers/pairing/authority';
import Notifier from 'lib/channels/notifier';
import Relier from 'models/reliers/relier';
import Session from 'lib/session';
import sinon from 'sinon';
import Notifier from 'lib/channels/notifier';
import User from 'models/user';
import View from 'views/pair/auth_allow';
@ -24,6 +25,7 @@ const REMOTE_METADATA = {
describe('views/pair/auth_allow', () => {
let broker;
let config;
let relier;
let user;
let notifier;
@ -31,9 +33,17 @@ describe('views/pair/auth_allow', () => {
let windowMock;
beforeEach(() => {
config = {
pairingClients: ['3c49430b43dfba77'],
};
relier = new Relier();
relier.set({
clientId: '3c49430b43dfba77',
});
user = new User();
notifier = new Notifier();
broker = new AuthorityBroker({
config,
notifier,
relier,
session: Session,

Просмотреть файл

@ -6,6 +6,7 @@ import {assert} from 'chai';
import AuthorityBroker from 'models/auth_brokers/pairing/authority';
import Session from 'lib/session';
import Notifier from 'lib/channels/notifier';
import Relier from 'models/reliers/relier';
import sinon from 'sinon';
import User from 'models/user';
import View from 'views/pair/auth_complete';
@ -23,6 +24,7 @@ const REMOTE_METADATA = {
describe('views/pair/auth_complete', () => {
let broker;
let config;
let relier;
let user;
let notifier;
@ -30,9 +32,17 @@ describe('views/pair/auth_complete', () => {
let windowMock;
beforeEach(() => {
config = {
pairingClients: ['3c49430b43dfba77'],
};
relier = new Relier();
relier.set({
clientId: '3c49430b43dfba77',
});
user = new User();
notifier = new Notifier();
broker = new AuthorityBroker({
config,
notifier,
relier,
session: Session,

Просмотреть файл

@ -6,6 +6,7 @@ import {assert} from 'chai';
import AuthorityBroker from 'models/auth_brokers/pairing/authority';
import Session from 'lib/session';
import Notifier from 'lib/channels/notifier';
import Relier from 'models/reliers/relier';
import sinon from 'sinon';
import User from 'models/user';
import View from 'views/pair/auth_wait_for_supp';
@ -23,6 +24,7 @@ const REMOTE_METADATA = {
describe('views/pair/auth_wait_for_supp', () => {
let broker;
let config;
let relier;
let user;
let notifier;
@ -30,9 +32,17 @@ describe('views/pair/auth_wait_for_supp', () => {
let windowMock;
beforeEach(() => {
config = {
pairingClients: ['3c49430b43dfba77'],
};
relier = new Relier();
relier.set({
clientId: '3c49430b43dfba77',
});
user = new User();
notifier = new Notifier();
broker = new AuthorityBroker({
config,
notifier,
relier,
session: Session,

Просмотреть файл

@ -31,12 +31,14 @@ describe('views/pair/supp_allow', () => {
beforeEach(() => {
config = {
pairingChannelServerUri: 'ws://test'
pairingChannelServerUri: 'ws://test',
pairingClients: ['3c49430b43dfba77'],
};
relier = new Relier();
relier.set({
channelId: '1',
channelKey: 'dGVzdA==',
clientId: '3c49430b43dfba77',
redirectUri: 'https://example.com?code=1&state=2'
});
notifier = new Notifier();

Просмотреть файл

@ -23,7 +23,7 @@ const REMOTE_METADATA = {
ua: 'Firefox 1.0',
};
describe('views/pair/supp_wait_for_supp', () => {
describe('views/pair/supp_wait_for_auth', () => {
let broker;
let config;
let relier;
@ -34,12 +34,14 @@ describe('views/pair/supp_wait_for_supp', () => {
beforeEach(() => {
config = {
pairingChannelServerUri: 'ws://test'
pairingChannelServerUri: 'ws://test',
pairingClients: ['3c49430b43dfba77'],
};
relier = new Relier();
relier.set({
channelId: '1',
channelKey: 'dGVzdA==',
clientId: '3c49430b43dfba77',
redirectUri: 'https://example.com?code=1&state=2'
});
notifier = new Notifier();

835
npm-shrinkwrap.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -18,6 +18,8 @@
"test-functional": "node tests/intern.js",
"test-functional-oauth": "node tests/intern.js --suites=oauth",
"test-latest": "node tests/intern.js --fxaAuthRoot=https://latest.dev.lcip.org/auth/v1 --fxaContentRoot=https://latest.dev.lcip.org/ --fxaEmailRoot=http://restmail.net --fxaOAuthApp=https://123done-latest.dev.lcip.org/ --fxaUntrustedOauthApp=https://321done-latest.dev.lcip.org/ --fxaProduction=true --fxaToken=https://token.dev.lcip.org/1.0/sync/1.5",
"test-pairing": "node tests/intern.js --suites=pairing",
"test-pairing-circle": "node tests/intern.js --suites=pairing --fxaAuthRoot=https://fxaci.dev.lcip.org/auth --fxaEmailRoot=http://restmail.net --fxaOAuthApp=https://123done-fxaci.dev.lcip.org/ --fxaProduction=true --bailAfterFirstFailure=true",
"test-server": "node tests/intern.js --suites=server",
"test-travis": "node tests/intern.js --suites=travis"
},
@ -60,6 +62,7 @@
"fxa-geodb": "1.0.4",
"fxa-js-client": "1.0.7",
"fxa-mustache-loader": "0.0.2",
"fxa-pairing-channel": "1.0.1",
"fxa-shared": "1.0.18",
"got": "6.7.1",
"grunt": "1.0.3",
@ -146,10 +149,12 @@
"husky": "0.11.4",
"install": "0.12.1",
"intern": "4.3.1",
"jimp": "0.6.0",
"leadfoot": "1.7.4",
"npmshrink": "2.0.0",
"otplib": "7.1.0",
"proxyquire": "1.7.4",
"qrcode-reader": "1.0.4",
"request": "2.88.0",
"request-promise": "4.2.0",
"sinon": "4.5.0",

Просмотреть файл

@ -346,6 +346,22 @@ const conf = module.exports = convict({
'dist'
]
},
pairing: {
clients: {
default: [
'3c49430b43dfba77', // Reference browser
'a2270f727f45f648', // Fenix
],
doc: 'OAuth Client IDs that are allowed to pair. Remove all clients from this list to disable pairing.',
env: 'PAIRING_CLIENTS',
format: Array,
},
server_base_uri: {
default: 'wss://channelserver.services.mozilla.com',
doc: 'The url of the Pairing channel server.',
env: 'PAIRING_SERVER_BASE_URI'
},
},
port: {
default: 3030,
doc: 'HTTPS port for local dev',
@ -415,7 +431,8 @@ const conf = module.exports = convict({
'https://lockbox.firefox.com/fxa/ios-redirect.html',
'https://lockbox.firefox.com/fxa/android-redirect.html',
'https://accounts.firefox.com/oauth/success/a2270f727f45f648', // Fenix
'https://accounts.firefox.com/oauth/success/3c49430b43dfba77' // Reference browser
'https://accounts.firefox.com/oauth/success/3c49430b43dfba77', // Reference browser
'urn:ietf:wg:oauth:2.0:oob:pair-auth-webchannel'
]
},
'https://identity.mozilla.com/apps/send': {

Просмотреть файл

@ -29,6 +29,8 @@ module.exports = function (config) {
const PROFILE_SERVER = getOrigin(config.get('profile_url'));
const PROFILE_IMAGES_SERVER = getOrigin(config.get('profile_images_url'));
const PUBLIC_URL = config.get('public_url');
const PAIRING_SERVER_WEBSOCKET = getOrigin(config.get('pairing.server_base_uri'));
const PAIRING_SERVER_HTTP = PAIRING_SERVER_WEBSOCKET.replace(/^ws/,'http');
//
// Double quoted values
@ -55,7 +57,9 @@ module.exports = function (config) {
AUTH_SERVER,
OAUTH_SERVER,
PROFILE_SERVER,
MARKETING_EMAIL_SERVER
MARKETING_EMAIL_SERVER,
PAIRING_SERVER_WEBSOCKET,
PAIRING_SERVER_HTTP,
],
defaultSrc: [
SELF
@ -95,6 +99,8 @@ module.exports = function (config) {
MARKETING_EMAIL_SERVER,
NONE,
OAUTH_SERVER,
PAIRING_SERVER_HTTP,
PAIRING_SERVER_WEBSOCKET,
PROFILE_IMAGES_SERVER,
PROFILE_SERVER,
PUBLIC_URL,

Просмотреть файл

@ -33,6 +33,7 @@ module.exports = function (config, i18n) {
redirectVersionedToUnversioned('reset_password'),
redirectVersionedToUnversioned('verify_email'),
require('./routes/get-apple-app-site-association')(),
require('./routes/get-frontend-pairing')(),
require('./routes/get-frontend')(),
require('./routes/get-terms-privacy')(i18n),
require('./routes/get-update-firefox')(config),

Просмотреть файл

@ -0,0 +1,27 @@
/* 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/. */
'use strict';
// This route handler prevents REFRESH behaviour for the pairing flow
// If the user refreshes the browser during pairing, we instruct them to start over
module.exports = function () {
// The array is converted into a RegExp
const PAIRING_ROUTES = [
'pair/auth/allow',
'pair/auth/complete',
'pair/auth/wait_for_supp',
'pair/supp/allow',
'pair/supp/wait_for_auth',
].join('|'); // prepare for use in a RegExp
return {
method: 'get',
path: new RegExp('^/(' + PAIRING_ROUTES + ')/?$'),
process: function (req, res) {
res.redirect(302, '/pair/failure');
}
};
};

Просмотреть файл

@ -26,6 +26,11 @@ module.exports = function () {
'oauth/force_auth',
'oauth/signin',
'oauth/signup',
'pair',
'pair/failure',
'pair/success',
'pair/supp',
'pair/unsupported',
'primary_email_verified',
'report_signin',
'reset_password',

Просмотреть файл

@ -26,6 +26,7 @@ module.exports = function (config) {
// a /v1 suffix, but Firefox client code expects it without.
auth_server_base_url: stripV1Suffix(normalizeUrl(config.get('fxaccount_url'))),
oauth_server_base_url: normalizeUrl(config.get('oauth_url')),
pairing_server_base_uri: normalizeUrl(config.get('pairing.server_base_uri')),
profile_server_base_url: normalizeUrl(config.get('profile_url')),
sync_tokenserver_base_url: normalizeUrl(config.get('sync_tokenserver_url'))
};

Просмотреть файл

@ -17,6 +17,8 @@ module.exports = function (config) {
const MARKETING_EMAIL_ENABLED = config.get('marketing_email.enabled');
const MARKETING_EMAIL_PREFERENCES_URL = config.get('marketing_email.preferences_url');
const OAUTH_SERVER_URL = config.get('oauth_url');
const PAIRING_CHANNEL_URI = config.get('pairing.server_base_uri');
const PAIRING_CLIENTS = config.get('pairing.clients');
const PROFILE_SERVER_URL = config.get('profile_url');
const STATIC_RESOURCE_URL = config.get('static_resource_url');
const SCOPED_KEYS_ENABLED = config.get('scopedKeys.enabled');
@ -34,6 +36,8 @@ module.exports = function (config) {
marketingEmailServerUrl: MARKETING_EMAIL_API_URL,
oAuthClientId: CLIENT_ID,
oAuthUrl: OAUTH_SERVER_URL,
pairingChannelServerUri: PAIRING_CHANNEL_URI,
pairingClients: PAIRING_CLIENTS,
profileUrl: PROFILE_SERVER_URL,
release: RELEASE,
scopedKeysEnabled: SCOPED_KEYS_ENABLED,

Просмотреть файл

@ -143,6 +143,12 @@ module.exports = {
HEADER: '#fxa-permissions-header',
SUBMIT: '#accept'
},
PAIRING: {
AUTH_SUBMIT: '#auth-approve-btn',
COMPLETE: '#fxa-404-header',
START_PAIRING: '#start-pairing',
SUPP_SUBMIT: '#supp-approve-btn',
},
RECOVERY_KEY: {
CANCEL_BUTTON: '.cancel',
CONFIRM_PASSWORD_CONTINUE: '.generate-key-link',

149
tests/functional/pairing.js Normal file
Просмотреть файл

@ -0,0 +1,149 @@
/* 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/. */
'use strict';
const { registerSuite } = intern.getInterface('object');
const assert = intern.getPlugin('chai').assert;
const selectors = require('./lib/selectors');
const TestHelpers = require('../lib/helpers');
const FunctionalHelpers = require('./lib/helpers');
const config = intern._config;
const QUERY_PARAMS = '?context=fx_desktop_v3&service=sync&forceAboutAccounts=true&automatedBrowser=true&action=email';
const SIGNIN_PAGE_URL = `${config.fxaContentRoot}signin${QUERY_PARAMS}`;
const REDIRECT_HOST = encodeURIComponent(config.fxaContentRoot);
const BAD_CLIENT_ID = 'dcdb5ae7add825d2';
const BAD_OAUTH_REDIRECT = `${config.fxaOAuthApp}api/oauth`;
const GOOD_CLIENT_ID = '3c49430b43dfba77';
const GOOD_PAIR_URL = `${config.fxaContentRoot}pair/supp?response_type=code&client_id=${GOOD_CLIENT_ID}&redirect_uri=${REDIRECT_HOST}oauth%2Fsuccess%2F3c49430b43dfba77&scope=profile%2Bhttps%3A%2F%2Fidentity.mozilla.com%2Fapps%2Foldsync&state=foo&code_challenge_method=S256&code_challenge=IpOAcntLUmKITcxI_rDqMvFTeC9n_g0B8_Pj2yWZp7w&access_type=offline&keys_jwk=eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImlmcWY2U1pwMlM0ZjA5c3VhS093dmNsbWJxUm8zZXdGY0pvRURpYnc4MTQiLCJ5IjoiSE9LTXh5c1FseExqRGttUjZZbFpaY1Y4MFZBdk9nSWo1ZHRVaWJmYy1qTSJ9`; //eslint-disable-line max-len
const BAD_PAIR_URL = `${config.fxaContentRoot}pair/supp?response_type=code&client_id=${BAD_CLIENT_ID}&redirect_uri=${BAD_OAUTH_REDIRECT}&scope=profile%2Bhttps%3A%2F%2Fidentity.mozilla.com%2Fapps%2Foldsync&state=foo&code_challenge_method=S256&code_challenge=IpOAcntLUmKITcxI_rDqMvFTeC9n_g0B8_Pj2yWZp7w&access_type=offline&keys_jwk=eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImlmcWY2U1pwMlM0ZjA5c3VhS093dmNsbWJxUm8zZXdGY0pvRURpYnc4MTQiLCJ5IjoiSE9LTXh5c1FseExqRGttUjZZbFpaY1Y4MFZBdk9nSWo1ZHRVaWJmYy1qTSJ9`; //eslint-disable-line max-len
const PASSWORD = 'PASSWORD123123';
let email;
const {
createUser,
click,
closeCurrentWindow,
openPage,
openTab,
switchToWindow,
type,
thenify,
testElementTextInclude,
testElementExists,
testIsBrowserNotified,
} = FunctionalHelpers;
function getQrData(buffer) {
return new Promise(function (resolve, reject) {
const Jimp = require('jimp');
const QrCode = require('qrcode-reader');
Jimp.read(buffer, (err, image) => {
if (err) {
console.error(err);
return reject(err);
}
const qr = new QrCode();
qr.callback = (err, value) => {
if (err) {
console.error(err);
return reject(err);
} else {
return resolve(value.result);
}
};
qr.decode(image.bitmap);
});
});
}
const waitForQR = thenify(function () {
let requestAttempts = 0;
const maxAttempts = 3;
const parent = this.parent;
function pollForScreenshot () {
return parent
.sleep(1500)
.takeScreenshot()
.then((buffer) => {
return getQrData(buffer)
.then((result) => {
const pairingStuff = result.split('#')[1];
return parent
.then(openTab(GOOD_PAIR_URL + '#' + pairingStuff, selectors.SIGNUP.HEADER));
})
.catch((err) => {
requestAttempts++;
if (requestAttempts >= maxAttempts) {
return Promise.reject(new Error(`QRTimeout: ${err}`));
} else {
return new Promise(function (resolve, reject) {
setTimeout(function () {
pollForScreenshot().then(resolve, reject);
}, 1000);
});
}
});
});
}
return pollForScreenshot();
});
registerSuite('pairing', {
tests: {
'it can pair': function () {
email = TestHelpers.createEmail();
return this.remote
.then(createUser(email, PASSWORD, { preVerified: true }))
.then(openPage(SIGNIN_PAGE_URL, selectors.ENTER_EMAIL.HEADER))
.then(type(selectors.ENTER_EMAIL.EMAIL, email))
.then(click(selectors.ENTER_EMAIL.SUBMIT, selectors.SIGNIN_PASSWORD.HEADER))
.then(type(selectors.SIGNIN_PASSWORD.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN_PASSWORD.SUBMIT, selectors.CONNECT_ANOTHER_DEVICE.HEADER))
// but the login message is sent automatically.
.then(testIsBrowserNotified('fxaccounts:login'))
.then(openPage(`${config.fxaContentRoot}pair`, selectors.PAIRING.START_PAIRING))
.then(click(selectors.PAIRING.START_PAIRING))
.then(waitForQR())
.then(switchToWindow(1))
.then(click(selectors.PAIRING.SUPP_SUBMIT))
.catch((err) => {
if (err.message && err.message.includes('Web element reference')) {
// We have to catch an error here due to https://bugzilla.mozilla.org/show_bug.cgi?id=1422769
// .click still works, but just throws for no reason. We assert below that pairing still works.
} else {
// if this is an unknown error, then we throw
throw err;
}
})
.then(switchToWindow(0))
.then(click(selectors.PAIRING.AUTH_SUBMIT))
.then(switchToWindow(1))
.then(testElementExists(selectors.PAIRING.COMPLETE))
.getCurrentUrl()
.then(function (redirectResult) {
assert.ok(redirectResult.includes('code='), 'final OAuth redirect has the code');
assert.ok(redirectResult.includes('state='), 'final OAuth redirect has the state');
})
.end()
.then(closeCurrentWindow());
},
'handles invalid clients': function () {
return this.remote
.then(openPage(`${BAD_PAIR_URL}#channel_id=foo&channel_key=bar`, selectors['400'].ERROR))
.then(testElementTextInclude(selectors['400'].ERROR, 'Invalid pairing client'));
}
}
});

Просмотреть файл

@ -0,0 +1,7 @@
/* 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/. */
module.exports = [
'tests/functional/pairing.js',
];

Просмотреть файл

@ -11,6 +11,7 @@ const testsMain = require('./functional');
const testsOAuth = require('./functional_oauth');
const testsCircleCi = require('./functional_circle');
const testsTravisCi = require('./functional_travis');
const testsPairing = require('./functional_pairing');
const testsServer = require('./tests_server');
const testsServerResources = require('./tests_server_resources');
const testsAll = testsMain.concat(testsOAuth);
@ -81,6 +82,10 @@ if (args.suites) {
case 'oauth':
config.functionalSuites = testsOAuth;
break;
case 'pairing':
config.functionalSuites = testsPairing;
config.isTestingPairing = true;
break;
case 'all':
config.functionalSuites = testsAll;
break;

Просмотреть файл

@ -46,12 +46,14 @@ suite.tests['blockingRules'] = function () {
const directives = blockingRules.directives;
const connectSrc = directives.connectSrc;
assert.lengthOf(connectSrc, 5);
assert.lengthOf(connectSrc, 7);
assert.include(connectSrc, Sources.SELF);
assert.include(connectSrc, Sources.AUTH_SERVER);
assert.include(connectSrc, Sources.OAUTH_SERVER);
assert.include(connectSrc, Sources.PROFILE_SERVER);
assert.include(connectSrc, Sources.MARKETING_EMAIL_SERVER);
assert.include(connectSrc, Sources.PAIRING_SERVER_HTTP);
assert.include(connectSrc, Sources.PAIRING_SERVER_WEBSOCKET);
const defaultSrc = directives.defaultSrc;
assert.lengthOf(defaultSrc, 1);

Просмотреть файл

@ -0,0 +1,33 @@
/* 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/. */
const { registerSuite } = intern.getInterface('object');
const assert = intern.getPlugin('chai').assert;
const got = require('got');
const serverUrl = intern._config.fxaContentRoot.replace(/\/$/, '');
registerSuite('routes/get-frontend-pairing', {
tests: {
'direct navigation to pairing routes redirects': function () {
const PAIRING_ROUTES = [
'pair/auth/allow',
'pair/auth/complete',
'pair/auth/wait_for_supp',
'pair/supp/allow',
'pair/supp/wait_for_auth',
];
const requests = [];
PAIRING_ROUTES.forEach((route) => {
requests.push(got(`${serverUrl}/${route}`, {}));
});
return Promise.all(requests).then((results) => {
results.forEach((res) => {
// 'got' follows the redirects to /pair/failure with 200 status code
assert.equal(res.statusCode, 200);
assert.equal(res.url, `${serverUrl}/pair/failure`);
});
});
}
}
});

Просмотреть файл

@ -35,6 +35,7 @@ suite.tests['get-fxa-client-configuration route function'] = {
mocks.config.oauth_url = 'https://oauth.accounts.firefox.com';
mocks.config.profile_url = 'https://profile.accounts.firefox.com';
mocks.config.sync_tokenserver_url = 'https://token.services.mozilla.org';
mocks.config['pairing.server_base_uri'] = 'wss://channelserver.services.mozilla.com';
/*eslint-enable camelcase*/
},
tests: {
@ -45,7 +46,7 @@ suite.tests['get-fxa-client-configuration route function'] = {
'route interface is correct': function () {
route = getFxAClientConfig(mocks.config);
assert.equal(mocks.config.get.callCount, 5);
assert.equal(mocks.config.get.callCount, 6);
assert.isObject(route);
assert.lengthOf(Object.keys(route), 3);
assert.equal(route.method, 'get');
@ -64,7 +65,7 @@ suite.tests['get-fxa-client-configuration route function'] = {
route = getFxAClientConfig(mocks.config);
route.process(mocks.request, mocks.response);
assert.equal(mocks.config.get.callCount, 5);
assert.equal(mocks.config.get.callCount, 6);
assert.equal(mocks.response.json.callCount, 1);
var args = mocks.response.json.args[0];
assert.lengthOf(args, 1);
@ -80,7 +81,7 @@ suite.tests['get-fxa-client-configuration route function'] = {
route = getFxAClientConfig(mocks.config);
route.process(mocks.request, mocks.response);
assert.equal(mocks.config.get.callCount, 5);
assert.equal(mocks.config.get.callCount, 6);
assert.equal(mocks.response.json.callCount, 1);
assert.equal(mocks.response.header.callCount, 1);
var args = mocks.response.header.args[0];
@ -95,7 +96,7 @@ suite.tests['get-fxa-client-configuration route function'] = {
route = getFxAClientConfig(mocks.config);
route.process(mocks.request, mocks.response);
assert.equal(mocks.config.get.callCount, 5);
assert.equal(mocks.config.get.callCount, 6);
assert.equal(mocks.response.json.callCount, 1);
assert.equal(mocks.response.header.callCount, 1);
var args = mocks.response.header.args[0];
@ -110,7 +111,7 @@ suite.tests['get-fxa-client-configuration route function'] = {
route = getFxAClientConfig(mocks.config);
route.process(mocks.request, mocks.response);
assert.equal(mocks.config.get.callCount, 5);
assert.equal(mocks.config.get.callCount, 6);
assert.equal(mocks.response.json.callCount, 1);
assert.equal(mocks.response.header.callCount, 0);
}
@ -129,7 +130,7 @@ suite.tests['#get /.well-known/fxa-client-configuration - returns a JSON doc wit
assert.equal(res.headers['cache-control'], 'public, max-age=' + maxAge);
var result = JSON.parse(res.body);
assert.equal(Object.keys(result).length, 4);
assert.equal(Object.keys(result).length, 5);
var conf = intern._config;
var expectAuthRoot = conf.fxaAuthRoot;
@ -139,6 +140,7 @@ suite.tests['#get /.well-known/fxa-client-configuration - returns a JSON doc wit
assert.equal(result.oauth_server_base_url, conf.fxaOAuthRoot);
assert.equal(result.profile_server_base_url, conf.fxaProfileRoot);
assert.equal(result.sync_tokenserver_base_url, conf.fxaTokenRoot);
assert.equal(result.pairing_server_base_uri, config.get('pairing.server_base_uri'));
}).then(dfd.resolve.bind(dfd), dfd.reject.bind(dfd));
return dfd;

Просмотреть файл

@ -68,6 +68,7 @@ registerSuite('routes/get-index', {
config.get('marketing_email.api_url'));
assert.equal(sentConfig.oAuthClientId, config.get('oauth_client_id'));
assert.equal(sentConfig.oAuthUrl, config.get('oauth_url'));
assert.equal(sentConfig.pairingChannelServerUri, config.get('pairing.server_base_uri'));
assert.equal(sentConfig.profileUrl, config.get('profile_url'));
assert.equal(sentConfig.scopedKeysEnabled, config.get('scopedKeys.enabled'));
assert.ok(sentConfig.scopedKeysValidation, 'config validation is present');

Просмотреть файл

@ -25,6 +25,7 @@ module.exports = [
'tests/server/remote-address.js',
'tests/server/routes/get-apple-app-site-association.js',
'tests/server/routes/get-config.js',
'tests/server/routes/get-frontend-pairing.js',
'tests/server/routes/get-fxa-client-configuration.js',
'tests/server/routes/get-index.js',
'tests/server/routes/get-lbheartbeat.js',

Просмотреть файл

@ -17,6 +17,12 @@ if (process.argv.length > 1) {
}
if (profile) {
if (profile.isTestingPairing) {
// pairing UA override, this can be removed once the main catches up
UA_OVERRIDE = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0 FxATester/1.0';
}
// remove blocking of HTTP request on HTTPS domains
myProfile.setPreference('security.mixed_content.block_active_content', false);
@ -69,6 +75,13 @@ if (profile) {
// This prevents the "Save file" dialog for the "Update Firefox" screen.
myProfile.setPreference('browser.helperApps.neverAsk.saveToDisk', 'application/x-iso9660-image,application/x-tar,application/octet-stream');
// allowHttp for local dev
myProfile.setPreference('identity.fxaccounts.allowHttp', true);
myProfile.setPreference('identity.fxaccounts.pairing.enabled', true);
myProfile.setPreference('webchannel.allowObject.urlWhitelist', profile.fxaContentRoot.slice(0, -1));
myProfile.setPreference('identity.fxaccounts.remote.root', profile.fxaContentRoot.slice(0, -1));
myProfile.setPreference('identity.fxaccounts.autoconfig.uri', profile.fxaContentRoot.slice(0, -1));
myProfile.updatePreferences();

Просмотреть файл

@ -63,6 +63,7 @@ const webpackConfig = {
'es6-promise': path.resolve(__dirname, 'node_modules/es6-promise/dist/es6-promise'),
fxaClient: 'fxa-js-client/client/FxAccountClient',
fxaCryptoDeriver: path.resolve(__dirname, 'node_modules/fxa-crypto-relier/dist/fxa-crypto-relier/fxa-crypto-deriver'),
fxaPairingChannel: path.resolve(__dirname, 'node_modules/fxa-pairing-channel/dist/FxAccountsPairingChannel.babel.umd.js'),
'base32-decode': path.resolve(__dirname, 'node_modules/base32-decode/index'),
// jwcrypto is used by the main app and only contains DSA
// jwcrypto.rs is used by the unit tests to unbundle and verify