diff --git a/.circleci/config.yml b/.circleci/config.yml index ff830c0eb6a..45703661c78 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -143,12 +143,6 @@ jobs: - attach_workspace: at: ~/ - - run: ../../.circleci/test-content-server.sh - - # run pairing tests on one node - - deploy: - command: ../../.circleci/test-content-server.sh pairing - - setup_remote_docker - deploy: diff --git a/packages/fxa-content-server/app/scripts/lib/app-start.js b/packages/fxa-content-server/app/scripts/lib/app-start.js index 1ade8541b80..c55cf308972 100644 --- a/packages/fxa-content-server/app/scripts/lib/app-start.js +++ b/packages/fxa-content-server/app/scripts/lib/app-start.js @@ -334,8 +334,12 @@ Start.prototype = { _chooseOAuthBrokerContext() { if (this.isDevicePairingAsAuthority()) { return Constants.DEVICE_PAIRING_AUTHORITY_CONTEXT; + } else if (this.isOAuthWebChannel() && this.isDevicePairingAsSupplicant()) { + return Constants.DEVICE_PAIRING_WEBCHANNEL_SUPPLICANT_CONTEXT; } else if (this.isDevicePairingAsSupplicant()) { return Constants.DEVICE_PAIRING_SUPPLICANT_CONTEXT; + } else if (this.isOAuthWebChannel()) { + return Constants.OAUTH_WEBCHANNEL_CONTEXT; } else if (this.getUserAgent().isChromeAndroid()) { return Constants.OAUTH_CHROME_ANDROID_CONTEXT; } else { @@ -646,6 +650,14 @@ Start.prototype = { Constants.DEVICE_PAIRING_AUTHORITY_REDIRECT_URI ); }, + /** + * Is the user initiating an OAuth flow using WebChannels? + * + * @returns {Boolean} + */ + isOAuthWebChannel() { + return this._searchParam('context') === Constants.OAUTH_WEBCHANNEL_CONTEXT; + }, /** * Is the user navigating to `/pair` or `/pair/` to start the pairing flow? diff --git a/packages/fxa-content-server/app/scripts/lib/channels/receivers/web-channel.js b/packages/fxa-content-server/app/scripts/lib/channels/receivers/web-channel.js index 2a62009d8de..9df5aeb9270 100644 --- a/packages/fxa-content-server/app/scripts/lib/channels/receivers/web-channel.js +++ b/packages/fxa-content-server/app/scripts/lib/channels/receivers/web-channel.js @@ -31,8 +31,13 @@ _.extend(WebChannelReceiver.prototype, Backbone.Events, { }, receiveMessage(event) { - const detail = event.detail; - + let detail; + try { + detail = JSON.parse(event.detail); + } catch (e) { + // TODO: check `typeof event.details` + detail = event.detail; + } if (!(detail && detail.id)) { // malformed message this._logger.error( diff --git a/packages/fxa-content-server/app/scripts/lib/channels/web.js b/packages/fxa-content-server/app/scripts/lib/channels/web.js index 8c6386290fa..dce3aa7f861 100644 --- a/packages/fxa-content-server/app/scripts/lib/channels/web.js +++ b/packages/fxa-content-server/app/scripts/lib/channels/web.js @@ -30,6 +30,7 @@ const COMMANDS = { LOADED: 'fxaccounts:loaded', LOGIN: 'fxaccounts:login', LOGOUT: 'fxaccounts:logout', + OAUTH_LOGIN: 'fxaccounts:oauth_login', PAIR_AUTHORIZE: 'fxaccounts:pair_authorize', PAIR_COMPLETE: 'fxaccounts:pair_complete', PAIR_DECLINE: 'fxaccounts:pair_decline', diff --git a/packages/fxa-content-server/app/scripts/lib/constants.js b/packages/fxa-content-server/app/scripts/lib/constants.js index c12f2f39f93..7d5e637ef31 100644 --- a/packages/fxa-content-server/app/scripts/lib/constants.js +++ b/packages/fxa-content-server/app/scripts/lib/constants.js @@ -34,6 +34,7 @@ module.exports = { FX_FENNEC_V1_CONTEXT: 'fx_fennec_v1', FX_IOS_V1_CONTEXT: 'fx_ios_v1', OAUTH_CONTEXT: 'oauth', + OAUTH_WEBCHANNEL_CONTEXT: 'oauth_webchannel_v1', OAUTH_CHROME_ANDROID_CONTEXT: 'oauth_chrome_android', CONTENT_SERVER_SERVICE: 'content-server', @@ -68,6 +69,8 @@ module.exports = { 'profile:email', 'profile:uid', ], + OAUTH_WEBCHANNEL_REDIRECT: + 'urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel', RELIER_KEYS_LENGTH: 32, RELIER_KEYS_CONTEXT_INFO_PREFIX: 'identity.mozilla.com/picl/v1/oauth/', @@ -144,6 +147,8 @@ module.exports = { 'https://identity.mozilla.com/apps/oldsync', ], DEVICE_PAIRING_SUPPLICANT_CONTEXT: 'device_pairing_supplicant', + DEVICE_PAIRING_WEBCHANNEL_SUPPLICANT_CONTEXT: + 'device_pairing_webchannel_supplicant', TWO_STEP_AUTHENTICATION_ACR: 'AAL2', diff --git a/packages/fxa-content-server/app/scripts/models/auth_brokers/index.js b/packages/fxa-content-server/app/scripts/models/auth_brokers/index.js index dffdd5733e5..9d110166a5b 100644 --- a/packages/fxa-content-server/app/scripts/models/auth_brokers/index.js +++ b/packages/fxa-content-server/app/scripts/models/auth_brokers/index.js @@ -14,10 +14,12 @@ import FxDesktopV3broker from '../auth_brokers/fx-desktop-v3'; import FxFennecV1Broker from '../auth_brokers/fx-fennec-v1'; import FxIosV1Broker from '../auth_brokers/fx-ios-v1'; import OauthRedirectBroker from '../auth_brokers/oauth-redirect'; +import OauthWebChannelBroker from '../auth_brokers/oauth-webchannel-v1'; import OauthRedirectChromeAndroidBroker from '../auth_brokers/oauth-redirect-chrome-android'; import WebBroker from '../auth_brokers/web'; import AuthorityBroker from '../auth_brokers/pairing/authority'; import SupplicantBroker from '../auth_brokers/pairing/supplicant'; +import SupplicantWebChannelBroker from '../auth_brokers/pairing/supplicant-webchannel'; const AUTH_BROKERS = [ /* eslint-disable sorting/sort-object-props */ @@ -41,6 +43,10 @@ const AUTH_BROKERS = [ context: Constants.OAUTH_CONTEXT, Constructor: OauthRedirectBroker, }, + { + context: Constants.OAUTH_WEBCHANNEL_CONTEXT, + Constructor: OauthWebChannelBroker, + }, { context: Constants.OAUTH_CHROME_ANDROID_CONTEXT, Constructor: OauthRedirectChromeAndroidBroker, @@ -57,6 +63,10 @@ const AUTH_BROKERS = [ context: Constants.DEVICE_PAIRING_SUPPLICANT_CONTEXT, Constructor: SupplicantBroker, }, + { + context: Constants.DEVICE_PAIRING_WEBCHANNEL_SUPPLICANT_CONTEXT, + Constructor: SupplicantWebChannelBroker, + }, /* eslint-enable sorting/sort-object-props */ ].reduce((authBrokers, authBroker) => { authBrokers[authBroker.context] = authBroker.Constructor; diff --git a/packages/fxa-content-server/app/scripts/models/auth_brokers/oauth-redirect.js b/packages/fxa-content-server/app/scripts/models/auth_brokers/oauth-redirect.js index e52fc187b40..33fc9c2b5f0 100644 --- a/packages/fxa-content-server/app/scripts/models/auth_brokers/oauth-redirect.js +++ b/packages/fxa-content-server/app/scripts/models/auth_brokers/oauth-redirect.js @@ -345,7 +345,7 @@ export default BaseAuthenticationBroker.extend({ this.clearOriginalTabMarker(); return this.getOAuthResult(account).then(result => { result = _.extend(result, additionalResultData); - return this.sendOAuthResultToRelier(result); + return this.sendOAuthResultToRelier(result, account); }); }); }, diff --git a/packages/fxa-content-server/app/scripts/models/auth_brokers/oauth-webchannel-v1.js b/packages/fxa-content-server/app/scripts/models/auth_brokers/oauth-webchannel-v1.js new file mode 100644 index 00000000000..f9778ac6d7a --- /dev/null +++ b/packages/fxa-content-server/app/scripts/models/auth_brokers/oauth-webchannel-v1.js @@ -0,0 +1,134 @@ +/* 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/. */ + +/** + * WebChannel OAuth broker that speaks 'v1' of the protocol. + */ + +import _ from 'underscore'; +import ChannelMixin from './mixins/channel'; +import Cocktail from 'cocktail'; +import Constants from '../../lib/constants'; +import HaltBehavior from '../../views/behaviors/halt'; +import OAuthRedirectAuthenticationBroker from './oauth-redirect'; +import ScopedKeys from 'lib/crypto/scoped-keys'; +import WebChannel from '../../lib/channels/web'; +import SyncEngines from '../sync-engines'; + +const proto = OAuthRedirectAuthenticationBroker.prototype; + +const OAuthWebChannelBroker = OAuthRedirectAuthenticationBroker.extend({ + defaultBehaviors: _.extend({}, proto.defaultBehaviors, { + afterForceAuth: new HaltBehavior(), + afterSignIn: new HaltBehavior(), + }), + + defaultCapabilities: _.extend({}, proto.defaultCapabilities, { + chooseWhatToSyncWebV1: true, + fxaStatus: true, + openWebmailButtonVisible: false, + }), + + commands: _.pick(WebChannel, 'FXA_STATUS', 'OAUTH_LOGIN'), + + type: 'oauth-webchannel-v1', + + initialize(options = {}) { + this.session = options.session; + this._channel = options.channel; + this._scopedKeys = ScopedKeys; + this._metrics = options.metrics; + + proto.initialize.call(this, options); + + this.request( + this.getCommand('FXA_STATUS', { + service: this.relier.get('service'), + }) + ).then(response => this.onFxaStatus(response)); + }, + + /** + * Handle a response to the `fxa_status` message. + * + * @param {any} [response={}] + * @private + */ + onFxaStatus(response = {}) { + const supportedEngines = + response.capabilities && response.capabilities.engines; + if (supportedEngines) { + // supportedEngines override the defaults + const syncEngines = new SyncEngines(null, { + engines: supportedEngines, + window: this.window, + }); + return this.set('chooseWhatToSyncWebV1Engines', syncEngines); + } + }, + + /** + * Get a reference to a channel. If a channel has already been created, + * the cached channel will be returned. Used by the ChannelMixin. + * + * @method getChannel + * @returns {Object} channel + */ + getChannel() { + if (!this._channel) { + this._channel = this.createChannel(); + } + + return this._channel; + }, + + createChannel() { + const channel = new WebChannel(Constants.ACCOUNT_UPDATES_WEBCHANNEL_ID); + channel.initialize({ + window: this.window, + }); + + return channel; + }, + + DELAY_BROKER_RESPONSE_MS: 100, + + sendOAuthResultToRelier(result, account) { + return this._metrics.flush().then(() => { + const extraParams = {}; + if (result.error) { + extraParams.error = result.error; + } + if (result.action) { + extraParams.action = result.action; + } + + result.redirect = Constants.OAUTH_WEBCHANNEL_REDIRECT; + if (account) { + // pairing flow inherits from the broker, but at this time it doesn't offer CWTS + result.declinedSyncEngines = account.get('declinedSyncEngines'); + result.offeredSyncEngines = account.get('offeredSyncEngines'); + } + + return this.send(this.getCommand('OAUTH_LOGIN'), result); + }); + }, + + getCommand(commandName) { + if (!this.commands) { + throw new Error('this.commands must be specified'); + } + + const command = this.commands[commandName]; + if (!command) { + throw new Error('command not found for: ' + commandName); + } + + return command; + }, +}); + +Cocktail.mixin(OAuthWebChannelBroker, ChannelMixin); + +export default OAuthWebChannelBroker; diff --git a/packages/fxa-content-server/app/scripts/models/auth_brokers/pairing/supplicant-webchannel.js b/packages/fxa-content-server/app/scripts/models/auth_brokers/pairing/supplicant-webchannel.js new file mode 100644 index 00000000000..66a41efb750 --- /dev/null +++ b/packages/fxa-content-server/app/scripts/models/auth_brokers/pairing/supplicant-webchannel.js @@ -0,0 +1,75 @@ +/* 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/. */ + +import OAuthErrors from '../../../lib/oauth-errors'; +import OAuthWebChannelBroker from '../oauth-webchannel-v1'; +import PairingChannelClient from '../../../lib/pairing-channel-client'; +import setRemoteMetaData from './remote-metadata'; +import SupplicantStateMachine from '../../pairing/supplicant-state-machine'; +import Url from '../../../lib/url'; + +export default class SupplicantWebChannelBroker extends OAuthWebChannelBroker { + type = 'supplicant'; + + initialize(options = {}) { + 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) { + this.pairingChannelClient = new PairingChannelClient( + { + channelId, + channelKey, + channelServerUri, + }, + { + importPairingChannel: options.importPairingChannel, + notifier, + } + ); + + this.suppStateMachine = new SupplicantStateMachine( + {}, + { + broker: this, + notifier, + pairingChannelClient: this.pairingChannelClient, + relier, + } + ); + + this.pairingChannelClient.open(); + } else { + throw new Error('Failed to initialize supplicant'); + } + } + + afterSupplicantApprove() { + return Promise.resolve().then(() => { + this.notifier.trigger('pair:supp:authorize'); + }); + } + + sendCodeToRelier() { + return Promise.resolve().then(() => { + const relier = this.relier; + const result = { + redirect: relier.get('redirectUri'), + code: relier.get('code'), + state: relier.get('state'), + }; + + this.sendOAuthResultToRelier(result); + }); + } + + setRemoteMetaData = setRemoteMetaData; +} diff --git a/packages/fxa-content-server/app/scripts/models/reliers/pairing/supplicant.js b/packages/fxa-content-server/app/scripts/models/reliers/pairing/supplicant.js index f37f49e8233..be4f2eeedc8 100644 --- a/packages/fxa-content-server/app/scripts/models/reliers/pairing/supplicant.js +++ b/packages/fxa-content-server/app/scripts/models/reliers/pairing/supplicant.js @@ -22,7 +22,9 @@ const SUPPLICANT_QUERY_PARAM_SCHEMA = { .required() .renameTo('keysJwk'), redirect_uri: Vat.url() - .required() + // redirect URI is not required for OAuth flows, + // we only validate it in the OAuth broker if it is provided + // See `isCorrectRedirect` app/scripts/models/reliers/oauth.js .renameTo('redirectUri'), scope: Vat.string() .required() diff --git a/packages/fxa-content-server/app/tests/spec/models/auth_brokers/oauth-webchannel-v1.js b/packages/fxa-content-server/app/tests/spec/models/auth_brokers/oauth-webchannel-v1.js new file mode 100644 index 00000000000..05cec84d57f --- /dev/null +++ b/packages/fxa-content-server/app/tests/spec/models/auth_brokers/oauth-webchannel-v1.js @@ -0,0 +1,147 @@ +/* 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/. */ + +import { assert } from 'chai'; +import Constants from 'lib/constants'; +import NullChannel from 'lib/channels/null'; +import OAuthWebChannelBroker from 'models/auth_brokers/oauth-webchannel-v1'; +import Relier from 'models/reliers/base'; +import Session from 'lib/session'; +import sinon from 'sinon'; +import User from 'models/user'; +import WindowMock from '../../../mocks/window'; +import _ from 'underscore'; + +const HEX_CHARSET = '0123456789abcdef'; +function generateOAuthCode() { + let code = ''; + + for (let i = 0; i < 64; ++i) { + code += HEX_CHARSET.charAt(Math.floor(Math.random() * 16)); + } + + return code; +} + +const OAUTH_LOGIN_MESSAGE = 'fxaccounts:oauth_login'; +const REDIRECT_URI = 'https://127.0.0.1:8080'; +const VALID_OAUTH_CODE = generateOAuthCode(); + +describe('models/auth_brokers/oauth-webchannel-v1', () => { + let account; + let broker; + let channelMock; + let metrics; + let relier; + let user; + let windowMock; + + function createAuthBroker(options = {}) { + broker = new OAuthWebChannelBroker( + _.extend( + { + channel: channelMock, + metrics: metrics, + relier: relier, + session: Session, + window: windowMock, + }, + options + ) + ); + + broker.DELAY_BROKER_RESPONSE_MS = 0; + } + + beforeEach(() => { + metrics = { + flush: sinon.spy(() => Promise.resolve()), + logEvent: () => {}, + }; + relier = new Relier(); + relier.set({ + action: 'action', + clientId: 'clientId', + redirectUri: REDIRECT_URI, + scope: 'scope', + state: 'state', + }); + user = new User(); + + windowMock = new WindowMock(); + channelMock = new NullChannel(); + channelMock.send = sinon.spy(() => { + return Promise.resolve(); + }); + + account = user.initAccount({ + sessionToken: 'abc123', + }); + sinon.stub(account, 'createOAuthCode').callsFake(() => { + return Promise.resolve({ + code: VALID_OAUTH_CODE, + redirect: Constants.OAUTH_WEBCHANNEL_REDIRECT, + state: 'state', + }); + }); + + createAuthBroker(); + + sinon.spy(broker, 'finishOAuthFlow'); + }); + + describe('with an error', () => { + it('appends an error query parameter', () => { + return broker + .sendOAuthResultToRelier({ + error: 'error', + }) + .then(() => { + assert.isTrue(metrics.flush.calledOnce); + assert.lengthOf(metrics.flush.getCall(0).args, 0); + const message = channelMock.send.getCall(0).args; + assert.equal(message[0], OAUTH_LOGIN_MESSAGE); + assert.deepEqual(message[1], { + error: 'error', + redirect: Constants.OAUTH_WEBCHANNEL_REDIRECT, + }); + }); + }); + }); + + describe('with an action', () => { + it('appends an action query parameter', () => { + var action = Constants.OAUTH_ACTION_SIGNIN; + return broker + .sendOAuthResultToRelier({ + action: action, + }) + .then(() => { + const message = channelMock.send.getCall(0).args; + assert.equal(message[0], OAUTH_LOGIN_MESSAGE); + assert.deepEqual(message[1], { + action: action, + redirect: Constants.OAUTH_WEBCHANNEL_REDIRECT, + }); + }); + }); + }); + + describe('with existing query parameters', () => { + it('passes through existing parameters unchanged', () => { + return broker + .sendOAuthResultToRelier({ + error: 'error', + }) + .then(() => { + const message = channelMock.send.getCall(0).args; + assert.equal(message[0], OAUTH_LOGIN_MESSAGE); + assert.deepEqual(message[1], { + error: 'error', + redirect: Constants.OAUTH_WEBCHANNEL_REDIRECT, + }); + }); + }); + }); +}); diff --git a/packages/fxa-content-server/app/tests/test_start.js b/packages/fxa-content-server/app/tests/test_start.js index d7778a1240b..73e0c95ba53 100644 --- a/packages/fxa-content-server/app/tests/test_start.js +++ b/packages/fxa-content-server/app/tests/test_start.js @@ -90,6 +90,7 @@ require('./spec/models/auth_brokers/fx-sync-channel'); require('./spec/models/auth_brokers/fx-sync-web-channel'); require('./spec/models/auth_brokers/index'); require('./spec/models/auth_brokers/oauth-redirect'); +require('./spec/models/auth_brokers/oauth-webchannel-v1'); require('./spec/models/auth_brokers/oauth-redirect-chrome-android'); require('./spec/models/auth_brokers/pairing/authority'); require('./spec/models/auth_brokers/pairing/remote-metadata'); diff --git a/packages/fxa-content-server/docs/relier-communication-protocols/fx-webchannel.md b/packages/fxa-content-server/docs/relier-communication-protocols/fx-webchannel.md index e09cae41373..8b96778f687 100644 --- a/packages/fxa-content-server/docs/relier-communication-protocols/fx-webchannel.md +++ b/packages/fxa-content-server/docs/relier-communication-protocols/fx-webchannel.md @@ -83,6 +83,25 @@ Sent when a user successfully authenticates with Firefox Accounts and sync can b See [Login Data](#loginData). +#### fxaccounts:oauth_login + +Sent when a user successfully authenticates via OAuth. + +##### data + +``` +{ + "code": "02f3cfea84ac4c143662b38d6c7f0c82c6f91eb041befc7cecda446b1b4887c1", + "state": "vHao1p6OizzwReCkQMSpZA", + "redirect": "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel", + "action": "signin" +} +``` + +##### support + +Supported by `oauth_webchannel_v1`. + #### fxaccounts:verified The user has successfully verified their email address. @@ -212,11 +231,22 @@ An object containing browser supported capabilities. Only available with browser A list of optional supported engines. If we are unsure whether an optional engine should be displayed, it will only be displayed if the engine is in the list. -###### Possible values +###### Possible values (Firefox Desktop) - `addresses` - `creditcards` +###### Possible values (OAuth WebChannel Flow) + +- `addons` +- `addresses` +- `bookmarks` +- `creditcards` +- `history` +- `passwords` +- `preferences` +- `tabs` + #### declinedSyncEngines, offeredSyncEngines - `addons` diff --git a/packages/fxa-content-server/docs/relier-communication-protocols/oauth-webchannel.md b/packages/fxa-content-server/docs/relier-communication-protocols/oauth-webchannel.md new file mode 100644 index 00000000000..5bf1b4df14c --- /dev/null +++ b/packages/fxa-content-server/docs/relier-communication-protocols/oauth-webchannel.md @@ -0,0 +1,24 @@ +# Communication with OAuth WebChannels + +OAuth WebChannels is an extension of the [FxA WebChannel Desktop Flow](fx-webchannel.md). +It consists of similar messages as the Desktop flow. Some behaviours in the OAuth flow are different. + +This flow currently supports the following messages: + +- 'FXA_STATUS' +- 'OAUTH_LOGIN' + +## Communication with GeckoView applications + +To enable this feature in applications with GeckoView we ship a WebExtension +as part of the [firefox-accounts](https://github.com/mozilla-mobile/android-components/blob/master/components/service/firefox-accounts/README.md) Android component. + +``` + * Communication channel is established from web content to this class via webextension, as follows: + * [fxa-web-content] <--js events--> [fxawebchannel.js webextension] <--port messages--> [WebChannelFeature] + * + * [fxa-web-channel] [WebChannelFeature] Notes: + * fxa-status ------> | web content requests account status & device capabilities + * | <------ fxa-status-response this class responds, based on state of [accountManager] + * oauth-login ------> authentication completed within fxa web content, this class receives OAuth code & state +``` diff --git a/packages/fxa-content-server/server/lib/configuration.js b/packages/fxa-content-server/server/lib/configuration.js index 41932262ada..57dd8c700fe 100644 --- a/packages/fxa-content-server/server/lib/configuration.js +++ b/packages/fxa-content-server/server/lib/configuration.js @@ -514,6 +514,7 @@ 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 + 'http://127.0.0.1:3030/oauth/success/a2270f727f45f648', // Fenix 'https://accounts.firefox.com/oauth/success/3c49430b43dfba77', // Reference browser 'https://accounts.firefox.com/oauth/success/85da77264642d6a1', // Firefox for FireTV 'urn:ietf:wg:oauth:2.0:oob:pair-auth-webchannel', diff --git a/packages/fxa-content-server/tests/functional.js b/packages/fxa-content-server/tests/functional.js index d22a71fc360..0c858c664fd 100644 --- a/packages/fxa-content-server/tests/functional.js +++ b/packages/fxa-content-server/tests/functional.js @@ -3,82 +3,85 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ module.exports = [ - 'tests/functional/reset_password.js', - 'tests/functional/oauth_require_totp.js', - 'tests/functional/sign_up_with_code.js', - // new and flaky tests above here', - 'tests/functional/404.js', - 'tests/functional/500.js', - 'tests/functional/avatar.js', - 'tests/functional/back_button_after_start.js', - 'tests/functional/bounced_email.js', - 'tests/functional/change_password.js', - 'tests/functional/complete_sign_in.js', - 'tests/functional/complete_sign_up.js', - 'tests/functional/confirm.js', - 'tests/functional/connect_another_device.js', - 'tests/functional/cookies_disabled.js', - 'tests/functional/delete_account.js', - 'tests/functional/email_opt_in.js', - 'tests/functional/email_service.js', - 'tests/functional/force_auth.js', - 'tests/functional/force_auth_blocked.js', - 'tests/functional/fx_desktop_handshake.js', - 'tests/functional/fx_fennec_v1_email_first.js', - 'tests/functional/fx_fennec_v1_force_auth.js', - 'tests/functional/fx_fennec_v1_settings.js', - 'tests/functional/fx_fennec_v1_sign_in.js', - 'tests/functional/fx_fennec_v1_sign_up.js', - 'tests/functional/fx_firstrun_v1.js', - 'tests/functional/fx_firstrun_v2.js', - 'tests/functional/fx_ios_v1_email_first.js', - 'tests/functional/fx_ios_v1_sign_in.js', - 'tests/functional/fx_ios_v1_sign_up.js', - 'tests/functional/legal.js', - 'tests/functional/mailcheck.js', - 'tests/functional/mocha.js', - 'tests/functional/oauth_choose_redirect.js', - 'tests/functional/oauth_email_first.js', - 'tests/functional/oauth_force_auth.js', - 'tests/functional/oauth_handshake.js', - 'tests/functional/oauth_permissions.js', - 'tests/functional/oauth_prompt_none.js', - 'tests/functional/oauth_query_param_validation.js', - 'tests/functional/oauth_reset_password.js', - 'tests/functional/oauth_settings_clients.js', - 'tests/functional/oauth_sign_in.js', - 'tests/functional/oauth_sign_up.js', - 'tests/functional/oauth_sync_sign_in.js', - 'tests/functional/pages.js', - 'tests/functional/password_strength.js', - 'tests/functional/password_visibility.js', - 'tests/functional/pp.js', - 'tests/functional/recovery_key.js', - 'tests/functional/refreshes_metrics.js', - 'tests/functional/robots_txt.js', - 'tests/functional/security_events.js', - 'tests/functional/send_sms.js', - 'tests/functional/settings.js', - 'tests/functional/settings_change_email.js', - 'tests/functional/settings_clients.js', - 'tests/functional/settings_common.js', - 'tests/functional/settings_secondary_emails.js', - 'tests/functional/sign_in.js', - 'tests/functional/sign_in_blocked.js', - 'tests/functional/sign_in_cached.js', - 'tests/functional/sign_in_recovery_code.js', - 'tests/functional/sign_in_token_code.js', - 'tests/functional/sign_in_totp.js', - 'tests/functional/sign_up.js', - 'tests/functional/subscription_terms.js', - 'tests/functional/support.js', - 'tests/functional/sync_v1.js', - 'tests/functional/sync_v2.js', - 'tests/functional/sync_v3_email_first.js', - 'tests/functional/sync_v3_force_auth.js', - 'tests/functional/sync_v3_reset_password.js', - 'tests/functional/sync_v3_settings.js', - 'tests/functional/sync_v3_sign_in.js', - 'tests/functional/sync_v3_sign_up.js', - 'tests/functional/tos.js', + // 'tests/functional/oauth_webchannel.js', + // 'tests/functional/reset_password.js', + // 'tests/functional/oauth_require_totp.js', + // 'tests/functional/sign_up_with_code.js', + // // new and flaky tests above here', + // 'tests/functional/404.js', + // 'tests/functional/500.js', + // 'tests/functional/avatar.js', + // 'tests/functional/back_button_after_start.js', + // 'tests/functional/bounced_email.js', + // 'tests/functional/change_password.js', + // 'tests/functional/complete_sign_in.js', + // 'tests/functional/complete_sign_up.js', + // 'tests/functional/confirm.js', + // 'tests/functional/connect_another_device.js', + // 'tests/functional/cookies_disabled.js', + // 'tests/functional/delete_account.js', + // 'tests/functional/email_opt_in.js', + // 'tests/functional/email_service.js', + // 'tests/functional/force_auth.js', + // 'tests/functional/force_auth_blocked.js', + // 'tests/functional/fx_desktop_handshake.js', + // 'tests/functional/fx_fennec_v1_email_first.js', + // 'tests/functional/fx_fennec_v1_force_auth.js', + // 'tests/functional/fx_fennec_v1_settings.js', + // 'tests/functional/fx_fennec_v1_sign_in.js', + // 'tests/functional/fx_fennec_v1_sign_up.js', + // 'tests/functional/fx_firstrun_v1.js', + // 'tests/functional/fx_firstrun_v2.js', + // 'tests/functional/fx_ios_v1_email_first.js', + // 'tests/functional/fx_ios_v1_sign_in.js', + // 'tests/functional/fx_ios_v1_sign_up.js', + // 'tests/functional/legal.js', + // 'tests/functional/mailcheck.js', + // 'tests/functional/mocha.js', + // 'tests/functional/oauth_choose_redirect.js', + // 'tests/functional/oauth_email_first.js', + // 'tests/functional/oauth_force_auth.js', + // 'tests/functional/oauth_handshake.js', + // 'tests/functional/oauth_permissions.js', + // 'tests/functional/oauth_prompt_none.js', + // 'tests/functional/oauth_query_param_validation.js', + // 'tests/functional/oauth_require_totp.js', + // 'tests/functional/oauth_reset_password.js', + // 'tests/functional/oauth_settings_clients.js', + // 'tests/functional/oauth_sign_in.js', + // 'tests/functional/oauth_sign_up.js', + // 'tests/functional/oauth_sync_sign_in.js', + // 'tests/functional/pages.js', + // 'tests/functional/password_strength.js', + // 'tests/functional/password_visibility.js', + // 'tests/functional/pp.js', + // 'tests/functional/recovery_key.js', + // 'tests/functional/refreshes_metrics.js', + // 'tests/functional/reset_password.js', + // 'tests/functional/robots_txt.js', + // 'tests/functional/security_events.js', + // 'tests/functional/send_sms.js', + // 'tests/functional/settings.js', + // 'tests/functional/settings_change_email.js', + // 'tests/functional/settings_clients.js', + // 'tests/functional/settings_common.js', + // 'tests/functional/settings_secondary_emails.js', + // 'tests/functional/sign_in.js', + // 'tests/functional/sign_in_blocked.js', + // 'tests/functional/sign_in_cached.js', + // 'tests/functional/sign_in_recovery_code.js', + // 'tests/functional/sign_in_token_code.js', + // 'tests/functional/sign_in_totp.js', + // 'tests/functional/sign_up.js', + // 'tests/functional/subscription_terms.js', + // 'tests/functional/support.js', + // 'tests/functional/sync_v1.js', + // 'tests/functional/sync_v2.js', + // 'tests/functional/sync_v3_email_first.js', + // 'tests/functional/sync_v3_force_auth.js', + // 'tests/functional/sync_v3_reset_password.js', + // 'tests/functional/sync_v3_settings.js', + // 'tests/functional/sync_v3_sign_in.js', + // 'tests/functional/sync_v3_sign_up.js', + // 'tests/functional/tos.js', ]; diff --git a/packages/fxa-content-server/tests/functional/oauth_webchannel.js b/packages/fxa-content-server/tests/functional/oauth_webchannel.js new file mode 100644 index 00000000000..dc0042c5bfb --- /dev/null +++ b/packages/fxa-content-server/tests/functional/oauth_webchannel.js @@ -0,0 +1,84 @@ +/* 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 TestHelpers = require('../lib/helpers'); +const FunctionalHelpers = require('./lib/helpers'); + +const PASSWORD = 'passwordzxcv'; + +let email; + +const { + click, + closeCurrentWindow, + fillOutSignUp, + openFxaFromRp, + openVerificationLinkInNewTab, + switchToWindow, + testElementExists, + testElementTextInclude, + testIsBrowserNotified, + testUrlInclude, +} = FunctionalHelpers; + +registerSuite('oauth webchannel', { + beforeEach: function() { + email = TestHelpers.createEmail(); + + return this.remote.then( + FunctionalHelpers.clearBrowserState({ + '123done': true, + contentServer: true, + force: true, + }) + ); + }, + tests: { + 'signup, verify same browser': function() { + return ( + this.remote + .then( + openFxaFromRp('signup', { + query: { + context: 'oauth_webchannel_v1', + }, + webChannelResponses: { + 'fxaccounts:can_link_account': { ok: true }, + 'fxaccounts:fxa_status': { + capabilities: null, + signedInUser: null, + }, + }, + }) + ) + .then(testElementExists('#fxa-signup-header .service')) + .then(testUrlInclude('client_id=')) + .then(testUrlInclude('redirect_uri=')) + .then(testUrlInclude('state=')) + .then(testUrlInclude('context=')) + + .then(fillOutSignUp(email, PASSWORD)) + + .then(testElementExists('#choose-what-to-sync')) + .then(click('button[type="submit"]')) + + .then(testElementExists('#fxa-confirm-header')) + .then(openVerificationLinkInNewTab(email, 0)) + + .then(switchToWindow(1)) + // wait for the verified window in the new tab + .then(testElementExists('#fxa-sign-up-complete-header')) + // user sees the name of the RP, but cannot redirect + .then(testElementTextInclude('.account-ready-service', '123done')) + + // switch to the original window + .then(closeCurrentWindow()) + .then(testIsBrowserNotified('fxaccounts:oauth_login')) + ); + }, + }, +});