feat(pairing): enable pairing
This commit is contained in:
Родитель
091f6070ff
Коммит
6625d04be3
|
@ -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();
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче