Skip to content

Commit 5e8f50e

Browse files
committed
feat(password): Add ability to set password after third party login to Sync
1 parent ba3bb85 commit 5e8f50e

File tree

9 files changed

+316
-1
lines changed

9 files changed

+316
-1
lines changed

packages/fxa-content-server/app/scripts/lib/fxa-client.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,6 +1390,13 @@ FxaClientWrapper.prototype = {
13901390
});
13911391
}
13921392
),
1393+
1394+
createPassword: withClient(
1395+
(client, token, email, password) => {
1396+
return client
1397+
.createPassword(token, email, password)
1398+
}
1399+
),
13931400
};
13941401

13951402
export default FxaClientWrapper;

packages/fxa-content-server/app/scripts/lib/router.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import SignInTotpCodeView from '../views/sign_in_totp_code';
3636
import SignInUnblockView from '../views/sign_in_unblock';
3737
import SignUpPasswordView from '../views/sign_up_password';
3838
import ThirdPartyAuthCallbackView from '../views/post_verify/third_party_auth/callback';
39+
import ThirdPartyAuthSetPasswordView from '../views/post_verify/third_party_auth/set_password';
3940
import Storage from './storage';
4041
import SubscriptionsProductRedirectView from '../views/subscriptions_product_redirect';
4142
import SubscriptionsManagementRedirectView from '../views/subscriptions_management_redirect';
@@ -302,7 +303,13 @@ Router = Router.extend({
302303
ThirdPartyAuthCallbackView
303304
);
304305
},
305-
306+
'post_verify/third_party_auth/set_password(/)': function () {
307+
this.createReactOrBackboneViewHandler(
308+
'post_verify/third_party_auth/set_password',
309+
ThirdPartyAuthSetPasswordView
310+
);
311+
},
312+
306313
'push/confirm_login(/)': createViewHandler('push/confirm_login'),
307314
'push/send_login(/)': createViewHandler('push/send_login'),
308315
'push/completed(/)': createViewHandler('push/completed'),

packages/fxa-content-server/app/scripts/models/account.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,10 @@ const Account = Backbone.Model.extend(
11121112
.then(this.set.bind(this));
11131113
},
11141114

