Skip to content

Commit c943522

Browse files
authored
feat(deletion): revoke fxa tokens when deleting accounts (#897)
* feat(deletion): revoke fxa tokens when deleting accounts This data was deleted before on the Pocket side, but now it will remove Pocket from integrations on the Mozilla account page. [POCKET-9990] * fix(test): add reference to user_firefox_account for test seeds * chore: fix a lost import and nock
1 parent 8980227 commit c943522

File tree

15 files changed

+282
-36
lines changed

15 files changed

+282
-36
lines changed

infrastructure/account-data-deleter/src/dataDeleterApp.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,18 @@ export class DataDeleterApp extends Construct {
217217
name: 'EXPORT_SIGNEDURL_USER_SECRET_KEY',
218218
valueFrom: `arn:aws:secretsmanager:${region.name}:${caller.accountId}:secret:${config.name}/${config.environment}/EXPORT_USER_CREDS:secretAccessKey::`,
219219
},
220+
{
221+
name: 'FXA_CLIENT_ID',
222+
valueFrom: `arn:aws:ssm:${region.name}:${caller.accountId}:parameter/Web/${config.environment}/FIREFOX_WEB_AUTH_CLIENT_ID`,
223+
},
224+
{
225+
name: 'FXA_CLIENT_SECRET',
226+
valueFrom: `arn:aws:ssm:${region.name}:${caller.accountId}:parameter/Web/${config.environment}/FIREFOX_WEB_AUTH_CLIENT_SECRET`,
227+
},
228+
{
229+
name: 'FXA_OAUTH_URL',
230+
valueFrom: `arn:aws:ssm:${region.name}:${caller.accountId}:parameter/Web/${config.environment}/FIREFOX_AUTH_OAUTH_URL`,
231+
},
220232
],
221233
},
222234
],

