Skip to content

Commit b86702e

Browse files
authored
Merge pull request #19536 from mozilla/FXA-12490
chore(script): Add Pocket oauth access token pruning script
2 parents 776cdc6 + 9b50299 commit b86702e

3 files changed

Lines changed: 512 additions & 0 deletions

File tree

packages/fxa-auth-server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"populate-vat-taxes": "CONFIG_FILES='config/secrets.json' node -r esbuild-register ./scripts/populate-vat-taxes.ts",
4747
"paypal-processor": "CONFIG_FILES='config/secrets.json' node -r esbuild-register ./scripts/paypal-processor.ts",
4848
"prune-tokens": "CONFIG_FILES='config/secrets.json' node -r esbuild-register ./scripts/prune-tokens.ts",
49+
"prune-pocket-access-tokens": "CONFIG_FILES='config/secrets.json' node -r esbuild-register ./scripts/prune-pocket-access-tokens.ts",
4950
"subscription-reminders": "CONFIG_FILES='config/secrets.json' node -r esbuild-register ./scripts/subscription-reminders.ts",
5051
"audit-orphaned-stripe-accounts": "CONFIG_FILES='config/secrets.json' node -r esbuild-register ./scripts/audit-orphaned-customers.ts",
5152
"remove-unverified-accounts": "CONFIG_FILES='config/secrets.json' node -r esbuild-register ./scripts/remove-unverified-accounts.ts",
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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+
/*/
6+
This script is used to perform a one-time cleanup of all Pocket OAuth
7+
access tokens stored in fxa_oauth.tokens. Pocket was FxA's first OAuth client
8+
and used a special workaround where access tokens were persisted in MySQL
9+
instead of Redis to avoid implementing refresh tokens.
10+
11+
This script will:
12+
1. Delete all access tokens for Pocket client IDs from the tokens table
13+
2. Optionally run in dry-run mode to see what would be deleted
14+
/*/
15+
16+
import { StatsD } from 'hot-shots';
17+
import program from 'commander';
18+
import { setupDatabase } from 'fxa-shared/db';
19+
import { BaseAuthModel } from 'fxa-shared/db/models/auth';
20+
21+
const pckg = require('../package.json');
22+
const config = require('../config').default.getProperties();
23+
const statsd = new StatsD(config.statsd);
24+
const log = require('../lib/log')(
25+
config.log.level,
26+
'prune-pocket-access-tokens',
27+
statsd
28+
);
29+
30+
// Pocket client IDs from config
31+
const POCKET_CLIENT_IDS = [
32+
'749818d3f2e7857f', // pocket-web
33+
'7377719276ad44ee', // pocket-mobile
34+
];
35+
36+
const DEFAULT_BATCH_SIZE = 1000;
37+
const DEFAULT_WAIT_MS = 1000;
38+
39+
export async function init() {
40+
program
41+
.version(pckg.version)
42+
.option(
43+
'--dry-run [boolean]',
44+
'Run in dry-run mode to see what would be deleted without actually deleting (default: true)',
45+
true
46+
)
47+
.option(
48+
'--batch-size <number>',
49+
'Number of tokens to delete per batch (default: 1000)',
50+
DEFAULT_BATCH_SIZE.toString()
51+
)
52+
.option(
53+
'--wait <number>',
54+
'Milliseconds to wait between batches in ms (default: 1000)',
55+
DEFAULT_WAIT_MS.toString()
56+
)
57+
.on('--help', function () {
58+
console.log(`
59+
Example:
60+
61+
> ./scripts/prune-pocket-access-tokens.sh
62+
> ./scripts/prune-pocket-access-tokens.sh --dry-run=false --batch-size=500 --wait=2000
63+
64+
Exit Codes:
65+
0 - success
66+
1 - unexpected error
67+
2 - error during initialization`);
68+
})
69+
.parse(process.argv);
70+
71+
const dryRun = program.dryRun !== 'false' && program.dryRun !== false;
72+
const batchSize = parseInt(program.batchSize) || DEFAULT_BATCH_SIZE;
73+
const waitMs = parseInt(program.wait) || DEFAULT_WAIT_MS;
74+
const sleep = () => new Promise((resolve) => setTimeout(resolve, waitMs));
75+
76+
log.info('Pocket token pruning start', {
77+
dryRun,
78+
batchSize,
79+
waitMs,
80+
pocketClientIds: POCKET_CLIENT_IDS,
81+
});
82+
83+
// Setup Knex Connection
84+
let db;
85+
try {
86+
db = setupDatabase({
87+
...config.oauthServer.mysql,
88+
});
89+
90+
// Binds knex once, which effectively binds for all BaseAuthModel inherited types
91+
BaseAuthModel.knex(db);
92+
} catch (err) {
93+
log.error('error during knex initialization', err);
94+
return 2;
95+
}
96+
97+
try {
98+
for (const clientId of POCKET_CLIENT_IDS) {
99+
const clientIdBuffer = Buffer.from(clientId, 'hex');
100+
101+
const countResult = await db('tokens')
102+
.where('clientId', clientIdBuffer)
103+
.count('* as total');
104+
const tokenCount = countResult[0].total;
105+
106+
console.log(
107+
`Found ${tokenCount} Pocket access tokens for client ID ${clientId}.`
108+
);
109+
110+
if (tokenCount === 0) {
111+
console.log(
112+
`No Pocket access tokens to delete for client ID ${clientId}.`
113+
);
114+
}
115+
116+
if (dryRun) {
117+
console.log(
118+
'Dry run mode is on. It is the default; use --dry-run=false when you are ready.'
119+
);
120+
} else {
121+
// Delete the tokens in batches
122+
let totalDeleted = 0;
123+
let batchCount = 0;
124+
let deleteCount = 0;
125+
126+
do {
127+
deleteCount = await db('tokens')
128+
.where('clientId', clientIdBuffer)
129+
.limit(batchSize)
130+
.del();
131+
132+
if (deleteCount > 0) {
133+
totalDeleted += deleteCount;
134+
batchCount++;
135+
136+
log.info('Deleted batch of Pocket tokens', {
137+
clientId,
138+
batchNumber: batchCount,
139+
batchDeleteCount: deleteCount,
140+
totalDeleted,
141+
remaining: tokenCount - totalDeleted,
142+
});
143+
144+
statsd.increment('pocket_tokens_deleted', deleteCount, {
145+
client_id: clientId,
146+
});
147+
148+
// Don't sleep after the last batch
149+
if (deleteCount === batchSize) {
150+
await sleep();
151+
}
152+
}
153+
} while (deleteCount > 0);
154+
155+
log.info('Deleted all pocket tokens for client', {
156+
clientId,
157+
totalDeleted,
158+
batches: batchCount,
159+
});
160+
}
161+
}
162+
163+
console.log('Pocket access token pruning complete!');
164+
return 0;
165+
} catch (err) {
166+
log.error('error during pocket token prune', err);
167+
return 1;
168+
} finally {
169+
if (db) {
170+
await db.destroy();
171+
}
172+
}
173+
}
174+
175+
if (require.main === module) {
176+
let exitStatus = 1;
177+
init()
178+
.then((result) => {
179+
exitStatus = result;
180+
})
181+
.catch((err) => {
182+
console.error(err);
183+
})
184+
.then(() => {
185+
// Make sure statsd closes cleanly so we don't lose any metrics
186+
return new Promise((resolve) => {
187+
statsd.close((err) => {
188+
if (err) {
189+
log.warn('statsd', { closed: true, err });
190+
} else {
191+
log.info('statsd', { closed: true });
192+
}
193+
resolve(true);
194+
});
195+
});
196+
})
197+
.finally(() => {
198+
process.exit(exitStatus);
199+
});
200+
}

0 commit comments

Comments
 (0)