Skip to content

Commit

Permalink
feat(oauth): support Fenix WebChannels
Browse files Browse the repository at this point in the history
  • Loading branch information
vladikoff committed Sep 4, 2019
1 parent 29cb2fc commit 56971a5
Show file tree
Hide file tree
Showing 27 changed files with 814 additions and 132 deletions.
12 changes: 12 additions & 0 deletions packages/fxa-content-server/app/scripts/lib/app-start.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,21 @@ _.extend(WebChannelReceiver.prototype, Backbone.Events, {
},

receiveMessage(event) {
const detail = event.detail;
let detail = event.detail;

if (_.isString(detail)) {
// if the event arrives as a string, then we need to parse it
// this affects message channels such as the GeckoView WebExtension
try {
detail = JSON.parse(event.detail);
} catch (e) {
this._logger.error(
'failed parsing WebChannelMessageToContent event',
detail
);
return;
}
}

if (!(detail && detail.id)) {
// malformed message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
5 changes: 5 additions & 0 deletions packages/fxa-content-server/app/scripts/lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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/',
Expand Down Expand Up @@ -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',

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,6 @@ const FxSyncChannelAuthenticationBroker = FxSyncAuthenticationBroker.extend(
sendChangePasswordNotice: true,
}),

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;
},

/**
* Initialize the broker
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
*/

import _ from 'underscore';
import Cocktail from 'cocktail';
import Constants from '../../lib/constants';
import FxSyncChannelAuthenticationBroker from './fx-sync-channel';
import WebChannel from '../../lib/channels/web';
import ChannelMixin from './mixins/channel';

const proto = FxSyncChannelAuthenticationBroker.prototype;

Expand Down Expand Up @@ -68,4 +70,6 @@ const FxSyncWebChannelAuthenticationBroker = FxSyncChannelAuthenticationBroker.e
}
);

Cocktail.mixin(FxSyncWebChannelAuthenticationBroker, ChannelMixin);

export default FxSyncWebChannelAuthenticationBroker;
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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,
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,40 @@ function ensureActionReturnsPromise(action) {
return action;
}

var ChannelMixin = {
const ChannelMixin = {
/**
* 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;
},

/**
* Get a command by commandName
* @param commandName
* @returns {String} command
*/
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;
},

/**
* Send a message to the remote listener, expect no response
*
Expand All @@ -37,8 +70,8 @@ var ChannelMixin = {
* The promise will resolve if the value was successfully sent.
*/
send(message, data) {
var channel = this.getChannel();
var send = ensureActionReturnsPromise(channel.send.bind(channel));
const channel = this.getChannel();
const send = ensureActionReturnsPromise(channel.send.bind(channel));

return send(message, data);
},
Expand All @@ -53,10 +86,10 @@ var ChannelMixin = {
* listener, or reject if there was an error.
*/
request(message, data) {
var channel = this.getChannel();
const channel = this.getChannel();
// only new channels have a request. If not, fall back to send.
var action = (channel.request || channel.send).bind(channel);
var request = ensureActionReturnsPromise(action);
const action = (channel.request || channel.send).bind(channel);
const request = ensureActionReturnsPromise(action);

return request(message, data);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export default BaseAuthenticationBroker.extend({
DELAY_BROKER_RESPONSE_MS: 100,

sendOAuthResultToRelier(result) {
return this._metrics.flush().then(() => {
return Promise.resolve().then(() => {
var extraParams = {};
if (result.error) {
extraParams.error = result.error;
Expand Down Expand Up @@ -345,7 +345,10 @@ export default BaseAuthenticationBroker.extend({
this.clearOriginalTabMarker();
return this.getOAuthResult(account).then(result => {
result = _.extend(result, additionalResultData);
return this.sendOAuthResultToRelier(result);

return this._metrics.flush().then(() => {
return this.sendOAuthResultToRelier(result, account);
});
});
});
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/* 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);
}
},

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) {
result.redirect = Constants.OAUTH_WEBCHANNEL_REDIRECT;
if (account && account.get('declinedSyncEngines')) {
result.declinedSyncEngines = account.get('declinedSyncEngines');
result.offeredSyncEngines = account.get('offeredSyncEngines');
}

return this.send(this.getCommand('OAUTH_LOGIN'), result);
},
});

Cocktail.mixin(OAuthWebChannelBroker, ChannelMixin);

export default OAuthWebChannelBroker;
Loading

0 comments on commit 56971a5

Please sign in to comment.