Merged oauth and oauth-redirect brokers
This commit is contained in:
Родитель
0a91eb5406
Коммит
64dd80f965
|
@ -7,17 +7,28 @@
|
|||
define(function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
const _ = require('underscore');
|
||||
const AuthErrors = require('../../lib/auth-errors');
|
||||
const BaseAuthenticationBroker = require('../auth_brokers/base');
|
||||
const Constants = require('../../lib/constants');
|
||||
const HaltBehavior = require('../../views/behaviors/halt');
|
||||
const NullBehavior = require('../../views/behaviors/null');
|
||||
const OAuthAuthenticationBroker = require('../auth_brokers/oauth');
|
||||
const p = require('../../lib/promise');
|
||||
const NavigateBehavior = require('../../views/behaviors/navigate');
|
||||
const OAuthErrors = require('../../lib/oauth-errors');
|
||||
const p = require('../../lib/promise');
|
||||
const ScopedKeys = require('lib/crypto/scoped-keys');
|
||||
const Transform = require('../../lib/transform');
|
||||
const Url = require('../../lib/url');
|
||||
const Vat = require('../../lib/vat');
|
||||
const VerificationMethods = require('../../lib/verification-methods');
|
||||
const VerificationReasons = require('../../lib/verification-reasons');
|
||||
|
||||
const proto = OAuthAuthenticationBroker.prototype;
|
||||
const proto = BaseAuthenticationBroker.prototype;
|
||||
|
||||
const OAUTH_CODE_RESPONSE_SCHEMA = {
|
||||
code: Vat.oauthCode().required(),
|
||||
state: Vat.string()
|
||||
};
|
||||
/**
|
||||
* Invoke `brokerMethod` on the broker and finish the OAuth flow by
|
||||
* invoking `finishMethod` if verifying in the original tab. If verifying
|
||||
|
@ -49,13 +60,18 @@ define(function (require, exports, module) {
|
|||
};
|
||||
}
|
||||
|
||||
module.exports = OAuthAuthenticationBroker.extend({
|
||||
module.exports = BaseAuthenticationBroker.extend({
|
||||
type: 'redirect',
|
||||
|
||||
initialize (options) {
|
||||
options = options || {};
|
||||
|
||||
this.session = options.session;
|
||||
this._assertionLibrary = options.assertionLibrary;
|
||||
this._oAuthClient = options.oAuthClient;
|
||||
this._scopedKeys = ScopedKeys;
|
||||
this._metrics = options.metrics;
|
||||
|
||||
return proto.initialize.call(this, options);
|
||||
},
|
||||
|
||||
|
@ -76,6 +92,121 @@ define(function (require, exports, module) {
|
|||
});
|
||||
},
|
||||
|
||||
getOAuthResult (account) {
|
||||
if (! account || ! account.get('sessionToken')) {
|
||||
return Promise.reject(AuthErrors.toError('INVALID_TOKEN'));
|
||||
}
|
||||
let assertion;
|
||||
const relier = this.relier;
|
||||
const clientId = relier.get('clientId');
|
||||
return this._assertionLibrary.generate(account.get('sessionToken'), null, clientId)
|
||||
.then((asser) => {
|
||||
assertion = asser;
|
||||
|
||||
if (relier.wantsKeys()) {
|
||||
return this._provisionScopedKeys(account, assertion);
|
||||
}
|
||||
})
|
||||
.then((keysJwe) => {
|
||||
const oauthParams = {
|
||||
acr_values: relier.get('acrValues'), //eslint-disable-line camelcase
|
||||
assertion: assertion,
|
||||
client_id: clientId, //eslint-disable-line camelcase
|
||||
code_challenge: relier.get('codeChallenge'), //eslint-disable-line camelcase
|
||||
code_challenge_method: relier.get('codeChallengeMethod'), //eslint-disable-line camelcase
|
||||
keys_jwe: keysJwe, //eslint-disable-line camelcase
|
||||
scope: relier.get('scope'),
|
||||
state: relier.get('state')
|
||||
};
|
||||
|
||||
if (relier.get('accessType') === Constants.ACCESS_TYPE_OFFLINE) {
|
||||
oauthParams.access_type = Constants.ACCESS_TYPE_OFFLINE; //eslint-disable-line camelcase
|
||||
}
|
||||
return this._oAuthClient.getCode(oauthParams);
|
||||
})
|
||||
.then((response) => {
|
||||
if (! response) {
|
||||
return Promise.reject(OAuthErrors.toError('INVALID_RESULT'));
|
||||
}
|
||||
// The oauth-server would previously construct and return the full redirect URI,
|
||||
// but we now expect to receive `code` and `state` and build it ourselves
|
||||
// using the relier's locally-validated redirectUri.
|
||||
delete response.redirect;
|
||||
const result = Transform.transformUsingSchema(response, OAUTH_CODE_RESPONSE_SCHEMA, OAuthErrors);
|
||||
result.redirect = Url.updateSearchString(relier.get('redirectUri'), {
|
||||
code: result.code,
|
||||
state: result.state
|
||||
});
|
||||
return result;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Derive scoped keys and encrypt them with the relier's public JWK
|
||||
*
|
||||
* @param {Object} account
|
||||
* @param {String} assertion
|
||||
* @returns {Promise} Returns a promise that resolves into an encrypted bundle
|
||||
* @private
|
||||
*/
|
||||
_provisionScopedKeys (account, assertion) {
|
||||
const relier = this.relier;
|
||||
const uid = account.get('uid');
|
||||
|
||||
return Promise.resolve().then(() => {
|
||||
if (account.canFetchKeys()) {
|
||||
// check if requested scopes provide scoped keys
|
||||
return this._oAuthClient.getClientKeyData({
|
||||
assertion: assertion,
|
||||
client_id: relier.get('clientId'), //eslint-disable-line camelcase
|
||||
scope: relier.get('scope')
|
||||
});
|
||||
}
|
||||
}).then((clientKeyData) => {
|
||||
if (! clientKeyData || Object.keys(clientKeyData).length === 0) {
|
||||
// if we got no key data then exit out
|
||||
return null;
|
||||
}
|
||||
|
||||
return account.accountKeys().then((keys) => {
|
||||
return this._scopedKeys.createEncryptedBundle(keys, uid, clientKeyData, relier.get('keysJwk'));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Overridden by subclasses to provide a strategy to finish the OAuth flow.
|
||||
*
|
||||
* @param {Object} [result] - state sent by OAuth RP
|
||||
* @param {String} [result.state] - state sent by OAuth RP
|
||||
* @param {String} [result.code] - OAuth code generated by the OAuth server
|
||||
* @param {String} [result.redirect] - URL that can be used to redirect to
|
||||
* the RP.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
finishOAuthSignInFlow (account) {
|
||||
return this.finishOAuthFlow(account, { action: Constants.OAUTH_ACTION_SIGNIN });
|
||||
},
|
||||
|
||||
finishOAuthSignUpFlow (account) {
|
||||
return this.finishOAuthFlow(account, { action: Constants.OAUTH_ACTION_SIGNUP });
|
||||
},
|
||||
|
||||
transformLink (link) { //not used
|
||||
if (link[0] !== '/') {
|
||||
link = '/' + link;
|
||||
}
|
||||
|
||||
// in addition to named routes, also transforms `/`
|
||||
if (/^\/(force_auth|signin|signup)?$/.test(link)) {
|
||||
link = '/oauth' + link;
|
||||
}
|
||||
|
||||
const windowSearchParams = Url.searchParams(this.window.location.search);
|
||||
return Url.updateSearchString(link, windowSearchParams);
|
||||
},
|
||||
/**
|
||||
* Sets a marker used to determine if this is the tab a user
|
||||
* signed up or initiated a password reset in. If the user replaces
|
||||
|
@ -98,18 +229,33 @@ define(function (require, exports, module) {
|
|||
// If the user replaces the current tab with the verification url,
|
||||
// finish the OAuth flow.
|
||||
return Promise.resolve().then(() => {
|
||||
var relier = this.relier;
|
||||
this.session.set('oauth', {
|
||||
access_type: relier.get('access_type'), //eslint-disable-line camelcase
|
||||
action: relier.get('action'),
|
||||
client_id: relier.get('clientId'), //eslint-disable-line camelcase
|
||||
keys: relier.get('keys'),
|
||||
scope: relier.get('scope'),
|
||||
state: relier.get('state')
|
||||
});
|
||||
this.setOriginalTabMarker();
|
||||
return proto.persistVerificationData.call(this, account);
|
||||
});
|
||||
},
|
||||
|
||||
finishOAuthFlow (account, additionalResultData) {
|
||||
this.session.clear('oauth');
|
||||
|
||||
return Promise.resolve().then(() => {
|
||||
// There are no ill side effects if the Original Tab Marker is
|
||||
// cleared in the a tab other than the original. Always clear it just
|
||||
// to make sure the bases are covered.
|
||||
this.clearOriginalTabMarker();
|
||||
return proto.finishOAuthFlow.call(this, account, additionalResultData);
|
||||
return this.getOAuthResult(account)
|
||||
.then((result) => {
|
||||
result = _.extend(result, additionalResultData);
|
||||
return this.sendOAuthResultToRelier(result);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -1,248 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/**
|
||||
* A broker that knows how to finish an OAuth flow. Should be subclassed
|
||||
* to override `sendOAuthResultToRelier`
|
||||
*/
|
||||
|
||||
import _ from 'underscore';
|
||||
import AuthErrors from '../../lib/auth-errors';
|
||||
import BaseAuthenticationBroker from '../auth_brokers/base';
|
||||
import Constants from '../../lib/constants';
|
||||
import HaltBehavior from '../../views/behaviors/halt';
|
||||
import OAuthErrors from '../../lib/oauth-errors';
|
||||
import ScopedKeys from 'lib/crypto/scoped-keys';
|
||||
import Transform from '../../lib/transform';
|
||||
import Url from '../../lib/url';
|
||||
import Vat from '../../lib/vat';
|
||||
|
||||
const OAUTH_CODE_RESPONSE_SCHEMA = {
|
||||
code: Vat.oauthCode().required(),
|
||||
state: Vat.string()
|
||||
};
|
||||
|
||||
var proto = BaseAuthenticationBroker.prototype;
|
||||
|
||||
var OAuthAuthenticationBroker = BaseAuthenticationBroker.extend({
|
||||
type: 'oauth',
|
||||
|
||||
defaultBehaviors: _.extend({}, proto.defaultBehaviors, {
|
||||
// the relier will take over after sign in, no need to transition.
|
||||
afterForceAuth: new HaltBehavior(),
|
||||
afterSignIn: new HaltBehavior(),
|
||||
afterSignInConfirmationPoll: new HaltBehavior()
|
||||
}),
|
||||
|
||||
defaultCapabilities: _.extend({}, proto.defaultCapabilities, {
|
||||
// Disable signed-in notifications for OAuth due to the potential for
|
||||
// unintended consequences from redirecting to a relier URL more than
|
||||
// once.
|
||||
handleSignedInNotification: false,
|
||||
reuseExistingSession: true,
|
||||
tokenCode: true
|
||||
}),
|
||||
|
||||
initialize (options) {
|
||||
options = options || {};
|
||||
|
||||
this.session = options.session;
|
||||
this._assertionLibrary = options.assertionLibrary;
|
||||
this._oAuthClient = options.oAuthClient;
|
||||
this._scopedKeys = ScopedKeys;
|
||||
|
||||
return BaseAuthenticationBroker.prototype.initialize.call(
|
||||
this, options);
|
||||
},
|
||||
|
||||
getOAuthResult (account) {
|
||||
if (! account || ! account.get('sessionToken')) {
|
||||
return Promise.reject(AuthErrors.toError('INVALID_TOKEN'));
|
||||
}
|
||||
let assertion;
|
||||
const relier = this.relier;
|
||||
const clientId = relier.get('clientId');
|
||||
return this._assertionLibrary.generate(account.get('sessionToken'), null, clientId)
|
||||
.then((asser) => {
|
||||
assertion = asser;
|
||||
|
||||
if (relier.wantsKeys()) {
|
||||
return this._provisionScopedKeys(account, assertion);
|
||||
}
|
||||
})
|
||||
.then((keysJwe) => {
|
||||
const oauthParams = {
|
||||
acr_values: relier.get('acrValues'), //eslint-disable-line camelcase
|
||||
assertion: assertion,
|
||||
client_id: clientId, //eslint-disable-line camelcase
|
||||
code_challenge: relier.get('codeChallenge'), //eslint-disable-line camelcase
|
||||
code_challenge_method: relier.get('codeChallengeMethod'), //eslint-disable-line camelcase
|
||||
keys_jwe: keysJwe, //eslint-disable-line camelcase
|
||||
scope: relier.get('scope'),
|
||||
state: relier.get('state')
|
||||
};
|
||||
|
||||
if (relier.get('accessType') === Constants.ACCESS_TYPE_OFFLINE) {
|
||||
oauthParams.access_type = Constants.ACCESS_TYPE_OFFLINE; //eslint-disable-line camelcase
|
||||
}
|
||||
return this._oAuthClient.getCode(oauthParams);
|
||||
})
|
||||
.then((response) => {
|
||||
if (! response) {
|
||||
return Promise.reject(OAuthErrors.toError('INVALID_RESULT'));
|
||||
}
|
||||
// The oauth-server would previously construct and return the full redirect URI,
|
||||
// but we now expect to receive `code` and `state` and build it ourselves
|
||||
// using the relier's locally-validated redirectUri.
|
||||
delete response.redirect;
|
||||
const result = Transform.transformUsingSchema(response, OAUTH_CODE_RESPONSE_SCHEMA, OAuthErrors);
|
||||
result.redirect = Url.updateSearchString(relier.get('redirectUri'), {
|
||||
code: result.code,
|
||||
state: result.state
|
||||
});
|
||||
return result;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Derive scoped keys and encrypt them with the relier's public JWK
|
||||
*
|
||||
* @param {Object} account
|
||||
* @param {String} assertion
|
||||
* @returns {Promise} Returns a promise that resolves into an encrypted bundle
|
||||
* @private
|
||||
*/
|
||||
_provisionScopedKeys (account, assertion) {
|
||||
const relier = this.relier;
|
||||
const uid = account.get('uid');
|
||||
|
||||
return Promise.resolve().then(() => {
|
||||
if (account.canFetchKeys()) {
|
||||
// check if requested scopes provide scoped keys
|
||||
return this._oAuthClient.getClientKeyData({
|
||||
assertion: assertion,
|
||||
client_id: relier.get('clientId'), //eslint-disable-line camelcase
|
||||
scope: relier.get('scope')
|
||||
});
|
||||
}
|
||||
}).then((clientKeyData) => {
|
||||
if (! clientKeyData || Object.keys(clientKeyData).length === 0) {
|
||||
// if we got no key data then exit out
|
||||
return null;
|
||||
}
|
||||
|
||||
return account.accountKeys().then((keys) => {
|
||||
return this._scopedKeys.createEncryptedBundle(keys, uid, clientKeyData, relier.get('keysJwk'));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Overridden by subclasses to provide a strategy to finish the OAuth flow.
|
||||
*
|
||||
* @param {Object} [result] - state sent by OAuth RP
|
||||
* @param {String} [result.state] - state sent by OAuth RP
|
||||
* @param {String} [result.code] - OAuth code generated by the OAuth server
|
||||
* @param {String} [result.redirect] - URL that can be used to redirect to
|
||||
* the RP.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
sendOAuthResultToRelier (/*result*/) {
|
||||
return Promise.reject(new Error('subclasses must override sendOAuthResultToRelier'));
|
||||
},
|
||||
|
||||
finishOAuthSignInFlow (account) {
|
||||
return this.finishOAuthFlow(account, { action: Constants.OAUTH_ACTION_SIGNIN });
|
||||
},
|
||||
|
||||
finishOAuthSignUpFlow (account) {
|
||||
return this.finishOAuthFlow(account, { action: Constants.OAUTH_ACTION_SIGNUP });
|
||||
},
|
||||
|
||||
finishOAuthFlow (account, additionalResultData = {}) {
|
||||
this.session.clear('oauth');
|
||||
|
||||
return this.getOAuthResult(account)
|
||||
.then((result) => {
|
||||
result = _.extend(result, additionalResultData);
|
||||
return this.sendOAuthResultToRelier(result);
|
||||
});
|
||||
},
|
||||
|
||||
persistVerificationData (account) {
|
||||
return Promise.resolve().then(() => {
|
||||
var relier = this.relier;
|
||||
this.session.set('oauth', {
|
||||
access_type: relier.get('access_type'), //eslint-disable-line camelcase
|
||||
action: relier.get('action'),
|
||||
client_id: relier.get('clientId'), //eslint-disable-line camelcase
|
||||
keys: relier.get('keys'),
|
||||
scope: relier.get('scope'),
|
||||
state: relier.get('state')
|
||||
});
|
||||
|
||||
return proto.persistVerificationData.call(this, account);
|
||||
});
|
||||
},
|
||||
|
||||
afterForceAuth (account) {
|
||||
return this.finishOAuthSignInFlow(account)
|
||||
.then(() => proto.afterForceAuth.call(this, account));
|
||||
},
|
||||
|
||||
afterSignIn (account) {
|
||||
return this.finishOAuthSignInFlow(account)
|
||||
.then(() => proto.afterSignIn.call(this, account));
|
||||
},
|
||||
|
||||
afterSignInConfirmationPoll (account) {
|
||||
return this.finishOAuthSignInFlow(account)
|
||||
.then(() => proto.afterSignInConfirmationPoll.call(this, account));
|
||||
},
|
||||
|
||||
afterCompleteSignInWithCode (account) {
|
||||
return this.finishOAuthSignInFlow(account)
|
||||
.then(() => proto.afterSignIn.call(this, account));
|
||||
},
|
||||
|
||||
afterSignUpConfirmationPoll (account) {
|
||||
// The original tab always finishes the OAuth flow if it is still open.
|
||||
|
||||
// Check to see if ths relier wants TOTP. Newly created accounts wouldn't have this
|
||||
// so lets redirect them to signin and show a message on how it can be setup.
|
||||
// This is temporary until we have a better landing page for this error.
|
||||
if (this.relier.wantsTwoStepAuthentication()) {
|
||||
return this.getBehavior('afterSignUpRequireTOTP');
|
||||
}
|
||||
|
||||
return this.finishOAuthSignUpFlow(account);
|
||||
},
|
||||
|
||||
afterResetPasswordConfirmationPoll (account) {
|
||||
return Promise.resolve().then(() => {
|
||||
if (account.get('verified') && ! account.get('verificationReason') && ! account.get('verificationMethod')) {
|
||||
return this.finishOAuthSignInFlow(account);
|
||||
} else {
|
||||
return proto.afterResetPasswordConfirmationPoll.call(this, account);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
transformLink (link) {
|
||||
if (link[0] !== '/') {
|
||||
link = '/' + link;
|
||||
}
|
||||
|
||||
// in addition to named routes, also transforms `/`
|
||||
if (/^\/(force_auth|signin|signup)?$/.test(link)) {
|
||||
link = '/oauth' + link;
|
||||
}
|
||||
|
||||
const windowSearchParams = Url.searchParams(this.window.location.search);
|
||||
return Url.updateSearchString(link, windowSearchParams);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = OAuthAuthenticationBroker;
|
|
@ -5,8 +5,13 @@
|
|||
define(function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
const Account = require('models/account');
|
||||
const { assert } = require('chai');
|
||||
const Assertion = require('lib/assertion');
|
||||
const AuthErrors = require('lib/auth-errors');
|
||||
const Constants = require('lib/constants');
|
||||
const OAuthClient = require('lib/oauth-client');
|
||||
const OAuthErrors = require('lib/oauth-errors');
|
||||
const RedirectAuthenticationBroker = require('models/auth_brokers/oauth-redirect');
|
||||
const Relier = require('models/reliers/base');
|
||||
const Session = require('lib/session');
|
||||
|
@ -18,28 +23,69 @@ define(function (require, exports, module) {
|
|||
|
||||
var REDIRECT_TO = 'https://redirect.here';
|
||||
|
||||
|
||||
var HEX_CHARSET = '0123456789abcdef';
|
||||
function generateOAuthCode() {
|
||||
var code = '';
|
||||
|
||||
for (var i = 0; i < 64; ++i) {
|
||||
code += HEX_CHARSET.charAt(Math.floor(Math.random() * 16));
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
const REDIRECT_URI = 'https://127.0.0.1:8080';
|
||||
const VALID_OAUTH_CODE = generateOAuthCode();
|
||||
const VALID_OAUTH_CODE_REDIRECT_URL = `${REDIRECT_URI}?code=${VALID_OAUTH_CODE}&state=state`;
|
||||
|
||||
describe('models/auth_brokers/redirect', () => {
|
||||
var account;
|
||||
var assertionLibrary;
|
||||
var broker;
|
||||
var metrics;
|
||||
var oAuthClient;
|
||||
var relier;
|
||||
var user;
|
||||
var windowMock;
|
||||
|
||||
beforeEach(() => {
|
||||
oAuthClient = new OAuthClient();
|
||||
sinon.stub(oAuthClient, 'getCode').callsFake(function () {
|
||||
return Promise.resolve({
|
||||
code: VALID_OAUTH_CODE,
|
||||
redirect: VALID_OAUTH_CODE_REDIRECT_URL,
|
||||
state: 'state'
|
||||
});
|
||||
});
|
||||
|
||||
assertionLibrary = new Assertion({});
|
||||
sinon.stub(assertionLibrary, 'generate').callsFake(function () {
|
||||
return Promise.resolve('assertion');
|
||||
});
|
||||
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();
|
||||
|
||||
account = user.initAccount({
|
||||
sessionToken: 'abc123'
|
||||
});
|
||||
broker = new RedirectAuthenticationBroker({
|
||||
assertionLibrary: assertionLibrary,
|
||||
metrics: metrics,
|
||||
oAuthClient: oAuthClient,
|
||||
relier: relier,
|
||||
session: Session,
|
||||
window: windowMock
|
||||
|
@ -49,14 +95,15 @@ define(function (require, exports, module) {
|
|||
sinon.stub(broker, 'finishOAuthFlow').callsFake(() => {
|
||||
return Promise.resolve();
|
||||
});
|
||||
// sinon.spy(broker, 'finishOAuthFlow');
|
||||
});
|
||||
|
||||
it('has the `signup` capability by default', () => {
|
||||
assert.isTrue(broker.hasCapability('signup'));
|
||||
});
|
||||
|
||||
it('does not have the `handleSignedInNotification` capability by default', () => {
|
||||
assert.isFalse(broker.hasCapability('handleSignedInNotification'));
|
||||
it('have the `handleSignedInNotification` capability by default', () => {
|
||||
assert.isTrue(broker.hasCapability('handleSignedInNotification'));
|
||||
});
|
||||
|
||||
it('has the `emailVerificationMarketingSnippet` capability by default', () => {
|
||||
|
@ -122,14 +169,246 @@ define(function (require, exports, module) {
|
|||
});
|
||||
|
||||
describe('persistVerificationData', () => {
|
||||
it('sets the Original Tab marker', () => {
|
||||
it('sets the Original Tab marker and saves OAuth params to session', () => {
|
||||
return broker.persistVerificationData(account)
|
||||
.then(() => {
|
||||
.then(function () {
|
||||
assert.ok(!! Session.oauth);
|
||||
assert.isTrue(broker.isOriginalTab());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOAuthResult', function () {
|
||||
it('gets an object with the OAuth login information', function () {
|
||||
return broker.getOAuthResult(account)
|
||||
.then(function (result) {
|
||||
assert.isTrue(assertionLibrary.generate.calledWith(account.get('sessionToken'), null, 'clientId'));
|
||||
assert.equal(result.redirect, VALID_OAUTH_CODE_REDIRECT_URL);
|
||||
assert.equal(result.state, 'state');
|
||||
assert.equal(result.code, VALID_OAUTH_CODE);
|
||||
});
|
||||
});
|
||||
|
||||
it('locally constructs the redirect URI, ignoring any provided by the server', function () {
|
||||
oAuthClient.getCode.restore();
|
||||
sinon.stub(oAuthClient, 'getCode').callsFake(function () {
|
||||
return Promise.resolve({
|
||||
code: VALID_OAUTH_CODE,
|
||||
redirect: 'https://the.server.got.confused',
|
||||
state: 'state'
|
||||
});
|
||||
});
|
||||
return broker.getOAuthResult(account)
|
||||
.then(function (result) {
|
||||
assert.isTrue(oAuthClient.getCode.calledOnce);
|
||||
assert.equal(result.redirect, VALID_OAUTH_CODE_REDIRECT_URL);
|
||||
assert.equal(result.state, 'state');
|
||||
assert.equal(result.code, VALID_OAUTH_CODE);
|
||||
});
|
||||
});
|
||||
|
||||
it('passes on errors from assertion generation', function () {
|
||||
assertionLibrary.generate.restore();
|
||||
sinon.stub(assertionLibrary, 'generate').callsFake(function () {
|
||||
return Promise.reject(new Error('uh oh'));
|
||||
});
|
||||
|
||||
return broker.getOAuthResult(account)
|
||||
.then(assert.fail, function (err) {
|
||||
assert.equal(err.message, 'uh oh');
|
||||
});
|
||||
});
|
||||
|
||||
it('passes on errors from oAuthClient.getCode', function () {
|
||||
oAuthClient.getCode.restore();
|
||||
sinon.stub(oAuthClient, 'getCode').callsFake(function () {
|
||||
return Promise.reject(new Error('uh oh'));
|
||||
});
|
||||
|
||||
return broker.getOAuthResult(account)
|
||||
.then(assert.fail, function (err) {
|
||||
assert.equal(err.message, 'uh oh');
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if oAuthClient.getCode returns nothing', function () {
|
||||
oAuthClient.getCode.restore();
|
||||
sinon.stub(oAuthClient, 'getCode').callsFake(function () {
|
||||
return;
|
||||
});
|
||||
|
||||
return broker.getOAuthResult(account)
|
||||
.then(assert.fail, function (err) {
|
||||
assert.isTrue(OAuthErrors.is(err, 'INVALID_RESULT'));
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if oAuthClient.getCode returns an empty object', function () {
|
||||
oAuthClient.getCode.restore();
|
||||
sinon.stub(oAuthClient, 'getCode').callsFake(function () {
|
||||
return {};
|
||||
});
|
||||
|
||||
return broker.getOAuthResult(account)
|
||||
.then(assert.fail, function (err) {
|
||||
assert.isTrue(OAuthErrors.is(err, 'MISSING_PARAMETER'));
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if oAuthClient.getCode returns an invalid code', function () {
|
||||
oAuthClient.getCode.restore();
|
||||
sinon.stub(oAuthClient, 'getCode').callsFake(function () {
|
||||
return {
|
||||
code: 'invalid-code'
|
||||
};
|
||||
});
|
||||
|
||||
return broker.getOAuthResult(account)
|
||||
.then(assert.fail, function (err) {
|
||||
assert.isTrue(OAuthErrors.is(err, 'INVALID_PARAMETER'));
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if oAuthClient.getCode returns an invalid state', function () {
|
||||
oAuthClient.getCode.restore();
|
||||
sinon.stub(oAuthClient, 'getCode').callsFake(function () {
|
||||
return {
|
||||
code: VALID_OAUTH_CODE,
|
||||
state: { invalid: 'state' }
|
||||
};
|
||||
});
|
||||
|
||||
return broker.getOAuthResult(account)
|
||||
.then(assert.fail, function (err) {
|
||||
assert.isTrue(OAuthErrors.is(err, 'INVALID_PARAMETER'));
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if accountData is missing', function () {
|
||||
return broker.getOAuthResult()
|
||||
.then(assert.fail, function (err) {
|
||||
assert.isTrue(AuthErrors.is(err, 'INVALID_TOKEN'));
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if accountData is missing a sessionToken', function () {
|
||||
return broker.getOAuthResult(user.initAccount())
|
||||
.then(assert.fail, function (err) {
|
||||
assert.isTrue(AuthErrors.is(err, 'INVALID_TOKEN'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformLink', () => {
|
||||
[
|
||||
'force_auth',
|
||||
'signin',
|
||||
'signup',
|
||||
].forEach(route => {
|
||||
describe(`${route}`, () => {
|
||||
it('prepends `/oauth` to the link', () => {
|
||||
assert.include(broker.transformLink(`/${route}`), `/oauth/${route}`);
|
||||
assert.include(broker.transformLink(`${route}`), `/oauth/${route}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/', () => {
|
||||
it('prepends `/oauth` to the link', () => {
|
||||
assert.include(broker.transformLink('/'), '/oauth/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('not transformed', () => {
|
||||
it('does not include the oauth prefix', () => {
|
||||
const transformed = broker.transformLink('not_oauth');
|
||||
assert.notInclude(transformed, 'oauth/not_oauth');
|
||||
assert.include(transformed, 'not_oauth');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_provisionScopedKeys', () => {
|
||||
let accountKey;
|
||||
const keysJwk = 'jwk';
|
||||
const keys = {
|
||||
kA: 'foo',
|
||||
kB: 'bar'
|
||||
};
|
||||
const scope = 'https://identity.mozilla.com/apps/sample-scope-can-scope-key';
|
||||
const keyData = {
|
||||
[scope]: {
|
||||
identifier: scope,
|
||||
keyRotationSecret: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
keyRotationTimestamp: 1506970363512
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(broker._oAuthClient, 'getClientKeyData').callsFake((args) => {
|
||||
assert.equal(args.assertion, 'assertion');
|
||||
assert.equal(args.client_id, 'clientId');
|
||||
assert.equal(args.scope, 'scope');
|
||||
|
||||
return Promise.resolve(keyData);
|
||||
});
|
||||
|
||||
accountKey = new Account({
|
||||
email: 'testuser@testuser.com',
|
||||
keyFetchToken: 'key-fetch-token',
|
||||
uid: 'uid',
|
||||
unwrapBKey: 'unwrap-b-key'
|
||||
});
|
||||
|
||||
relier.set('keysJwk', keysJwk);
|
||||
|
||||
sinon.stub(accountKey, 'accountKeys').callsFake((args) => {
|
||||
return Promise.resolve(keys);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls _provisionScopedKeys to encrypt the bundle', () => {
|
||||
relier.set('keysJwk', keysJwk);
|
||||
|
||||
sinon.stub(broker._scopedKeys, 'createEncryptedBundle').callsFake((_keys, _uid, _keyData, _jwk) => {
|
||||
assert.equal(_keys, keys);
|
||||
assert.equal(_uid, 'uid');
|
||||
assert.equal(_keyData, keyData);
|
||||
assert.equal(_jwk, keysJwk);
|
||||
|
||||
return Promise.resolve('bundle');
|
||||
});
|
||||
|
||||
return broker._provisionScopedKeys(accountKey, 'assertion')
|
||||
.then((result) => {
|
||||
assert.isTrue(broker._scopedKeys.createEncryptedBundle.calledOnce);
|
||||
assert.equal(result, 'bundle');
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null if no unwrapBKey', () => {
|
||||
accountKey.set('unwrapBKey', null);
|
||||
|
||||
return broker._provisionScopedKeys(accountKey, 'assertion')
|
||||
.then((result) => {
|
||||
assert.equal(result, null);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null if no clientKeyData', () => {
|
||||
broker._oAuthClient.getClientKeyData.restore();
|
||||
sinon.stub(broker._oAuthClient, 'getClientKeyData').callsFake((args) => {
|
||||
return Promise.resolve({});
|
||||
});
|
||||
|
||||
return broker._provisionScopedKeys(accountKey, 'assertion')
|
||||
.then((result) => {
|
||||
assert.equal(result, null);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('finishOAuthFlow', () => {
|
||||
it('clears the original tab marker', () => {
|
||||
broker.finishOAuthFlow.restore();
|
||||
|
@ -216,7 +495,7 @@ define(function (require, exports, module) {
|
|||
return broker.afterCompleteResetPassword(account)
|
||||
.then((behavior) => {
|
||||
assert.isFalse(broker.finishOAuthFlow.called);
|
||||
assert.equal(behavior.type, 'null');
|
||||
assert.equal(behavior.type, 'navigate');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -230,7 +509,7 @@ define(function (require, exports, module) {
|
|||
return broker.afterCompleteResetPassword(account)
|
||||
.then((behavior) => {
|
||||
assert.isFalse(broker.finishOAuthFlow.called);
|
||||
assert.equal(behavior.type, 'null');
|
||||
assert.equal(behavior.type, 'navigate');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,504 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Account from 'models/account';
|
||||
import { assert } from 'chai';
|
||||
import Assertion from 'lib/assertion';
|
||||
import AuthErrors from 'lib/auth-errors';
|
||||
import Constants from 'lib/constants';
|
||||
import OAuthAuthenticationBroker from 'models/auth_brokers/oauth';
|
||||
import OAuthClient from 'lib/oauth-client';
|
||||
import OAuthErrors from 'lib/oauth-errors';
|
||||
import Relier from 'models/reliers/relier';
|
||||
import Session from 'lib/session';
|
||||
import sinon from 'sinon';
|
||||
import User from 'models/user';
|
||||
import VerificationMethods from 'lib/verification-methods';
|
||||
import VerificationReasons from 'lib/verification-reasons';
|
||||
|
||||
var HEX_CHARSET = '0123456789abcdef';
|
||||
function generateOAuthCode() {
|
||||
var code = '';
|
||||
|
||||
for (var i = 0; i < 64; ++i) {
|
||||
code += HEX_CHARSET.charAt(Math.floor(Math.random() * 16));
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
const REDIRECT_URI = 'https://127.0.0.1:8080';
|
||||
const VALID_OAUTH_CODE = generateOAuthCode();
|
||||
const VALID_OAUTH_CODE_REDIRECT_URL = `${REDIRECT_URI}?code=${VALID_OAUTH_CODE}&state=state`;
|
||||
|
||||
describe('models/auth_brokers/oauth', function () {
|
||||
var account;
|
||||
var assertionLibrary;
|
||||
var broker;
|
||||
var oAuthClient;
|
||||
var relier;
|
||||
var user;
|
||||
|
||||
beforeEach(function () {
|
||||
oAuthClient = new OAuthClient();
|
||||
sinon.stub(oAuthClient, 'getCode').callsFake(function () {
|
||||
return Promise.resolve({
|
||||
code: VALID_OAUTH_CODE,
|
||||
redirect: VALID_OAUTH_CODE_REDIRECT_URL,
|
||||
state: 'state'
|
||||
});
|
||||
});
|
||||
|
||||
assertionLibrary = new Assertion({});
|
||||
sinon.stub(assertionLibrary, 'generate').callsFake(function () {
|
||||
return Promise.resolve('assertion');
|
||||
});
|
||||
|
||||
relier = new Relier();
|
||||
relier.set({
|
||||
action: 'action',
|
||||
clientId: 'clientId',
|
||||
redirectUri: REDIRECT_URI,
|
||||
scope: 'scope',
|
||||
state: 'state'
|
||||
});
|
||||
|
||||
user = new User();
|
||||
|
||||
account = user.initAccount({
|
||||
sessionToken: 'abc123'
|
||||
});
|
||||
|
||||
broker = new OAuthAuthenticationBroker({
|
||||
assertionLibrary: assertionLibrary,
|
||||
oAuthClient: oAuthClient,
|
||||
relier: relier,
|
||||
session: Session
|
||||
});
|
||||
|
||||
sinon.spy(broker, 'finishOAuthFlow');
|
||||
});
|
||||
|
||||
it('has the `signup` capability by default', function () {
|
||||
assert.isTrue(broker.hasCapability('signup'));
|
||||
});
|
||||
|
||||
it('has the `reuseExistingSession` capability by default', () => {
|
||||
assert.isTrue(broker.hasCapability('reuseExistingSession'));
|
||||
});
|
||||
|
||||
it('does not have the `handleSignedInNotification` capability by default', function () {
|
||||
assert.isFalse(broker.hasCapability('handleSignedInNotification'));
|
||||
});
|
||||
|
||||
it('has the `emailVerificationMarketingSnippet` capability by default', function () {
|
||||
assert.isTrue(broker.hasCapability('emailVerificationMarketingSnippet'));
|
||||
});
|
||||
|
||||
describe('sendOAuthResultToRelier', function () {
|
||||
it('must be overridden', function () {
|
||||
return broker.sendOAuthResultToRelier()
|
||||
.then(assert.fail, function (err) {
|
||||
assert.ok(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('afterSignInConfirmationPoll', () => {
|
||||
it('calls sendOAuthResultToRelier with the correct options', () => {
|
||||
sinon.stub(broker, 'sendOAuthResultToRelier').callsFake(() => {
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
return broker.afterSignInConfirmationPoll(account)
|
||||
.then((behavior) => {
|
||||
assert.isTrue(broker.finishOAuthFlow.calledWith(account, {
|
||||
action: Constants.OAUTH_ACTION_SIGNIN
|
||||
}));
|
||||
assert.isTrue(broker.sendOAuthResultToRelier.calledWith({
|
||||
action: Constants.OAUTH_ACTION_SIGNIN,
|
||||
code: VALID_OAUTH_CODE,
|
||||
redirect: VALID_OAUTH_CODE_REDIRECT_URL,
|
||||
state: 'state'
|
||||
}));
|
||||
// The Hello window will close the screen, no need to transition
|
||||
assert.isTrue(behavior.halt);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns any errors returned by getOAuthResult', () => {
|
||||
sinon.stub(broker, 'getOAuthResult').callsFake(() => {
|
||||
return Promise.reject(new Error('uh oh'));
|
||||
});
|
||||
|
||||
return broker.afterSignInConfirmationPoll(account)
|
||||
.then(assert.fail, (err) => {
|
||||
assert.equal(err.message, 'uh oh');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('afterSignIn', function () {
|
||||
it('calls sendOAuthResultToRelier with the correct options', function () {
|
||||
sinon.stub(broker, 'sendOAuthResultToRelier').callsFake(function () {
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
return broker.afterSignIn(account)
|
||||
.then(function () {
|
||||
assert.isTrue(broker.finishOAuthFlow.calledWith(account, {
|
||||
action: Constants.OAUTH_ACTION_SIGNIN
|
||||
}));
|
||||
assert.isTrue(broker.sendOAuthResultToRelier.calledWith({
|
||||
action: Constants.OAUTH_ACTION_SIGNIN,
|
||||
code: VALID_OAUTH_CODE,
|
||||
redirect: VALID_OAUTH_CODE_REDIRECT_URL,
|
||||
state: 'state'
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
it('returns any errors returned by getOAuthResult', function () {
|
||||
sinon.stub(broker, 'getOAuthResult').callsFake(function () {
|
||||
return Promise.reject(new Error('uh oh'));
|
||||
});
|
||||
|
||||
return broker.afterSignIn(account)
|
||||
.then(assert.fail, function (err) {
|
||||
assert.equal(err.message, 'uh oh');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('persistVerificationData', function () {
|
||||
it('saves OAuth params to session', function () {
|
||||
return broker.persistVerificationData(account)
|
||||
.then(function () {
|
||||
assert.ok(!! Session.oauth);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('afterSignUpConfirmationPoll', function () {
|
||||
describe('relier requires TOTP', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(relier, 'wantsTwoStepAuthentication').callsFake(() => {
|
||||
return true;
|
||||
});
|
||||
sinon.spy(broker, 'getBehavior');
|
||||
return broker.afterSignUpConfirmationPoll(account);
|
||||
});
|
||||
|
||||
it('calls afterSignUpRequireTOTP', () => {
|
||||
assert.equal(broker.getBehavior.callCount, 1);
|
||||
assert.equal(broker.getBehavior.args[0][0], 'afterSignUpRequireTOTP');
|
||||
});
|
||||
});
|
||||
|
||||
it('calls sendOAuthResultToRelier with the correct options', function () {
|
||||
sinon.stub(broker, 'sendOAuthResultToRelier').callsFake(function () {
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
return broker.afterSignUpConfirmationPoll(account)
|
||||
.then(function () {
|
||||
assert.isTrue(broker.finishOAuthFlow.calledWith(account, {
|
||||
action: Constants.OAUTH_ACTION_SIGNUP
|
||||
}));
|
||||
assert.isTrue(broker.sendOAuthResultToRelier.calledWith({
|
||||
action: Constants.OAUTH_ACTION_SIGNUP,
|
||||
code: VALID_OAUTH_CODE,
|
||||
redirect: VALID_OAUTH_CODE_REDIRECT_URL,
|
||||
state: 'state'
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('afterResetPasswordConfirmationPoll', function () {
|
||||
describe('with a verified account', () => {
|
||||
it('calls sendOAuthResultToRelier with the expected options', function () {
|
||||
account.set('verified', true);
|
||||
sinon.stub(broker, 'sendOAuthResultToRelier').callsFake(() => Promise.resolve());
|
||||
|
||||
return broker.afterResetPasswordConfirmationPoll(account)
|
||||
.then(() => {
|
||||
assert.isTrue(broker.finishOAuthFlow.calledWith(account, {
|
||||
action: Constants.OAUTH_ACTION_SIGNIN
|
||||
}));
|
||||
assert.isTrue(broker.sendOAuthResultToRelier.calledWith({
|
||||
action: Constants.OAUTH_ACTION_SIGNIN,
|
||||
code: VALID_OAUTH_CODE,
|
||||
redirect: VALID_OAUTH_CODE_REDIRECT_URL,
|
||||
state: 'state'
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unverified session that requires TOTP', () => {
|
||||
it('asks the user to enter a TOTP code', () => {
|
||||
account.set({
|
||||
verificationMethod: VerificationMethods.TOTP_2FA,
|
||||
verificationReason: VerificationReasons.SIGN_IN,
|
||||
verified: false
|
||||
});
|
||||
|
||||
return broker.afterResetPasswordConfirmationPoll(account)
|
||||
.then((behavior) => {
|
||||
assert.isFalse(broker.finishOAuthFlow.called);
|
||||
assert.equal(behavior.type, 'navigate');
|
||||
assert.equal(behavior.endpoint, 'signin_totp_code');
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores account `verified` if verification method and reason set', function () {
|
||||
account.set({
|
||||
verificationMethod: VerificationMethods.TOTP_2FA,
|
||||
verificationReason: VerificationReasons.SIGN_IN,
|
||||
verified: true
|
||||
});
|
||||
|
||||
return broker.afterResetPasswordConfirmationPoll(account)
|
||||
.then((behavior) => {
|
||||
assert.isFalse(broker.finishOAuthFlow.called);
|
||||
assert.equal(behavior.type, 'navigate');
|
||||
assert.equal(behavior.endpoint, 'signin_totp_code');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOAuthResult', function () {
|
||||
it('gets an object with the OAuth login information', function () {
|
||||
return broker.getOAuthResult(account)
|
||||
.then(function (result) {
|
||||
assert.isTrue(assertionLibrary.generate.calledWith(account.get('sessionToken'), null, 'clientId'));
|
||||
assert.equal(result.redirect, VALID_OAUTH_CODE_REDIRECT_URL);
|
||||
assert.equal(result.state, 'state');
|
||||
assert.equal(result.code, VALID_OAUTH_CODE);
|
||||
});
|
||||
});
|
||||
|
||||
it('locally constructs the redirect URI, ignoring any provided by the server', function () {
|
||||
oAuthClient.getCode.restore();
|
||||
sinon.stub(oAuthClient, 'getCode').callsFake(function () {
|
||||
return Promise.resolve({
|
||||
code: VALID_OAUTH_CODE,
|
||||
redirect: 'https://the.server.got.confused',
|
||||
state: 'state'
|
||||
});
|
||||
});
|
||||
return broker.getOAuthResult(account)
|
||||
.then(function (result) {
|
||||
assert.isTrue(oAuthClient.getCode.calledOnce);
|
||||
assert.equal(result.redirect, VALID_OAUTH_CODE_REDIRECT_URL);
|
||||
assert.equal(result.state, 'state');
|
||||
assert.equal(result.code, VALID_OAUTH_CODE);
|
||||
});
|
||||
});
|
||||
|
||||
it('passes on errors from assertion generation', function () {
|
||||
assertionLibrary.generate.restore();
|
||||
sinon.stub(assertionLibrary, 'generate').callsFake(function () {
|
||||
return Promise.reject(new Error('uh oh'));
|
||||
});
|
||||
|
||||
return broker.getOAuthResult(account)
|
||||
.then(assert.fail, function (err) {
|
||||
assert.equal(err.message, 'uh oh');
|
||||
});
|
||||
});
|
||||
|
||||
it('passes on errors from oAuthClient.getCode', function () {
|
||||
oAuthClient.getCode.restore();
|
||||
sinon.stub(oAuthClient, 'getCode').callsFake(function () {
|
||||
return Promise.reject(new Error('uh oh'));
|
||||
});
|
||||
|
||||
return broker.getOAuthResult(account)
|
||||
.then(assert.fail, function (err) {
|
||||
assert.equal(err.message, 'uh oh');
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if oAuthClient.getCode returns nothing', function () {
|
||||
oAuthClient.getCode.restore();
|
||||
sinon.stub(oAuthClient, 'getCode').callsFake(function () {
|
||||
return;
|
||||
});
|
||||
|
||||
return broker.getOAuthResult(account)
|
||||
.then(assert.fail, function (err) {
|
||||
assert.isTrue(OAuthErrors.is(err, 'INVALID_RESULT'));
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if oAuthClient.getCode returns an empty object', function () {
|
||||
oAuthClient.getCode.restore();
|
||||
sinon.stub(oAuthClient, 'getCode').callsFake(function () {
|
||||
return {};
|
||||
});
|
||||
|
||||
return broker.getOAuthResult(account)
|
||||
.then(assert.fail, function (err) {
|
||||
assert.isTrue(OAuthErrors.is(err, 'MISSING_PARAMETER'));
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if oAuthClient.getCode returns an invalid code', function () {
|
||||
oAuthClient.getCode.restore();
|
||||
sinon.stub(oAuthClient, 'getCode').callsFake(function () {
|
||||
return {
|
||||
code: 'invalid-code'
|
||||
};
|
||||
});
|
||||
|
||||
return broker.getOAuthResult(account)
|
||||
.then(assert.fail, function (err) {
|
||||
assert.isTrue(OAuthErrors.is(err, 'INVALID_PARAMETER'));
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if oAuthClient.getCode returns an invalid state', function () {
|
||||
oAuthClient.getCode.restore();
|
||||
sinon.stub(oAuthClient, 'getCode').callsFake(function () {
|
||||
return {
|
||||
code: VALID_OAUTH_CODE,
|
||||
state: { invalid: 'state' }
|
||||
};
|
||||
});
|
||||
|
||||
return broker.getOAuthResult(account)
|
||||
.then(assert.fail, function (err) {
|
||||
assert.isTrue(OAuthErrors.is(err, 'INVALID_PARAMETER'));
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if accountData is missing', function () {
|
||||
return broker.getOAuthResult()
|
||||
.then(assert.fail, function (err) {
|
||||
assert.isTrue(AuthErrors.is(err, 'INVALID_TOKEN'));
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if accountData is missing a sessionToken', function () {
|
||||
return broker.getOAuthResult(user.initAccount())
|
||||
.then(assert.fail, function (err) {
|
||||
assert.isTrue(AuthErrors.is(err, 'INVALID_TOKEN'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformLink', () => {
|
||||
[
|
||||
'force_auth',
|
||||
'signin',
|
||||
'signup',
|
||||
].forEach(route => {
|
||||
describe(`${route}`, () => {
|
||||
it('prepends `/oauth` to the link', () => {
|
||||
assert.include(broker.transformLink(`/${route}`), `/oauth/${route}`);
|
||||
assert.include(broker.transformLink(`${route}`), `/oauth/${route}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/', () => {
|
||||
it('prepends `/oauth` to the link', () => {
|
||||
assert.include(broker.transformLink('/'), '/oauth/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('not transformed', () => {
|
||||
it('does not include the oauth prefix', () => {
|
||||
const transformed = broker.transformLink('not_oauth');
|
||||
assert.notInclude(transformed, 'oauth/not_oauth');
|
||||
assert.include(transformed, 'not_oauth');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_provisionScopedKeys', () => {
|
||||
let accountKey;
|
||||
const keysJwk = 'jwk';
|
||||
const keys = {
|
||||
kA: 'foo',
|
||||
kB: 'bar'
|
||||
};
|
||||
const scope = 'https://identity.mozilla.com/apps/sample-scope-can-scope-key';
|
||||
const keyData = {
|
||||
[scope]: {
|
||||
identifier: scope,
|
||||
keyRotationSecret: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
keyRotationTimestamp: 1506970363512
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(broker._oAuthClient, 'getClientKeyData').callsFake((args) => {
|
||||
assert.equal(args.assertion, 'assertion');
|
||||
assert.equal(args.client_id, 'clientId');
|
||||
assert.equal(args.scope, 'scope');
|
||||
|
||||
return Promise.resolve(keyData);
|
||||
});
|
||||
|
||||
accountKey = new Account({
|
||||
email: 'testuser@testuser.com',
|
||||
keyFetchToken: 'key-fetch-token',
|
||||
uid: 'uid',
|
||||
unwrapBKey: 'unwrap-b-key'
|
||||
});
|
||||
|
||||
relier.set('keysJwk', keysJwk);
|
||||
|
||||
sinon.stub(accountKey, 'accountKeys').callsFake((args) => {
|
||||
return Promise.resolve(keys);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls _provisionScopedKeys to encrypt the bundle', () => {
|
||||
relier.set('keysJwk', keysJwk);
|
||||
|
||||
sinon.stub(broker._scopedKeys, 'createEncryptedBundle').callsFake((_keys, _uid, _keyData, _jwk) => {
|
||||
assert.equal(_keys, keys);
|
||||
assert.equal(_uid, 'uid');
|
||||
assert.equal(_keyData, keyData);
|
||||
assert.equal(_jwk, keysJwk);
|
||||
|
||||
return Promise.resolve('bundle');
|
||||
});
|
||||
|
||||
return broker._provisionScopedKeys(accountKey, 'assertion')
|
||||
.then((result) => {
|
||||
assert.isTrue(broker._scopedKeys.createEncryptedBundle.calledOnce);
|
||||
assert.equal(result, 'bundle');
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null if no unwrapBKey', () => {
|
||||
accountKey.set('unwrapBKey', null);
|
||||
|
||||
return broker._provisionScopedKeys(accountKey, 'assertion')
|
||||
.then((result) => {
|
||||
assert.equal(result, null);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null if no clientKeyData', () => {
|
||||
broker._oAuthClient.getClientKeyData.restore();
|
||||
sinon.stub(broker._oAuthClient, 'getClientKeyData').callsFake((args) => {
|
||||
return Promise.resolve({});
|
||||
});
|
||||
|
||||
return broker._provisionScopedKeys(accountKey, 'assertion')
|
||||
.then((result) => {
|
||||
assert.equal(result, null);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -3,7 +3,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { assert } from 'chai';
|
||||
import OAuthBroker from 'models/auth_brokers/oauth';
|
||||
import OAuthBroker from 'models/auth_brokers/oauth-redirect';
|
||||
import OAuthClient from 'lib/oauth-client';
|
||||
import OAuthRelier from 'models/reliers/oauth';
|
||||
import Session from 'lib/session';
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { assert } from 'chai';
|
||||
import AuthBroker from 'models/auth_brokers/oauth';
|
||||
import AuthBroker from 'models/auth_brokers/oauth-redirect';
|
||||
import AuthErrors from 'lib/auth-errors';
|
||||
import FormPrefill from 'models/form-prefill';
|
||||
import OAuthIndexView from 'views/oauth_index';
|
||||
|
|
|
@ -11,7 +11,7 @@ define(function (require, exports, module) {
|
|||
const FxaClient = require('lib/fxa-client');
|
||||
const Metrics = require('lib/metrics');
|
||||
const Notifier = require('lib/channels/notifier');
|
||||
const OAuthBroker = require('models/auth_brokers/oauth');
|
||||
const OAuthBroker = require('models/auth_brokers/oauth-redirect');
|
||||
const OAuthRelier = require('models/reliers/oauth');
|
||||
const SentryMetrics = require('lib/sentry');
|
||||
const Session = require('lib/session');
|
||||
|
|
|
@ -10,7 +10,7 @@ define(function (require, exports, module) {
|
|||
const FormPrefill = require('models/form-prefill');
|
||||
const Metrics = require('lib/metrics');
|
||||
const Notifier = require('lib/channels/notifier');
|
||||
const OAuthBroker = require('models/auth_brokers/oauth');
|
||||
const OAuthBroker = require('models/auth_brokers/oauth-redirect');
|
||||
const OAuthClient = require('lib/oauth-client');
|
||||
const OAuthRelier = require('models/reliers/oauth');
|
||||
const Session = require('lib/session');
|
||||
|
|
|
@ -9,7 +9,7 @@ import Backbone from 'backbone';
|
|||
import VerificationReasons from 'lib/verification-reasons';
|
||||
import FxaClient from 'lib/fxa-client';
|
||||
import Notifier from 'lib/channels/notifier';
|
||||
import OAuthBroker from 'models/auth_brokers/oauth';
|
||||
import OAuthBroker from 'models/auth_brokers/oauth-redirect';
|
||||
import Session from 'lib/session';
|
||||
import sinon from 'sinon';
|
||||
import SyncRelier from 'models/reliers/sync';
|
||||
|
|
|
@ -95,7 +95,6 @@ require('./spec/models/auth_brokers/fx-sync');
|
|||
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');
|
||||
require('./spec/models/auth_brokers/oauth-redirect');
|
||||
require('./spec/models/auth_brokers/oauth-redirect-chrome-android');
|
||||
require('./spec/models/auth_brokers/pairing/authority');
|
||||
|
|
Загрузка…
Ссылка в новой задаче