123 lines
4.7 KiB
JavaScript
123 lines
4.7 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { createHash, randomUUID } from 'node:crypto';
|
|
import { createRequire } from 'node:module';
|
|
|
|
const redisUrl = process.env.AUTH_REDIS_INTEGRATION_URL;
|
|
if (!redisUrl) {
|
|
throw new Error('AUTH_REDIS_INTEGRATION_URL is required; use an isolated Redis database');
|
|
}
|
|
|
|
process.env.REDIS_URL = redisUrl;
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const { SessionService } = require('../../api/dist/auth/session.service.js');
|
|
const IORedis = require('../../api/node_modules/ioredis');
|
|
|
|
const redis = new IORedis(redisUrl, { enableReadyCheck: true, maxRetriesPerRequest: 1 });
|
|
const first = new SessionService();
|
|
const second = new SessionService();
|
|
const runId = randomUUID();
|
|
const digest = (value) => createHash('sha256').update(value.trim()).digest('hex');
|
|
const keys = new Set();
|
|
|
|
function remember(...values) {
|
|
for (const value of values) keys.add(value);
|
|
}
|
|
|
|
async function ttlInRange(key, minimum, maximum) {
|
|
const ttl = await redis.ttl(key);
|
|
assert.ok(ttl >= minimum && ttl <= maximum, `${key} TTL ${ttl} is outside ${minimum}..${maximum}`);
|
|
}
|
|
|
|
try {
|
|
assert.equal(await redis.ping(), 'PONG');
|
|
|
|
const { token } = await first.create(`integration-user-${runId}`, 'client', 1);
|
|
const sessionKey = `cmpp:auth:session:${digest(token)}`;
|
|
remember(sessionKey);
|
|
const crossInstanceSession = await second.validate(token, false);
|
|
assert.equal(crossInstanceSession.status, 'active');
|
|
assert.equal(crossInstanceSession.record.userId, `integration-user-${runId}`);
|
|
|
|
const captchaId = `integration-captcha-${runId}`;
|
|
const captchaKey = `cmpp:auth:captcha:${captchaId}`;
|
|
remember(captchaKey);
|
|
await first.storeCaptcha(captchaId, '8291', 120);
|
|
assert.equal(await second.consumeCaptcha(captchaId), '8291');
|
|
assert.equal(await first.consumeCaptcha(captchaId), null);
|
|
|
|
const captchaIp = `198.51.100.${(Number.parseInt(runId.slice(0, 2), 16) % 200) + 1}`;
|
|
const captchaRateKey = `cmpp:auth:captcha-rate:ip:${digest(captchaIp)}`;
|
|
remember(captchaRateKey);
|
|
for (let index = 0; index < 30; index += 1) {
|
|
assert.equal(await (index % 2 === 0 ? first : second).assertCaptchaRequestAllowed(captchaIp), true);
|
|
}
|
|
assert.equal(await second.assertCaptchaRequestAllowed(captchaIp), false);
|
|
await ttlInRange(captchaRateKey, 1, 300);
|
|
|
|
const pairLogin = `pair-${runId}@integration.invalid`;
|
|
const pairIp = '198.51.100.210';
|
|
const accountDigest = digest(pairLogin.toLowerCase());
|
|
const ipDigest = digest(pairIp);
|
|
const pairDigest = digest(`${accountDigest}:${ipDigest}`);
|
|
const pairKeys = [
|
|
`cmpp:auth:failure:${accountDigest}`,
|
|
`cmpp:auth:failure:ip:${ipDigest}`,
|
|
`cmpp:auth:failure:pair:${pairDigest}`,
|
|
`cmpp:auth:lock:${accountDigest}`,
|
|
`cmpp:auth:lock:ip:${ipDigest}`,
|
|
`cmpp:auth:lock:pair:${pairDigest}`,
|
|
];
|
|
remember(...pairKeys);
|
|
for (let index = 0; index < 5; index += 1) {
|
|
await (index % 2 === 0 ? first : second).recordAnonymousLoginFailure(pairLogin, pairIp);
|
|
}
|
|
assert.equal(await second.anonymousLoginLockScope(pairLogin, pairIp), 'account');
|
|
await ttlInRange(pairKeys[0], 86_000, 86_400);
|
|
await ttlInRange(pairKeys[3], 86_000, 86_400);
|
|
await ttlInRange(pairKeys[2], 86_000, 86_400);
|
|
await ttlInRange(pairKeys[5], 86_000, 86_400);
|
|
|
|
const scanIp = '198.51.100.211';
|
|
const scanIpDigest = digest(scanIp);
|
|
const scanFailureKey = `cmpp:auth:failure:ip:${scanIpDigest}`;
|
|
const scanLockKey = `cmpp:auth:lock:ip:${scanIpDigest}`;
|
|
remember(scanFailureKey, scanLockKey);
|
|
for (let index = 0; index < 30; index += 1) {
|
|
const login = `scan-${runId}-${index}@integration.invalid`;
|
|
const loginDigest = digest(login.toLowerCase());
|
|
const loginPairDigest = digest(`${loginDigest}:${scanIpDigest}`);
|
|
remember(
|
|
`cmpp:auth:failure:${loginDigest}`,
|
|
`cmpp:auth:failure:pair:${loginPairDigest}`,
|
|
`cmpp:auth:lock:${loginDigest}`,
|
|
`cmpp:auth:lock:pair:${loginPairDigest}`,
|
|
);
|
|
await (index % 2 === 0 ? first : second).recordAnonymousLoginFailure(login, scanIp);
|
|
}
|
|
assert.equal(await first.anonymousLoginLockScope(`fresh-${runId}@integration.invalid`, scanIp), 'ip');
|
|
await ttlInRange(scanFailureKey, 600, 900);
|
|
await ttlInRange(scanLockKey, 600, 900);
|
|
|
|
const listedKeys = await redis.keys(`cmpp:auth:*${runId}*`);
|
|
assert.equal(listedKeys.length, 0, 'raw login/run identifiers must not appear in Redis keys');
|
|
|
|
console.log(
|
|
JSON.stringify({
|
|
status: 'passed',
|
|
sharedSession: true,
|
|
oneTimeCaptcha: true,
|
|
captchaRateLimit: true,
|
|
accountAndPairLock: true,
|
|
randomAccountIpLock: true,
|
|
ttlVerified: true,
|
|
hashedKeysOnly: true,
|
|
}),
|
|
);
|
|
} finally {
|
|
if (keys.size > 0) await redis.del(...keys);
|
|
await first.onModuleDestroy();
|
|
await second.onModuleDestroy();
|
|
redis.disconnect();
|
|
}
|