1115+
createPassword(email, password) {
1116+
return this._fxaClient.createPassword(this.get('sessionToken'), email, password);
1117+
},
1118+
11151119
/**
11161120
* Fetch the account's list of attached clients.
11171121
*
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<div class="card">
2+
<header class="mb-2">
3+
<h1 id="fxa-signup-password-header" class="card-header">
4+
{{#t}}Set your password{{/t}}
5+
</h1>
6+
</header>
7+
8+
<section>
9+
<div class="error"></div>
10+
<div class="success"></div>
11+
12+
<form novalidate>
13+
<p id="prefillEmail" class="text-base break-all mb-5">{{ email }}</p>
14+
15+
<p class="mb-5">
16+
{{#t}}Please create a password to continue to Firefox Sync. Your data is encrypted with your password to protect your privacy.{{/t}}
17+
</p>
18+
19+
<div class="tooltip-container mb-5">
20+
<input name="password" id="password" type="password" class="input-text tooltip-below" placeholder="{{#t}}Password{{/t}}" value="{{ password }}" pattern=".{8,}" required autofocus data-form-prefill="password" data-synchronize-show="true" />
21+
<div id="password-strength-balloon-container" tabindex="-1"></div>
22+
</div>
23+
24+
<div class="tooltip-container mb-5">
25+
<input name="vpassword" id="vpassword" type="password" class="input-text tooltip-below" placeholder="{{#t}}Repeat password{{/t}}" pattern=".{8,}" required data-synchronize-show="true" />
26+
27+
<div class="input-balloon hidden" id="vpassword-balloon">
28+
<div class="before:content-key flex before:w-8">
29+
<p class="ltr:pl-2 rtl:pr-2">
30+
{{#unsafeTranslate}}You need this password to access any encrypted data you store with us.<br class="mb-3">A reset means potentially losing data like passwords and bookmarks.{{/unsafeTranslate}}
31+
</p>
32+
</div>
33+
</div>
34+
</div>
35+
36+
<header>
37+
<h2 id="fxa-choose-what-to-sync-header" class="font-normal mb-5 text-base">
38+
{{#t}}Choose what to sync{{/t}}
39+
</h2>
40+
</header>
41+
<div class="flex flex-wrap text-start ltr:mobileLandscape:ml-6 rtl:mobileLandscape:mr-6 mb-1">
42+
{{#engines}}
43+
<div class="mb-4 relative flex-50% rtl:mobileLandscape:pr-6 ltr:mobileLandscape:pl-6 rtl:pr-3 ltr:pl-3 flex items-center">
44+
<input name="sync-content" class="input-checkbox" id="sync-engine-{{id}}" type="checkbox" {{#checked}}checked="checked"{{/checked}} value="{{id}}" tabindex="{{tabindex}}" /><label class="input-checkbox-label" for="sync-engine-{{id}}">{{text}}</label>
45+
</div>
46+
{{/engines}}
47+
</div>
48+
49+
<div class="flex">
50+
<button id="submit-btn" class="cta-primary cta-xl" type="submit">{{#t}}Create Password{{/t}}</button>
51+
</div>
52+
</form>
53+
</section>
54+
</div>

packages/fxa-content-server/app/scripts/views/mixins/third-party-auth-mixin.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,13 @@ export default {
206206
this.logFlowEvent(`${provider}.signin-complete`);
207207

208208
this.metrics.flush();
209+
210+
// Sync service requires a password to be set before it can be used.
211+
// Note that once a password is set, the user will not have an option to use
212+
// third party login for Sync since it always requires a password.
213+
if (this.relier.isSync()) {
214+
return this.navigate('/post_verify/third_party_auth/set_password', { provider });
215+
}
209216

210217
return this.signIn(updatedAccount);
211218
})
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import AuthErrors from '../../../lib/auth-errors';
6+
import Cocktail from '../../../lib/cocktail';
7+
import Template from '../../../templates/post_verify/third_party_auth/set_password.mustache';
8+
import FormView from '../../form';
9+
import FlowEventsMixin from '../../mixins/flow-events-mixin';
10+
import PasswordMixin from '../../mixins/password-mixin';
11+
import PasswordStrengthMixin from '../../mixins/password-strength-mixin';
12+
import CWTSOnSignupPasswordMixin from '../../mixins/cwts-on-signup-password';
13+
import ServiceMixin from '../../mixins/service-mixin';
14+
import AccountSuggestionMixin from '../../mixins/account-suggestion-mixin';
15+
import SigninMixin from '../../mixins/signin-mixin';
16+
17+
const PASSWORD_INPUT_SELECTOR = '#password';
18+
const VPASSWORD_INPUT_SELECTOR = '#vpassword';
19+
20+
class SetPassword extends FormView {
21+
template = Template;
22+
23+
_getPassword() {
24+
return this.$(PASSWORD_INPUT_SELECTOR).val();
25+
}
26+
27+
_getVPassword() {
28+
return this.$(VPASSWORD_INPUT_SELECTOR).val();
29+
}
30+
31+
isValidEnd() {
32+
return this._getPassword() === this._getVPassword();
33+
}
34+
35+
showValidationErrorsEnd() {
36+
if (this._getPassword() !== this._getVPassword()) {
37+
const err = AuthErrors.toError('PASSWORDS_DO_NOT_MATCH');
38+
this.showValidationError(this.$(PASSWORD_INPUT_SELECTOR), err, true);
39+
}
40+
}
41+
42+
getAccount() {
43+
return this.getSignedInAccount();
44+
}
45+
46+
beforeRender() {
47+
const account = this.getSignedInAccount();
48+
if (account.isDefault()) {
49+
return this.replaceCurrentPage('/');
50+
}
51+
}
52+
53+
setInitialContext(context) {
54+
const email = this.getAccount().get('email');
55+
context.set({
56+
email,
57+
});
58+
}
59+
60+
submit() {
61+
const account = this.getAccount();
62+
const password = this._getPassword();
63+
64+
return account.createPassword(account.get('email'), password)
65+
.then(() => {
66+
// After the user has set a password, initiated the standard Sync
67+
// login flow with the password they set.
68+
return this.signIn(account, password);
69+
});
70+
}
71+
}
72+
73+
Cocktail.mixin(
74+
SetPassword,
75+
PasswordMixin,
76+
CWTSOnSignupPasswordMixin,
77+
PasswordStrengthMixin({
78+
balloonEl: '#password-strength-balloon-container',
79+
passwordEl: PASSWORD_INPUT_SELECTOR,
80+
}),
81+
ServiceMixin,
82+
AccountSuggestionMixin,
83+
FlowEventsMixin,
84+
SigninMixin
85+
);
86+
87+
export default SetPassword;
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import _ from 'underscore';
6+
import { assert } from 'chai';
7+
import Account from 'models/account';
8+
import Backbone from 'backbone';
9+
import BaseBroker from 'models/auth_brokers/base';
10+
import Metrics from 'lib/metrics';
11+
import Relier from 'models/reliers/relier';
12+
import SentryMetrics from 'lib/sentry';
13+
import sinon from 'sinon';
14+
import User from 'models/user';
15+
import View from 'views/post_verify/third_party_auth/set_password';
16+
import WindowMock from '../../../../mocks/window';
17+
import $ from 'jquery';
18+
19+
const PASSWORD_INPUT_SELECTOR = '#password';
20+
const VPASSWORD_INPUT_SELECTOR = '#vpassword';
21+
const PASSWORD = 'passwordzxcv';
22+
23+
describe('views/post_verify/third_party_auth/set_password', () => {
24+
let account;
25+
let broker;
26+
let metrics;
27+
let model;
28+
let notifier;
29+
let relier;
30+
let sentryMetrics;
31+
let user;
32+
let view;
33+
let windowMock;
34+
35+
beforeEach(() => {
36+
windowMock = new WindowMock();
37+
relier = new Relier({
38+
window: windowMock,
39+
});
40+
broker = new BaseBroker({
41+
relier,
42+
window: windowMock,
43+
});
44+
account = new Account({
45+
46+
uid: 'uid',
47+
});
48+
model = new Backbone.Model({
49+
account,
50+
});
51+
notifier = _.extend({}, Backbone.Events);
52+
sentryMetrics = new SentryMetrics();
53+
metrics = new Metrics({ notifier, sentryMetrics });
54+
user = new User();
55+
view = new View({
56+
broker,
57+
metrics,
58+
model,
59+
notifier,
60+
relier,
61+
user,
62+
});
63+
64+
sinon.stub(view, 'getSignedInAccount').callsFake(() => account);
65+
sinon
66+
.stub(account, 'createPassword')
67+
.callsFake(() => Promise.resolve());
68+
sinon
69+
.stub(view, 'signIn')
70+
.callsFake(() => Promise.resolve());
71+
72+
return view.render().then(() => $('#container').html(view.$el));
73+
});
74+
75+
afterEach(function () {
76+
view.remove();
77+
view.destroy();
78+
});
79+
80+
describe('render', () => {
81+
it('renders the view', () => {
82+
assert.lengthOf(view.$('#fxa-signup-password-header'), 1);
83+
assert.include(view.$('#prefillEmail').text(), '[email protected]');
84+
assert.lengthOf(view.$('#fxa-choose-what-to-sync-header'), 1);
85+
assert.lengthOf(view.$('#submit-btn'), 1);
86+
});
87+
88+
describe('without an account', () => {
89+
beforeEach(() => {
90+
account = new Account({});
91+
sinon.spy(view, 'navigate');
92+
return view.render();
93+
});
94+
95+
it('redirects to the email first page', () => {
96+
assert.isTrue(view.navigate.calledWith('/'));
97+
});
98+
});
99+
});
100+
101+
describe('validateAndSubmit', () => {
102+
beforeEach(() => {
103+
sinon.stub(view, 'submit').callsFake(() => Promise.resolve());
104+
sinon.spy(view, 'showValidationError');
105+
});
106+
107+
describe('with invalid password', () => {
108+
beforeEach(() => {
109+
view.$(PASSWORD_INPUT_SELECTOR).val('');
110+
return view.validateAndSubmit().then(assert.fail, () => {});
111+
});
112+
113+
it('displays a tooltip, does not call submit', () => {
114+
assert.isTrue(view.showValidationError.called);
115+
assert.isFalse(view.submit.called);
116+
});
117+
});
118+
119+
describe('with valid password', () => {
120+
beforeEach(() => {
121+
view.$(PASSWORD_INPUT_SELECTOR).val(PASSWORD);
122+
view.$(VPASSWORD_INPUT_SELECTOR).val(PASSWORD);
123+
return view.validateAndSubmit();
124+
});
125+
126+
it('calls submit', () => {
127+
assert.equal(view.submit.callCount, 1);
128+
});
129+
});
130+
});
131+
132+
describe('submit', () => {
133+
describe('success', () => {
134+
beforeEach(() => {
135+
sinon.spy(view, 'navigate');
136+
view.$(PASSWORD_INPUT_SELECTOR).val(PASSWORD);
137+
view.$(VPASSWORD_INPUT_SELECTOR).val(PASSWORD);
138+
return view.submit();
139+
});
140+
141+
it('calls correct methods', () => {
142+
assert.isTrue(account.createPassword.calledWith('[email protected]', PASSWORD),);
143+
assert.isTrue(view.signIn.calledWith(account, PASSWORD,));
144+
});
145+
});
146+
});
147+
});

packages/fxa-content-server/app/tests/test_start.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ require('./spec/views/post_verify/newsletters/add_newsletters');
232232
require('./spec/views/post_verify/password/force_password_change');
233233
require('./spec/views/post_verify/secondary_email/add_secondary_email');
234234
require('./spec/views/post_verify/secondary_email/confirm_secondary_email');
235+
require('./spec/views/post_verify/third_party_auth/set_password');
235236
require('./spec/views/progress_indicator');
236237
require('./spec/views/push/confirm_login');
237238
require('./spec/views/push/send_login');

packages/fxa-content-server/server/lib/routes/react-app/content-server-routes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const FRONTEND_ROUTES = [
5353
'post_verify/secondary_email/confirm_secondary_email',
5454
'post_verify/secondary_email/verified_secondary_email',
5555
'post_verify/third_party_auth/callback',
56+
'post_verify/third_party_auth/set_password',
5657
'primary_email_verified',
5758
'push/completed',
5859
'push/confirm_login',

0 commit comments

Comments
 (0)