pre-verify tokens work end to end!

* Add functional tests for the preverified flow.

To test using 123done, apply the following diff to fxa-auth-server/

diff --git a/config/dev.json b/config/dev.json
index 7919bd9..f08a03a 100644
--- a/config/dev.json
+++ b/config/dev.json
@@ -19,5 +19,6 @@
   "bounces": {
     "region": "us-east-1"
   },
-  "customsUrl": "none"
+  "customsUrl": "none",
+  "trustedJKUs": ["http://127.0.0.1:8080/.well-known/public-keys"]
 }

Then visit:

http://127.0.0.1:8080/api/preverify-signup?email=<your_email>
This commit is contained in:
Shane Tomlinson 2014-09-04 16:40:57 +01:00
Родитель d30dd6d3f4
Коммит acbb5523f6
12 изменённых файлов: 269 добавлений и 129 удалений

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

@ -89,9 +89,13 @@ function (P, jwcrypto, FxaClient) {
});
}
bundle.generate = bundle;
function Assertion() {
}
return bundle;
Assertion.prototype = {
generate: bundle
};
return Assertion;
});

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

@ -98,12 +98,14 @@ define([
}
return {
setupOAuth: function (params) {
setupOAuth: function (params, deps) {
deps = deps || {};
if (! this._configLoader) {
this._configLoader = new ConfigLoader();
}
this._oAuthClient = new OAuthClient();
this._oAuthClient = deps.oAuthClient || new OAuthClient();
if (! params) {
// params listed in:
@ -120,7 +122,7 @@ define([
// assertion library to use to generate assertions
// can be substituted for testing
this.assertionLibrary = Assertion;
this.assertionLibrary = deps.assertionLibrary || new Assertion();
Session.set('service', this.service);
// A hint that allows Session to determine whether the user

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

@ -16,11 +16,16 @@ function (_, p, BaseView, SignUpView, ServiceMixin) {
className: 'sign-up oauth-sign-up',
initialize: function (options) {
options = options || {};
/* jshint camelcase: false */
SignUpView.prototype.initialize.call(this, options);
// Set up OAuth so we can retrieve the pretty service name
this.setupOAuth();
this.setupOAuth(null, {
assertionLibrary: options.assertionLibrary,
oAuthClient: options.oAuthClient
});
},
beforeRender: function() {
@ -40,7 +45,13 @@ function (_, p, BaseView, SignUpView, ServiceMixin) {
// Store oauth state for when/if the oauth flow completes
// in this browser
this.persistOAuthParams();
return SignUpView.prototype.onSignUpSuccess.call(this, accountData);
if (accountData.verified) {
// the account is verified using the pre-verify flow. Send the user
// back to the RP without further interaction.
return this.finishOAuthFlow();
} else {
return SignUpView.prototype.onSignUpSuccess.call(this, accountData);
}
}
});

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

@ -49,7 +49,8 @@ define([
function removeFxaClientSpy(fxaClient) {
// return the client to its original state.
for (var key in fxaClient) {
if (typeof fxaClient[key] === 'function') {
if (typeof fxaClient[key] === 'function' &&
typeof fxaClient[key].restore === 'function') {
fxaClient[key].restore();
}
}

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

@ -28,10 +28,12 @@ function (chai, $, P,
var email;
var password = 'password';
var client;
var assertionLibrary;
describe('lib/assertion', function () {
beforeEach(function () {
Session.clear();
assertionLibrary = new Assertion();
client = new FxaClientWrapper();
email = ' testuser' + Math.random() + '@testuser.com ';
return client.signUp(email, password, { preVerified: true });
@ -44,7 +46,7 @@ function (chai, $, P,
describe('validate', function () {
it('generates a valid assertion', function () {
var assertion;
return Assertion.generate(AUDIENCE)
return assertionLibrary.generate(AUDIENCE)
.then(function(ass) {
assertion = ass;
assert.isNotNull(ass, 'Assertion is not null');

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

@ -5,6 +5,7 @@
define([
'chai',
'jquery',
'sinon',
'lib/promise',
'../../mocks/channel',
'../../lib/helpers',
@ -16,7 +17,7 @@ define([
// FxaClientWrapper is the object that is used in
// fxa-content-server views. It wraps FxaClient to
// take care of some app-specific housekeeping.
function (chai, $, p, ChannelMock, testHelpers,
function (chai, $, sinon, p, ChannelMock, testHelpers,
Session, FxaClientWrapper, AuthErrors, Constants) {
'use strict';
@ -168,6 +169,18 @@ function (chai, $, p, ChannelMock, testHelpers,
it('signUp a preverified user using preVerifyToken', function () {
var password = 'password';
var preVerifyToken = 'somebiglongtoken';
// we are going to take over from here.
testHelpers.removeFxaClientSpy(realClient);
sinon.stub(realClient, 'signUp', function () {
return true;
});
sinon.stub(realClient, 'signIn', function () {
return {
sessionToken: 'asessiontoken'
};
});
return client.signUp(email, password, {
preVerifyToken: preVerifyToken
})

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

@ -7,25 +7,58 @@
define([
'chai',
'jquery',
'sinon',
'views/oauth_sign_up',
'lib/promise',
'lib/session',
'lib/fxa-client',
'lib/metrics',
'lib/auth-errors',
'lib/oauth-client',
'lib/assertion',
'models/reliers/relier',
'../../mocks/window',
'../../mocks/router',
'../../mocks/oauth_servers',
'../../lib/helpers'
],
function (chai, $, View, Session, FxaClient, WindowMock, RouterMock, OAuthServersMock, TestHelpers) {
function (chai, $, sinon, View, p, Session, FxaClient, Metrics, AuthErrors,
OAuthClient, Assertion, Relier, WindowMock, RouterMock, TestHelpers) {
var assert = chai.assert;
describe('views/oauth_sign_up', function () {
var view, email, router, windowMock, CLIENT_ID, STATE, SCOPE, CLIENT_NAME, BASE_REDIRECT_URL, fxaClient, oAuthServersMock;
function fillOutSignUp (email, password, opts) {
opts = opts || {};
var context = opts.context || window;
var year = opts.year || '1960';
CLIENT_ID = 'dcdb5ae7add825d2';
STATE = '123';
SCOPE = 'profile:email';
CLIENT_NAME = '123Done';
BASE_REDIRECT_URL = 'http://127.0.0.1:8080/api/oauth';
context.$('[type=email]').val(email);
context.$('[type=password]').val(password);
if (!opts.ignoreYear) {
$('#fxa-age-year').val(year);
}
if (context.enableSubmitIfValid) {
context.enableSubmitIfValid();
}
}
var CLIENT_ID = 'dcdb5ae7add825d2';
var STATE = '123';
var SCOPE = 'profile:email';
var CLIENT_NAME = '123Done';
var BASE_REDIRECT_URL = 'http://127.0.0.1:8080/api/oauth';
describe('views/oauth_sign_up', function () {
var nowYear = (new Date()).getFullYear();
var view;
var router;
var email;
var metrics;
var windowMock;
var fxaClient;
var oAuthClient;
var assertionLibrary;
var relier;
beforeEach(function () {
Session.clear();
@ -34,15 +67,32 @@ function (chai, $, View, Session, FxaClient, WindowMock, RouterMock, OAuthServer
windowMock = new WindowMock();
windowMock.location.search = '?client_id=' + CLIENT_ID + '&state=' + STATE + '&scope=' + SCOPE + '&redirect_uri=' + encodeURIComponent(BASE_REDIRECT_URL);
oAuthServersMock = new OAuthServersMock();
metrics = new Metrics();
relier = new Relier();
oAuthClient = new OAuthClient();
sinon.stub(oAuthClient, 'getClientInfo', function () {
return p({
name: '123Done',
//jshint camelcase: false
redirect_uri: BASE_REDIRECT_URL
});
});
assertionLibrary = new Assertion();
fxaClient = new FxaClient();
view = new View({
router: router,
metrics: metrics,
window: windowMock,
fxaClient: fxaClient
fxaClient: fxaClient,
relier: relier,
assertionLibrary: assertionLibrary,
oAuthClient: oAuthClient
});
return view.render()
.then(function () {
$('#container').html(view.el);
@ -53,7 +103,6 @@ function (chai, $, View, Session, FxaClient, WindowMock, RouterMock, OAuthServer
Session.clear();
view.remove();
view.destroy();
oAuthServersMock.destroy();
});
describe('render', function () {
@ -67,27 +116,78 @@ function (chai, $, View, Session, FxaClient, WindowMock, RouterMock, OAuthServer
});
});
describe('submit', function () {
describe('submit without a preVerifyToken', function () {
it('sets up the user\'s ouath session on success', function () {
var password = 'password';
fillOutSignUp(email, 'password', { year: nowYear - 14, context: view });
// the screen is rendered, we can take over from here.
oAuthServersMock.destroy();
return view.fxaClient.signUp(email, password)
.then(function () {
$('.email').val(email);
$('[type=password]').val(password);
$('#fxa-age-year').val('1990');
return view.submit();
})
sinon.stub(fxaClient, 'signUp', function () {
return p({
sessionToken: 'asessiontoken',
verified: false
});
});
return view.submit()
.then(function () {
assert.equal(Session.oauth.state, STATE);
assert.equal(Session.service, CLIENT_ID);
});
});
});
});
describe('submit with a preVerifyToken', function () {
beforeEach(function () {
relier.set('preVerifyToken', 'preverifytoken');
});
it('redirects to the rp if pre-verification is successful', function () {
sinon.stub(fxaClient, 'signUp', function () {
return p({
sessionToken: 'asessiontoken',
verified: true
});
});
sinon.stub(oAuthClient, 'getCode', function () {
return {
redirect: BASE_REDIRECT_URL + '?state=fakestate&code=faketcode'
};
});
sinon.stub(assertionLibrary, 'generate', function () {
return 'fakeassertion';
});
fillOutSignUp(email, 'password', { year: nowYear - 14, context: view });
return view.submit()
.then(function () {
assert.include(windowMock.location.href, BASE_REDIRECT_URL);
});
});
it('redirects to /confirm if pre-verification is not successful', function () {
sinon.stub(fxaClient, 'signUp', function (email, password, options) {
// force the preVerifyToken to be invalid
if (options.preVerifyToken) {
return p().then(function () {
throw AuthErrors.toError('INVALID_VERIFICATION_CODE');
});
} else {
return p({
sessionToken: 'sessiontoken',
verified: false
});
}
});
fillOutSignUp(email, 'password', { year: nowYear - 14, context: view });
return view.submit()
.then(function () {
assert.equal(router.page, 'confirm');
});
});
});
});
});

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

@ -9,7 +9,6 @@ define([
'chai',
'underscore',
'jquery',
'sinon',
'lib/promise',
'views/sign_up',
'lib/session',
@ -18,13 +17,12 @@ define([
'lib/fxa-client',
'lib/translator',
'lib/service-name',
'models/reliers/relier',
'../../mocks/router',
'../../mocks/window',
'../../lib/helpers'
],
function (chai, _, $, sinon, p, View, Session, AuthErrors, Metrics, FxaClient, Translator, ServiceName,
Relier, RouterMock, WindowMock, TestHelpers) {
function (chai, _, $, p, View, Session, AuthErrors, Metrics, FxaClient, Translator, ServiceName,
RouterMock, WindowMock, TestHelpers) {
var assert = chai.assert;
var wrapAssertion = TestHelpers.wrapAssertion;
var translator = new Translator('en-US', ['en-US']);
@ -429,94 +427,6 @@ function (chai, _, $, sinon, p, View, Session, AuthErrors, Metrics, FxaClient, T
});
});
describe('views/sign_up with a pre-verified user', function () {
var view, router, email, metrics, windowMock, fxaClient, fakeServer, relier;
beforeEach(function () {
Session.clear();
email = TestHelpers.createEmail();
document.cookie = 'tooyoung=1; expires=Thu, 01-Jan-1970 00:00:01 GMT';
router = new RouterMock();
windowMock = new WindowMock();
metrics = new Metrics();
fxaClient = new FxaClient();
relier = new Relier();
relier.set('preVerifyToken', 'bigscarytoken');
fakeServer = sinon.fakeServer.create();
fakeServer.autoRespond = true;
fakeServer.respondWith('GET', '/config',
[200, { 'Content-Type': 'application/json' }, JSON.stringify({
fxaccountUrl: 'http://127.0.0.1:9000/v1'
})]);
view = new View({
router: router,
metrics: metrics,
window: windowMock,
fxaClient: fxaClient,
relier: relier
});
return view.render()
.then(function () {
$('#container').append(view.el);
});
});
afterEach(function () {
metrics.destroy();
view.remove();
view.destroy();
document.cookie = 'tooyoung=1; expires=Thu, 01-Jan-1970 00:00:01 GMT';
fakeServer.restore();
fakeServer = view = router = metrics = null;
});
describe('submit', function () {
it('redirects to /signup_complete if pre-verification is successful', function () {
fakeServer.respondWith('POST', 'http://127.0.0.1:9000/v1/account/create?keys=true',
[200, { 'Content-Type': 'application/json' }, JSON.stringify({})]);
fakeServer.respondWith('POST', 'http://127.0.0.1:9000/v1/account/login?keys=true',
[200, { 'Content-Type': 'application/json' }, JSON.stringify({ verified: true })]);
var nowYear = (new Date()).getFullYear();
fillOutSignUp(email, 'password', { year: nowYear - 14, context: view });
return view.submit()
.then(function () {
assert.equal(router.page, 'signup_complete');
});
});
it('redirects to /confirm if pre-verification is not successful', function () {
fakeServer.respondWith(/\/account\/create\?keys=true/, function (xhr) {
// force the preVerifyToken to be invalid.
if (xhr.requestBody.preVerifyToken) {
xhr.respond(500, { 'Content-Type': 'application/json' }, JSON.stringify({
errno: AuthErrors.toCode('INVALID_VERIFICATION_CODE')
}));
} else {
xhr.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({}));
}
});
fakeServer.respondWith('POST', 'http://127.0.0.1:9000/v1/account/login?keys=true',
[200, { 'Content-Type': 'application/json' }, JSON.stringify({ verfied: false })]);
var nowYear = (new Date()).getFullYear();
fillOutSignUp(email, 'password', { year: nowYear - 14, context: view });
return view.submit()
.then(function () {
fakeServer.restore();
assert.equal(router.page, 'confirm');
});
});
});
});
});

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

@ -6,7 +6,7 @@
"blanket": "1.1.5",
"chai": "1.8.1",
"fxa-content-server-l10n": "https://github.com/mozilla/fxa-content-server-l10n.git",
"fxa-js-client": "https://github.com/mozilla/fxa-js-client.git#0.1.23",
"fxa-js-client": "https://github.com/mozilla/fxa-js-client.git#0.1.24",
"html5shiv": "3.7.2",
"jquery": "1.11.1",
"mocha": "1.18.2",

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

@ -39,7 +39,6 @@ module.exports = function (config, templates, i18n) {
);
}
return function (app) {
// handle password reset links
app.get('/v1/complete_reset_password', function (req, res) {

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

@ -0,0 +1,95 @@
/* 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/. */
define([
'intern',
'intern!object',
'intern/chai!assert',
'require',
'intern/node_modules/dojo/node!xmlhttprequest',
'app/bower_components/fxa-js-client/fxa-client',
'tests/lib/restmail',
'tests/lib/helpers',
'tests/functional/lib/helpers'
], function (intern, registerSuite, assert, require, nodeXMLHttpRequest, FxaClient, restmail, TestHelpers, FunctionalHelpers) {
'use strict';
var config = intern.config;
var CONTENT_SERVER = config.fxaContentRoot;
var OAUTH_APP = config.fxaOauthApp;
var TOO_YOUNG_YEAR = new Date().getFullYear() - 13;
var PASSWORD = 'password';
var user;
var email;
registerSuite({
name: 'preverified oauth sign up',
setup: function () {
email = TestHelpers.createEmail();
user = TestHelpers.emailToUser(email);
},
beforeEach: function () {
var self = this;
// clear localStorage to avoid polluting other tests.
// Without the clear, /signup tests fail because of the info stored
// in prefillEmail
return self.get('remote')
// always go to the content server so the browser state is cleared
.get(require.toUrl(CONTENT_SERVER))
.setFindTimeout(intern.config.pageLoadTimeout)
.then(function () {
return FunctionalHelpers.clearBrowserState(self);
});
},
'preverified sign up': function () {
var self = this;
return TestHelpers.getEmailPreverifyToken(email)
.then(function (token) {
var SIGNUP_URL = OAUTH_APP + 'api/preverified-signup?' +
'email=' + encodeURIComponent(email);
return self.get('remote')
.get(require.toUrl(SIGNUP_URL))
.setFindTimeout(intern.config.pageLoadTimeout)
.findByCssSelector('#fxa-signup-header')
.end()
.findByCssSelector('form input.password')
.click()
.type(PASSWORD)
.end()
.findByCssSelector('#fxa-age-year')
.click()
.end()
.findById('fxa-' + (TOO_YOUNG_YEAR - 1))
.pressMouseButton()
.releaseMouseButton()
.click()
.end()
.findByCssSelector('button[type="submit"]')
.click()
.end()
// user is pre-verified and sent directly to the RP.
.findByCssSelector('#loggedin')
.getVisibleText()
.then(function (text) {
// user is signed in as pre-verified email
assert.equal(text, email);
})
.end();
});
}
});
});

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

@ -6,7 +6,10 @@ define([
'./functional/oauth_sign_in',
'./functional/oauth_sign_up',
'./functional/oauth_reset_password',
'./functional/oauth_webchannel'
'./functional/oauth_webchannel'/*,
TODO - enable this whenever 123done and the oauth-server are patched to handle
preverified emails.
'./functional/oauth_preverified_sign_up'*/
], function () {
'use strict';
});