Skip to content

Commit 1d75626

Browse files
authored
Merge pull request #704 from charles-openclaw/fix-auth-challenge-sweep
fix(auth): sweep expired challenges
2 parents d60e2c2 + 97271c4 commit 1d75626

2 files changed

Lines changed: 66 additions & 0 deletions

File tree

backend/src/middleware/auth.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,44 @@ const STELLAR_NETWORK =
1414

1515
// In-memory challenge store: publicKey -> { nonce, expiresAt }
1616
const challenges = new Map<string, { nonce: string; expiresAt: number }>();
17+
const DEFAULT_CHALLENGE_SWEEP_INTERVAL_MS = 5 * 60 * 1000;
18+
let challengeSweepTimer: NodeJS.Timeout | undefined;
19+
20+
function resolveChallengeSweepIntervalMs(): number {
21+
const raw = process.env.AUTH_CHALLENGE_SWEEP_INTERVAL_MS;
22+
if (!raw) return DEFAULT_CHALLENGE_SWEEP_INTERVAL_MS;
23+
const parsed = Number(raw);
24+
return Number.isFinite(parsed) && parsed > 0
25+
? parsed
26+
: DEFAULT_CHALLENGE_SWEEP_INTERVAL_MS;
27+
}
28+
29+
export function sweepExpiredChallenges(now = Date.now()): number {
30+
let deleted = 0;
31+
for (const [publicKey, challenge] of challenges.entries()) {
32+
if (challenge.expiresAt < now) {
33+
challenges.delete(publicKey);
34+
deleted += 1;
35+
}
36+
}
37+
return deleted;
38+
}
39+
40+
export function startChallengeSweep(intervalMs = resolveChallengeSweepIntervalMs()): void {
41+
if (challengeSweepTimer) return;
42+
challengeSweepTimer = setInterval(() => {
43+
sweepExpiredChallenges();
44+
}, intervalMs);
45+
challengeSweepTimer.unref?.();
46+
}
47+
48+
export function stopChallengeSweep(): void {
49+
if (!challengeSweepTimer) return;
50+
clearInterval(challengeSweepTimer);
51+
challengeSweepTimer = undefined;
52+
}
53+
54+
startChallengeSweep();
1755

1856
// ─── Minimal JWT (no external dep) ──────────────────────────────────────────
1957

@@ -62,6 +100,13 @@ export function issueChallenge(req: Request, res: Response): void {
62100
res.json({ nonce, expiresAt: Date.now() + 60_000 });
63101
}
64102

103+
export const __authChallengeTestUtils = {
104+
challenges,
105+
startChallengeSweep,
106+
stopChallengeSweep,
107+
sweepExpiredChallenges,
108+
};
109+
65110
export function verifyChallenge(req: Request, res: Response): void {
66111
const { publicKey, signedTransaction } = req.body as {
67112
publicKey?: string;

backend/tests/auth.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import request from 'supertest';
33
import * as crypto from 'crypto';
44
import * as StellarSdk from '@stellar/stellar-sdk';
55
import app from '../src/app.js';
6+
import { __authChallengeTestUtils } from '../src/middleware/auth.js';
67

78
// ─── Helpers ─────────────────────────────────────────────────────────────────
89

@@ -39,6 +40,7 @@ function buildSignedTransaction(keypair: StellarSdk.Keypair, nonce: string): str
3940
describe('Authentication & Middleware Tests', () => {
4041
beforeEach(() => {
4142
vi.restoreAllMocks();
43+
__authChallengeTestUtils.challenges.clear();
4244
});
4345

4446
describe('POST /v1/auth/challenge', () => {
@@ -56,6 +58,25 @@ describe('Authentication & Middleware Tests', () => {
5658
expect(res.body).toHaveProperty('expiresAt');
5759
expect(res.body.expiresAt).toBeGreaterThan(Date.now());
5860
});
61+
62+
it('test_challenge_sweep_removes_expired_entries', () => {
63+
const expiredKey = makeKeypair().publicKey();
64+
const activeKey = makeKeypair().publicKey();
65+
const now = Date.now();
66+
67+
__authChallengeTestUtils.challenges.set(expiredKey, {
68+
nonce: crypto.randomBytes(32).toString('hex'),
69+
expiresAt: now - 1,
70+
});
71+
__authChallengeTestUtils.challenges.set(activeKey, {
72+
nonce: crypto.randomBytes(32).toString('hex'),
73+
expiresAt: now + 60_000,
74+
});
75+
76+
expect(__authChallengeTestUtils.sweepExpiredChallenges(now)).toBe(1);
77+
expect(__authChallengeTestUtils.challenges.has(expiredKey)).toBe(false);
78+
expect(__authChallengeTestUtils.challenges.has(activeKey)).toBe(true);
79+
});
5980
});
6081

6182
describe('POST /v1/auth/verify', () => {

0 commit comments

Comments
 (0)