Merge pull request #13805 from mozilla/FXA-79-prune-oauth-authz-codez

feat(scripts): add script to prune oauth codes
This commit is contained in:
Barry Chen 2022-08-02 07:42:31 -07:00 коммит произвёл GitHub
Родитель 66b10b6f5e 69920fb77d
Коммит ab49dcb9d5
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 193 добавлений и 19 удалений

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

@ -220,6 +220,12 @@ class OauthDB extends ConnectedServicesDb {
getPocketIds() {
return POCKET_IDS;
}
async pruneAuthorizationCodes(ttlInMs) {
return await this.mysql._pruneAuthorizationCodes(
ttlInMs || config.get('oauthServer.expiration.code')
);
}
}
// Helper functions

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

@ -17,6 +17,7 @@ const { StatsD } = require('hot-shots');
const REQUIRED_SQL_MODES = ['STRICT_ALL_TABLES', 'NO_ENGINE_SUBSTITUTION'];
const QUERY_GET_LOCK = 'SELECT GET_LOCK(?, ?) AS acquired';
const QUERY_RELEASE_LOCK = 'SELECT RELEASE_LOCK(?)';
const QUERY_CLIENT_REGISTER =
'INSERT INTO clients ' +
'(id, name, imageUri, hashedSecret, hashedSecretPrevious, redirectUri,' +
@ -149,6 +150,8 @@ const DELETE_ACTIVE_REFRESH_TOKENS_BY_CLIENT_AND_UID =
'DELETE FROM refreshTokens WHERE clientId=? AND userId=?';
const DELETE_REFRESH_TOKEN_WITH_CLIENT_AND_UID =
'DELETE FROM refreshTokens WHERE token=? AND clientId=? AND userId=?';
const PRUNE_AUTHZ_CODES =
'DELETE FROM codes WHERE TIMESTAMPDIFF(SECOND, createdAt, NOW()) > ? LIMIT 10000';
// Scope queries
const QUERY_SCOPE_FIND = 'SELECT * ' + 'FROM scopes ' + 'WHERE scopes.scope=?;';
@ -187,9 +190,16 @@ class MysqlStore extends MysqlOAuthShared {
}
}
getLock(lockName, timeout = 3) {
// returns `acquired: 1` on success
return this._readOne(QUERY_GET_LOCK, [lockName, timeout]);
async _withLock(cb, lockName, timeout = 3) {
const conn = await this._getConnection();
try {
this._queryWithConnection(conn, QUERY_GET_LOCK, [lockName, timeout]);
return await cb(conn);
} finally {
this._queryWithConnection(conn, QUERY_RELEASE_LOCK, [lockName]);
conn.release();
}
}
// createdAt is DEFAULT NOW() in the schema.sql
@ -452,6 +462,23 @@ class MysqlStore extends MysqlOAuthShared {
return Promise.all([deleteCodes, deleteTokens, deleteRefreshTokens]);
}
async _pruneAuthorizationCodes(ttl) {
const pruneAuthzCodes = async (conn) => {
const ttlInSeconds = ttl / 1000;
await this._queryWithConnection(conn, PRUNE_AUTHZ_CODES, [ttlInSeconds]);
const prunedCount = await this._queryWithConnection(
conn,
'SELECT ROW_COUNT() AS pruned'
);
return firstRow(prunedCount);
};
return await this._withLock(
pruneAuthzCodes,
'fxa-oauth.auth-codes.prune-lock'
);
}
/**
* Delete a specific refresh token, for some clientId and uid.
* Also deletes *all* access tokens for the clientId and uid combination,
@ -662,19 +689,23 @@ class MysqlStore extends MysqlOAuthShared {
async _query(sql, params) {
const conn = await this._getConnection();
try {
return await new Promise(function (resolve, reject) {
conn.query(sql, params || [], function (err, results) {
if (err) {
return reject(err);
}
resolve(results);
});
});
return await this._queryWithConnection(conn, sql, params);
} finally {
conn.release();
}
}
async _queryWithConnection(conn, sql, params) {
return await new Promise(function (resolve, reject) {
conn.query(sql, params || [], function (err, results) {
if (err) {
return reject(err);
}
resolve(results);
});
});
}
getProxyableFunctions() {
return Reflect.ownKeys(MysqlStore.prototype).filter(
(x) => x !== 'constructor' && /^[a-zA-Z]/.test(x)

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

@ -0,0 +1,65 @@
/* 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/. */
import program from 'commander';
import { StatsD } from 'hot-shots';
import { promisify } from 'util';
const pckg = require('../package.json');
const DEFAULT_TTL_MS = 900000;
export async function init() {
// Setup utilities
const config = require('../config').getProperties();
const statsd = new StatsD({ ...config.statsd, maxBufferSize: 0 });
const log = require('../lib/log')(
config.log.level,
'prune-oauth-codes',
statsd
);
const oauthDb = require('../lib/oauth/db');
// Parse args
program
.version(pckg.version)
.option(
'--ttl <number>',
'The TTL of OAuth authorization codes in milliseconds. Defaults to 15 minutes.',
DEFAULT_TTL_MS
)
.on('--help', () =>
console.log('\n\nPrunes up to 10000 expired OAuth authorization codes.')
)
.parse(process.argv);
const ttlInMs = parseInt(program.ttl) || DEFAULT_TTL_MS;
log.info('OAuth codes pruning', { ttl: ttlInMs });
try {
log.info('OAuth codes pruning start', { ttl: ttlInMs });
const result = await oauthDb.pruneAuthorizationCodes(ttlInMs);
statsd.increment('oauth-codes.pruned', result.pruned);
log.info('token pruning complete', result);
} catch (err) {
log.error('error during prune', err);
return 1;
}
await promisify(statsd.close).bind(statsd)();
return 0;
}
if (require.main === module) {
let exitStatus = 1;
init()
.then((result) => {
exitStatus = result;
})
.catch((err) => {
console.error(err);
})
.finally(() => {
process.exit(exitStatus);
});
}

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

@ -12,11 +12,18 @@ const ScopeSet = require('fxa-shared').oauth.scopes;
const encrypt = require('fxa-shared/auth/encrypt');
const db = require('../../../lib/oauth/db');
const config = require('../../../config');
function randomString(len) {
return crypto.randomBytes(Math.ceil(len)).toString('hex');
}
describe('db', function () {
before(async () => {
// some other tests are not cleaning up their authorization codes
await db.pruneAuthorizationCodes(1);
});
describe('utf-8', function () {
function makeTest(clientId, clientName) {
return function () {
@ -574,14 +581,45 @@ describe('db', function () {
});
});
describe('getLock', function () {
it('should return an acquired status', function () {
const lockName = randomString(10);
return db.getLock(lockName, 3).then(function (result) {
assert.ok(result);
assert.ok('acquired' in result);
assert.ok(result.acquired === 1);
});
describe('pruneAuthorizationCodes', () => {
const clientId = buf(randomString(8));
const userId = buf(randomString(16));
const scope = ScopeSet.fromArray(['no_scope']).toString();
const QUERY_CODE_INSERT =
'INSERT INTO codes (code, clientId, userId, scope, createdAt) VALUES (?, ?, ?, ?, DATE_SUB(NOW(), INTERVAL ? SECOND ))';
const insertAuthzCode = async (ageInMs) => {
await db.mysql._query(QUERY_CODE_INSERT, [
randomString(16),
clientId,
userId,
scope,
ageInMs / 1000,
]);
};
it('prunes codes older than the given ttl', async () => {
const ttl = 598989;
const pruneCodesCount = 3;
for (let i = 0; i < pruneCodesCount; i++) {
await insertAuthzCode(ttl + 1000);
}
const validCodesCount = 2;
for (let i = 0; i < validCodesCount; i++) {
await insertAuthzCode(1);
}
const res = await db.pruneAuthorizationCodes(ttl);
assert.equal(res.pruned, pruneCodesCount);
});
it('prunes codes older than the default ttl', async () => {
const ttl = config.get('oauthServer.expiration.code');
const codesCount = 7;
for (let i = 0; i < codesCount; i++) {
await insertAuthzCode(ttl + 1000);
}
const res = await db.pruneAuthorizationCodes();
assert.equal(res.pruned, codesCount);
});
});

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

@ -0,0 +1,34 @@
/* 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/. */
'use strict';
const ROOT_DIR = '../..';
const cp = require('child_process');
const util = require('util');
const path = require('path');
const execAsync = util.promisify(cp.exec);
const cwd = path.resolve(__dirname, ROOT_DIR);
const execOptions = {
cwd,
};
describe('scripts/prune-oauth-authorization-codes:', () => {
it('does not fail with no argument', () => {
return execAsync(
'node -r esbuild-register scripts/prune-oauth-authorization-codes',
execOptions
);
});
it('does not fail with an argument', () => {
return execAsync(
'node -r esbuild-register scripts/prune-oauth-authorization-codes --ttl 600000',
execOptions
);
});
});