Merged oauth and oauth-redirect brokers

This commit is contained in:
hritvi 2019-02-13 21:30:08 +05:30
Родитель 0a91eb5406
Коммит 64dd80f965
10 изменённых файлов: 441 добавлений и 769 удалений

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

@ -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');