Skip to content

Commit efbbc7a

Browse files
committed
task(auth): Add more tests around account destroy
1 parent 12cd5af commit efbbc7a

4 files changed

Lines changed: 253 additions & 68 deletions

File tree

packages/fxa-auth-server/config/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1977,6 +1977,14 @@ const convictConf = convict({
19771977
env: 'OTP_SIGNUP_DIGIT',
19781978
},
19791979
},
1980+
accountDestroy: {
1981+
requireVerifiedAccount: {
1982+
doc: 'Whether or not the account must be verified in order to destroy it.',
1983+
default: true,
1984+
format: Boolean,
1985+
env: 'ACCOUNT_DESTROY__REQUIRE_VERIFIED_ACCOUNT',
1986+
},
1987+
},
19801988
passwordForgotOtp: {
19811989
digits: {
19821990
doc: 'Number of digits in token',

packages/fxa-auth-server/lib/routes/account.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1857,6 +1857,26 @@ export class AccountHandler {
18571857
throw error.unverifiedSession();
18581858
}
18591859

1860+
// We can also check that the email was verified. This proves the account is active and
1861+
// related to a valid email. Accounts that aren't activated get deleted automatically
1862+
// by cron jobs anyways...
1863+
if (
1864+
this.config.accountDestroy.requireVerifiedAccount &&
1865+
!accountRecord.emailVerified
1866+
) {
1867+
throw error.unverifiedAccount();
1868+
}
1869+
1870+
// Regardless of whether or not an account has TOTP, we must make sure the user actually
1871+
// owns the account. This means the sessionToken must be verified.
1872+
//
1873+
// The UI will request an OTP code verification before destroying the account in the event
1874+
// the session is currently unverified. The following check will ensure that this OTP code
1875+
// was actually provided by the user.
1876+
if (!sessionToken.tokenVerified) {
1877+
throw error.unverifiedSession();
1878+
}
1879+
18601880
// In other scenarios, fall back to the default behavior and let the user
18611881
// delete the account. If they have a password set, we verify it here. Users
18621882
// that don't have a password set will be able to delete their account without

packages/fxa-auth-server/test/client/api.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,6 @@ module.exports = (config) => {
219219
if (!options) {
220220
options = { keys: true };
221221
}
222-
223222
return this.doRequest(
224223
'POST',
225224
`${this.baseURL}/account/login${getQueryString(options)}`,

packages/fxa-auth-server/test/remote/account_destroy_tests.js

Lines changed: 225 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
const { assert } = require('chai');
88
const TestServer = require('../test_server');
99
const Client = require('../client')();
10+
const otplib = require('otplib');
11+
const crypto = require('crypto');
1012

1113
const config = require('../../config').default.getProperties();
1214

@@ -15,108 +17,264 @@ const config = require('../../config').default.getProperties();
1517
describe(`#integration${testOptions.version} - remote account destroy`, function () {
1618
this.timeout(60000);
1719
let server;
20+
let tempConfigValue;
1821

1922
before(async function () {
23+
// Important, this config impacts test logic. By default this is enabled
24+
// in development/test with a very large max time. In the real world the max time
25+
// is much lower, and confirmation is not skipped very often.The test cases in this
26+
// suite are looking at edge cases on unconfirmed accounts and sessions. Therefore,
27+
// this config will always be disabled here.
28+
tempConfigValue = config.signinConfirmation.skipForNewAccounts.enabled;
29+
config.signinConfirmation.skipForNewAccounts.enabled = false;
2030
server = await TestServer.start(config);
2131
});
2232

2333
after(async function () {
34+
config.signinConfirmation.skipForNewAccounts.enabled = tempConfigValue;
2435
await TestServer.stop(server);
2536
});
2637

27-
it('account destroy', () => {
38+
// Note that this test case most closely aligns with what most users experience.
39+
// In this case we have a user with a verified account, but unverified session
40+
// and no totp. When this occurs our UI will send an OTP email to user and prompt
41+
// them to enter the code prior to deleting the account.
42+
it('can delete account by providing short code', async function () {
2843
const email = server.uniqueEmail();
29-
const password = 'allyourbasearebelongtous';
30-
let client = null;
31-
return Client.createAndVerify(
44+
const password = 'ok';
45+
await Client.create(config.publicUrl, email, password, {
46+
...testOptions,
47+
verificationMethod: 'email-2fa',
48+
keys: true,
49+
});
50+
const client = await Client.login(config.publicUrl, email, password, {
51+
...testOptions,
52+
verificationMethod: 'email-2fa',
53+
keys: true,
54+
});
55+
56+
// Send a short code, this will validate the account and the session.
57+
// In the UI this happens when a user clicks on delete account, and
58+
// OTP message box pops up.
59+
await client.resendVerifyShortCodeEmail();
60+
const emailData = await server.mailbox.waitForEmail(email);
61+
let code;
62+
for (let i = 0; i < emailData.length; i++) {
63+
if (emailData[i].headers['x-verify-short-code']) {
64+
code = emailData[i].headers['x-verify-short-code'];
65+
}
66+
}
67+
assert.isDefined(code);
68+
await client.verifyShortCodeEmail(code);
69+
70+
// Should not throw
71+
await client.destroyAccount();
72+
});
73+
74+
it('can delete account by providing verify code', async function () {
75+
const email = server.uniqueEmail();
76+
const password = 'ok';
77+
await Client.create(config.publicUrl, email, password, {
78+
...testOptions,
79+
verificationMethod: 'email-2fa',
80+
keys: true,
81+
});
82+
const client = await Client.login(config.publicUrl, email, password, {
83+
...testOptions,
84+
verificationMethod: 'email-2fa',
85+
keys: true,
86+
});
87+
88+
const emailData = await server.mailbox.waitForEmail(email);
89+
const code = emailData[emailData.length - 1].headers['x-verify-code'];
90+
await client.verifyEmail(code);
91+
92+
// Should not throw
93+
await client.destroyAccount();
94+
});
95+
96+
it('cannot delete account with invalid authPW', async function () {
97+
const email = server.uniqueEmail();
98+
const password = 'ok';
99+
const c = await Client.createAndVerify(
32100
config.publicUrl,
33101
email,
34102
password,
35103
server.mailbox,
36104
testOptions
37-
)
38-
.then((x) => {
39-
client = x;
40-
return client.sessionStatus();
41-
})
42-
.then((status) => {
43-
return client.destroyAccount();
44-
})
45-
.then(() => {
46-
return client.keys();
47-
})
48-
.then(
49-
(keys) => {
50-
assert(false, 'account not destroyed');
51-
},
52-
(err) => {
53-
assert.equal(err.message, 'Unknown account', 'account destroyed');
54-
}
105+
);
106+
107+
c.authPW = Buffer.from(
108+
'0000000000000000000000000000000000000000000000000000000000000000',
109+
'hex'
110+
);
111+
c.authPWVersion2 = Buffer.from(
112+
'0000000000000000000000000000000000000000000000000000000000000000',
113+
'hex'
114+
);
115+
116+
try {
117+
await c.destroyAccount();
118+
assert.fail(
119+
'should not be able to destroy account with invalid password'
55120
);
121+
} catch (err) {
122+
assert.equal(err.errno, 103);
123+
}
56124
});
57125

58-
it('invalid authPW on account destroy', () => {
126+
it('cannot delete account without verifying TOTP', async function () {
59127
const email = server.uniqueEmail();
60128
const password = 'ok';
61-
return Client.createAndVerify(
129+
130+
await Client.createAndVerifyAndTOTP(
62131
config.publicUrl,
63132
email,
64133
password,
65134
server.mailbox,
135+
{
136+
...testOptions,
137+
keys: true,
138+
}
139+
);
140+
141+
// Create a new unverified session
142+
const client = await Client.login(
143+
config.publicUrl,
144+
email,
145+
password,
146+
testOptions
147+
);
148+
const res = await await client.emailStatus();
149+
assert.equal(res.sessionVerified, false, 'session not verified');
150+
151+
try {
152+
await client.destroyAccount();
153+
assert.fail(
154+
'Should not be able to destroy account without verifying totp'
155+
);
156+
} catch (err) {
157+
assert.equal(err.errno, 138, 'unverified session');
158+
}
159+
});
160+
161+
it('cannot delete account with TOTP by supplying email otp code', async function () {
162+
const email = server.uniqueEmail();
163+
const password = 'ok';
164+
await Client.create(config.publicUrl, email, password, {
165+
...testOptions,
166+
verificationMethod: 'email-2fa',
167+
keys: true,
168+
});
169+
let client = await Client.login(config.publicUrl, email, password, {
170+
...testOptions,
171+
verificationMethod: 'email-2fa',
172+
keys: true,
173+
});
174+
175+
// Send a short code, this will validate the account and the session.
176+
// In the UI this happens when a user clicks on delete account, and
177+
// OTP message box pops up.
178+
await client.resendVerifyShortCodeEmail();
179+
const emailData = await server.mailbox.waitForEmail(email);
180+
let code;
181+
for (let i = 0; i < emailData.length; i++) {
182+
if (emailData[i].headers['x-verify-short-code']) {
183+
code = emailData[i].headers['x-verify-short-code'];
184+
}
185+
}
186+
assert.isDefined(code);
187+
await client.verifyShortCodeEmail(code);
188+
189+
// Add totp to account.
190+
client.totpAuthenticator = new otplib.authenticator.Authenticator();
191+
const totpTokenResult = await client.createTotpToken();
192+
assert.isDefined(totpTokenResult);
193+
60;
194+
client.totpAuthenticator.options = {
195+
secret: totpTokenResult.secret,
196+
crypto: crypto,
197+
};
198+
const totpCode = client.totpAuthenticator.generate();
199+
await client.verifyTotpCode(totpCode);
200+
201+
// Log in again. This creates a new unverified session
202+
client = await Client.login(
203+
config.publicUrl,
204+
email,
205+
password,
66206
testOptions
67-
)
68-
.then((c) => {
69-
c.authPW = Buffer.from(
70-
'0000000000000000000000000000000000000000000000000000000000000000',
71-
'hex'
72-
);
73-
c.authPWVersion2 = Buffer.from(
74-
'0000000000000000000000000000000000000000000000000000000000000000',
75-
'hex'
76-
);
77-
return c.destroyAccount();
78-
})
79-
.then(
80-
(r) => {
81-
assert(false);
82-
},
83-
(err) => {
84-
assert.equal(err.errno, 103);
85-
}
207+
);
208+
const res = await client.emailStatus();
209+
assert.equal(res.sessionVerified, false, 'session not verified');
210+
211+
// Try verifying the the session with a short code. This should
212+
// not be enough to bypass 2FA. This sort of mimics a valid email code
213+
// being stolen...
214+
await client.verifyShortCodeEmail(code);
215+
assert.equal(
216+
(await client.emailStatus()).sessionVerified,
217+
true,
218+
'session should be verified'
219+
);
220+
221+
// Destroying the account should not work. Despite the session being 'verified',
222+
// totp has not been provided.
223+
try {
224+
await client.destroyAccount();
225+
assert.fail(
226+
'Should not be able to destroy account without verifying totp'
86227
);
228+
} catch (error) {
229+
assert.equal(error.errno, 138, 'unverified session');
230+
}
87231
});
88232

89-
it('should fail to delete account with TOTP with unverified session', () => {
233+
it('cannot delete without verifying session', async function () {
90234
const email = server.uniqueEmail();
91235
const password = 'ok';
92-
let client;
93-
return Client.createAndVerifyAndTOTP(
236+
let client = await Client.createAndVerify(
94237
config.publicUrl,
95238
email,
96239
password,
97240
server.mailbox,
98-
{
99-
...testOptions,
100-
keys: true,
101-
}
102-
)
103-
.then(() => {
104-
// Create a new unverified session
105-
return Client.login(config.publicUrl, email, password, testOptions);
106-
})
107-
.then((response) => {
108-
client = response;
109-
return client.emailStatus();
110-
})
111-
.then((res) =>
112-
assert.equal(res.sessionVerified, false, 'session not verified')
113-
)
114-
.then(() => client.destroyAccount())
115-
.then(assert.fail, (err) => {
116-
assert.equal(err.errno, 138, 'unverified session');
117-
});
118-
});
241+
testOptions
242+
);
243+
244+
// Login again requiring email-2fa for session verification. The account is now verified but the session is not.
245+
client = await Client.login(config.publicUrl, email, password, {
246+
...testOptions,
247+
verificationMethod: 'email-2fa',
248+
});
119249

250+
try {
251+
await client.destroyAccount();
252+
assert.fail('Should not be able allowed to destroy account.');
253+
} catch (err) {
254+
assert.equal(err.message, 'Unconfirmed session');
255+
}
256+
});
120257

258+
it('cannot delete without verifying account', async function () {
259+
const email = server.uniqueEmail();
260+
const password = 'ok';
261+
await Client.create(config.publicUrl, email, password, {
262+
...testOptions,
263+
verificationMethod: 'email-2fa',
264+
keys: true,
265+
});
266+
const client = await Client.login(
267+
config.publicUrl,
268+
email,
269+
password,
270+
testOptions
271+
);
272+
try {
273+
await client.destroyAccount();
274+
assert.fail('Should not be able allowed to destroy account.');
275+
} catch (err) {
276+
assert.equal(err.message, 'Unconfirmed account');
277+
}
278+
});
121279
});
122280
});

0 commit comments

Comments
 (0)