lambdas/account-data-deleter-events/src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const config = {
88
'https://account-data-deleter-api.getpocket.dev',
99
queueDeletePath: '/queueDelete',
1010
stripeDeletePath: '/stripeDelete',
11+
fxaRevokePath: '/revokeFxa',
1112
sentry: {
1213
// these values are inserted into the environment in
1314
// .aws/src/.ts

lambdas/account-data-deleter-events/src/handlers/accountDelete/index.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ describe('accountDelete handler', () => {
1717
}),
1818
};
1919

20+
beforeEach(() => {
21+
nock(config.endpoint).post(config.fxaRevokePath).reply(200);
22+
});
23+
2024
afterEach(() => {
2125
nock.cleanAll();
2226
jest.restoreAllMocks();

lambdas/account-data-deleter-events/src/handlers/accountDelete/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AccountDeleteEvent } from '../../schemas/accountDeleteEvent.ts';
22
import {
33
callQueueDeleteEndpoint,
44
callStripeDeleteEndpoint,
5+
callFxARevokeEndpoint,
56
} from './postRequest.ts';
67
import { SQSRecord } from 'aws-lambda';
78
import { AggregateError } from '../../errors/AggregateError.ts';
@@ -47,6 +48,7 @@ export function validatePostBody(
4748
* rows from Pocket's internal database
4849
* * stripe API - deletes stripe customer data (and internal
4950
* stripe-related data in database)
51+
* * FxA Auth API - revokes access tokens from FxA (Mozilla Accounts)
5052
* @param record the event forwarded from event bridge via SQS
5153
* @throws Error if the record body does not conform to expected schema
5254
* @throws AggregateError if any errors encountered making
@@ -59,10 +61,12 @@ export async function accountDeleteHandler(record: SQSRecord): Promise<void> {
5961
const postBody = validatePostBody(message);
6062
const queueRes = await callQueueDeleteEndpoint(postBody);
6163
const stripeRes = await callStripeDeleteEndpoint(postBody);
64+
const fxaRes = await callFxARevokeEndpoint(postBody);
6265
const errors: Error[] = [];
6366
for await (const { endpoint, res } of [
6467
{ endpoint: 'queueDelete', res: queueRes },
6568
{ endpoint: 'stripeDelete', res: stripeRes },
69+
{ endpoint: 'fxaDelete', res: fxaRes },
6670
]) {
6771
if (!res.ok) {
6872
const data = await res.json();

lambdas/account-data-deleter-events/src/handlers/accountDelete/postRequest.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,12 @@ export async function callQueueDeleteEndpoint(body: any): Promise<any> {
5151
export async function callStripeDeleteEndpoint(body: any): Promise<any> {
5252
return postRequest(body, config.stripeDeletePath, config.endpoint);
5353
}
54+
55+
/**
56+
* Revoke FxA Access Token when a user deletes their account
57+
* @param body
58+
* @returns
59+
*/
60+
export async function callFxARevokeEndpoint(body: any): Promise<any> {
61+
return postRequest(body, config.fxaRevokePath, config.endpoint);
62+
}

pnpm-lock.yaml

Lines changed: 25 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

servers/account-data-deleter/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"csv-stringify": "^6.5.1",
4141
"express": "4.21.2",
4242
"express-validator": "^7.1.0",
43+
"fetch-retry": "^5.0.6",
4344
"knex": "3.1.0",
4445
"lodash": "4.17.21",
4546
"mysql2": "3.12.0",

servers/account-data-deleter/src/config/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ export const config = {
7979
apiVersion: '2024-06-20' as const,
8080
productId: 7,
8181
},
82+
fxa: {
83+
clientId: process.env.FXA_CLIENT_ID || 'somefakeclientid',
84+
secret: process.env.FXA_CLIENT_SECRET || 'somefakesecret',
85+
oauthEndpoint: process.env.FXA_OAUTH_URL || 'https://localhost/',
86+
version: 'v1',
87+
},
8288
database: {
8389
// contains tables for user, list, tags, annotations, etc.
8490
read: {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { FxaRevoker } from './FxaRevoker';
2+
3+
describe('FxARevoker', () => {
4+
afterEach(() => {
5+
jest.restoreAllMocks();
6+
});
7+
describe('with an access token', () => {
8+
beforeEach(() => {
9+
jest
10+
.spyOn(FxaRevoker.prototype, 'fetchAccessToken')
11+
.mockResolvedValue('accessme123');
12+
});
13+
it('returns false if revoking throws an error', async () => {
14+
jest
15+
.spyOn(FxaRevoker.prototype, 'requestRevokeToken')
16+
.mockRejectedValueOnce(new Error('error fetching'));
17+
const res = await new FxaRevoker('abc123').revokeToken();
18+
expect(res).toBeFalse();
19+
});
20+
it('returns false if revoking response is not ok', async () => {
21+
jest
22+
.spyOn(FxaRevoker.prototype, 'requestRevokeToken')
23+
.mockResolvedValueOnce(new Response(null, { status: 500 }));
24+
const res = await new FxaRevoker('abc123').revokeToken();
25+
expect(res).toBeFalse();
26+
});
27+
it('returns false if db delete method throws error', async () => {
28+
jest
29+
.spyOn(FxaRevoker.prototype, 'deleteAuthRecord')
30+
.mockRejectedValueOnce(new Error('error deleting'));
31+
const res = await new FxaRevoker('abc123').revokeToken();
32+
expect(res).toBeFalse();
33+
});
34+
it('returns true if process does not error', async () => {
35+
jest
36+
.spyOn(FxaRevoker.prototype, 'requestRevokeToken')
37+
.mockResolvedValueOnce(new Response(null, { status: 200 }));
38+
jest
39+
.spyOn(FxaRevoker.prototype, 'deleteAuthRecord')
40+
.mockResolvedValueOnce(1);
41+
const res = await new FxaRevoker('abc123').revokeToken();
42+
expect(res).toBeTrue();
43+
});
44+
});
45+
it('returns true if no FxA tokens exist', async () => {
46+
jest
47+
.spyOn(FxaRevoker.prototype, 'fetchAccessToken')
48+
.mockResolvedValueOnce(undefined);
49+
const res = await new FxaRevoker('abc123').revokeToken();
50+
expect(res).toBeTrue();
51+
});
52+
});

0 commit comments

Comments
 (0)