diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 799c49e..d816693 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -19,7 +19,13 @@ export class AuthController { } /** - * Refresh access token using a valid refresh token + * Refresh access token using a valid refresh token. + * + * On success the consumed refresh token is revoked and a fresh token pair + * (access + refresh) is returned. Single-use enforcement means a reused + * token is an unambiguous theft signal: all tokens for the user are + * immediately revoked and the request is rejected with 401. + * * POST /auth/refresh */ async refreshToken(req: Request, res: Response, next: NextFunction): Promise { @@ -53,12 +59,15 @@ export class AuthController { return; } - // Check if token is revoked or expired + // Reuse detection — token is already revoked, which means it was + // previously rotated. Presenting it again is a theft signal. + // Revoke all user tokens and reject the request. if (storedToken.isRevoked) { await this.refreshTokenService.handleReuse(storedToken, this.refreshTokenRepository); - logger.warn('[AuthController] Attempted to use revoked refresh token', { + logger.warn('[AuthController] Theft signal: revoked token presented again — all user tokens revoked', { tokenId: tokenPayload.tokenId, - userId: tokenPayload.userId + userId: tokenPayload.userId, + familyId: storedToken.familyId }); next(new UnauthorizedError('Refresh token has been revoked', 'REVOKED_TOKEN')); return; @@ -83,22 +92,24 @@ export class AuthController { return; } - // Update last used timestamp - await this.refreshTokenRepository.updateLastUsed(storedToken.id, storedToken.userId); - - // Generate new access token - const newAccessToken = this.refreshTokenService.refreshAccessToken( + // Rotate: revoke consumed token, issue fresh access + refresh token pair + // in the same family so the entire lineage is covered by theft detection. + const newTokenPair = await this.refreshTokenService.rotateRefreshToken( + storedToken, storedToken.userId, - undefined // walletAddress not available in refresh flow + undefined, // walletAddress not available in refresh flow + this.refreshTokenRepository ); - logger.info('[AuthController] Access token refreshed successfully', { + logger.info('[AuthController] Token pair rotated successfully', { userId: storedToken.userId, - tokenId: storedToken.id + consumedTokenId: storedToken.id, + familyId: storedToken.familyId }); res.json({ - accessToken: newAccessToken, + accessToken: newTokenPair.accessToken, + refreshToken: newTokenPair.refreshToken, tokenType: 'Bearer' }); @@ -174,7 +185,6 @@ export class AuthController { */ async revokeAllTokens(req: Request, res: Response, next: NextFunction): Promise { try { - // This endpoint should be protected by requireAuth middleware const userId = (req as any).developerId || res.locals.authenticatedUser?.id; if (!userId) { @@ -211,7 +221,7 @@ export class AuthController { res.json({ activeRefreshTokens: activeTokenCount, - maxAllowedTokens: 5 // Configurable limit + maxAllowedTokens: 5 }); } catch (error) { diff --git a/src/services/refreshTokenService.ts b/src/services/refreshTokenService.ts index fb72340..caeb63d 100644 --- a/src/services/refreshTokenService.ts +++ b/src/services/refreshTokenService.ts @@ -41,12 +41,13 @@ export class RefreshTokenService { } /** - * Create access and refresh token pair + * Create access and refresh token pair. + * Both tokens share the same tokenId so the refresh token record + * can be located by ID during the rotation flow. */ createTokenPair(userId: string, walletAddress?: string): TokenPair { const tokenId = this.generateTokenId(); - // Create access token (short-lived) const accessTokenPayload: AccessTokenPayload = { userId, walletAddress, @@ -58,7 +59,6 @@ export class RefreshTokenService { algorithm: 'HS256' }); - // Create refresh token (long-lived) const refreshTokenPayload: RefreshTokenPayload = { userId, tokenId, @@ -70,10 +70,7 @@ export class RefreshTokenService { algorithm: 'HS256' }); - return { - accessToken, - refreshToken - }; + return { accessToken, refreshToken }; } /** @@ -194,14 +191,71 @@ export class RefreshTokenService { } /** - * Handle refresh token reuse: revoke the entire family atomically and log audit event + * Rotate a refresh token — revoke the consumed token and issue a new one + * in the same family. This is called on every successful refresh so that + * each refresh token can only be used once. Single-use enforcement makes + * theft detectable: if the old token is presented again after rotation, + * `isRevoked` will be true and `handleReuse` will fire. + * + * @param consumedToken - The refresh token record that was just validated + * @param userId - Owner of the token + * @param walletAddress - Optional wallet address to embed in the new access token + * @param repository - Token repository for persistence + * @returns A fresh { accessToken, refreshToken } pair in the same family */ - async handleReuse(storedToken: RefreshToken, repository: RefreshTokenRepository): Promise { - logger.warn('[RefreshTokenService] Confirmed refresh token reuse detected. Revoking entire family.', { - familyId: storedToken.familyId, - userId: storedToken.userId, - tokenId: storedToken.id + async rotateRefreshToken( + consumedToken: RefreshToken, + userId: string, + walletAddress: string | undefined, + repository: RefreshTokenRepository + ): Promise { + // 1. Revoke the consumed token so it cannot be reused + await repository.revokeRefreshToken(consumedToken.id, userId); + + // 2. Issue a new token pair — carry the familyId forward so theft + // detection covers the entire lineage + const newPair = this.createTokenPair(userId, walletAddress); + const newRecord = this.createRefreshTokenRecord(userId, newPair.refreshToken, consumedToken.familyId); + await repository.createRefreshToken(newRecord); + + logger.info('[RefreshTokenService] Refresh token rotated', { + userId, + consumedTokenId: consumedToken.id, + newTokenId: newRecord.id, + familyId: consumedToken.familyId }); - await repository.revokeFamily(storedToken.familyId, storedToken.userId); + + return newPair; + } + + /** + * Handle refresh token reuse (theft signal). + * + * When a token that is already revoked is presented again it means one of: + * a) The legitimate user's token was stolen and the attacker rotated it, + * leaving the victim holding a now-revoked token. + * b) The attacker's rotated token was stolen back by the legitimate user. + * + * In either case we cannot tell who is legitimate, so the safest response + * is to revoke ALL tokens for the user, forcing a full re-authentication. + * Revoking only the family is insufficient because an attacker who has + * already rotated the token may have started a new family. + * + * @param storedToken - The revoked token record that was presented again + * @param repository - Token repository for persistence + */ + async handleReuse(storedToken: RefreshToken, repository: RefreshTokenRepository): Promise { + logger.warn( + '[RefreshTokenService] Refresh token reuse detected — revoking ALL user tokens as theft countermeasure.', + { + userId: storedToken.userId, + familyId: storedToken.familyId, + tokenId: storedToken.id + } + ); + + // Revoke every token for this user, not just the family, because the + // attacker may have rotated into a new family after the initial theft. + await repository.revokeAllUserTokens(storedToken.userId); } } diff --git a/tests/integration/refreshToken.test.ts b/tests/integration/refreshToken.test.ts index ec6afdc..20ea319 100644 --- a/tests/integration/refreshToken.test.ts +++ b/tests/integration/refreshToken.test.ts @@ -6,9 +6,11 @@ import { AuthController } from '../../src/controllers/authController.js'; import { createAuthRoutes } from '../../src/routes/authRoutes.js'; import { errorHandler } from '../../src/middleware/errorHandler.js'; import { TEST_JWT_SECRET } from '../helpers/jwt.js'; -import { createTestDb } from '../helpers/db.js'; -// Mock repository for testing +// --------------------------------------------------------------------------- +// Mock repository +// --------------------------------------------------------------------------- + class MockRefreshTokenRepository { private tokens: Map = new Map(); @@ -38,7 +40,7 @@ class MockRefreshTokenRepository { } async updateLastUsed(tokenId: string, userId: string): Promise { - for (const [id, token] of this.tokens.entries()) { + for (const token of this.tokens.values()) { if (token.id === tokenId && token.userId === userId) { token.lastUsedAt = new Date(); break; @@ -47,7 +49,7 @@ class MockRefreshTokenRepository { } async revokeRefreshToken(tokenId: string, userId: string): Promise { - for (const [id, token] of this.tokens.entries()) { + for (const token of this.tokens.values()) { if (token.id === tokenId && token.userId === userId) { token.isRevoked = true; break; @@ -91,13 +93,23 @@ class MockRefreshTokenRepository { } return count; } + + /** Test helper: return all tokens for a user */ + getAllForUser(userId: string): any[] { + return Array.from(this.tokens.values()).filter(t => t.userId === userId); + } } -function buildTestApp(refreshTokenService: RefreshTokenService, mockRepository: MockRefreshTokenRepository) { +// --------------------------------------------------------------------------- +// App builder +// --------------------------------------------------------------------------- + +function buildTestApp( + refreshTokenService: RefreshTokenService, + mockRepository: MockRefreshTokenRepository +) { const app = express(); app.use(express.json()); - - // Set up JWT secret for testing process.env.JWT_SECRET = TEST_JWT_SECRET; const authController = new AuthController({ @@ -107,10 +119,13 @@ function buildTestApp(refreshTokenService: RefreshTokenService, mockRepository: app.use('/auth', createAuthRoutes(authController)); app.use(errorHandler); - return app; } +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + describe('Refresh Token Integration Tests', () => { let app: express.Express; let refreshTokenService: RefreshTokenService; @@ -126,12 +141,12 @@ describe('Refresh Token Integration Tests', () => { app = buildTestApp(refreshTokenService, mockRepository); }); + // ── POST /auth/refresh ───────────────────────────────────────────────────── + describe('POST /auth/refresh', () => { - it('should refresh access token with valid refresh token', async () => { + it('should return both a new access token and a new refresh token on success', async () => { const userId = 'test-user-123'; const tokenPair = refreshTokenService.createTokenPair(userId); - - // Store the refresh token in mock repository const tokenRecord = refreshTokenService.createRefreshTokenRecord(userId, tokenPair.refreshToken); await mockRepository.createRefreshToken(tokenRecord); @@ -141,19 +156,78 @@ describe('Refresh Token Integration Tests', () => { expect(res.status).toBe(200); expect(res.body).toHaveProperty('accessToken'); + expect(res.body).toHaveProperty('refreshToken'); expect(res.body.tokenType).toBe('Bearer'); + }); - // Verify the new access token - const decoded = JSON.parse(Buffer.from(res.body.accessToken.split('.')[1], 'base64').toString()); + it('new access token should carry the correct userId claim', async () => { + const userId = 'test-user-123'; + const tokenPair = refreshTokenService.createTokenPair(userId); + const tokenRecord = refreshTokenService.createRefreshTokenRecord(userId, tokenPair.refreshToken); + await mockRepository.createRefreshToken(tokenRecord); + + const res = await request(app) + .post('/auth/refresh') + .send({ refreshToken: tokenPair.refreshToken }); + + const decoded = JSON.parse( + Buffer.from(res.body.accessToken.split('.')[1], 'base64').toString() + ); expect(decoded.userId).toBe(userId); expect(decoded.type).toBe('access'); }); - it('should reject missing refresh token', async () => { + it('consumed refresh token should be revoked after rotation', async () => { + const userId = 'test-user-123'; + const tokenPair = refreshTokenService.createTokenPair(userId); + const tokenRecord = refreshTokenService.createRefreshTokenRecord(userId, tokenPair.refreshToken); + const stored = await mockRepository.createRefreshToken(tokenRecord); + + await request(app) + .post('/auth/refresh') + .send({ refreshToken: tokenPair.refreshToken }); + + const afterRotation = await mockRepository.findRefreshTokenById(stored.id, userId); + expect(afterRotation?.isRevoked).toBe(true); + }); + + it('new refresh token should be in the same family as the consumed token', async () => { + const userId = 'test-user-123'; + const tokenPair = refreshTokenService.createTokenPair(userId); + const tokenRecord = refreshTokenService.createRefreshTokenRecord(userId, tokenPair.refreshToken); + const stored = await mockRepository.createRefreshToken(tokenRecord); + + const res = await request(app) + .post('/auth/refresh') + .send({ refreshToken: tokenPair.refreshToken }); + + // Find the newly created token in the repository + const allTokens = mockRepository.getAllForUser(userId); + const newToken = allTokens.find(t => !t.isRevoked); + expect(newToken).toBeDefined(); + expect(newToken.familyId).toBe(stored.familyId); + }); + + it('old refresh token should not work after rotation (single-use enforcement)', async () => { + const userId = 'test-user-123'; + const tokenPair = refreshTokenService.createTokenPair(userId); + const tokenRecord = refreshTokenService.createRefreshTokenRecord(userId, tokenPair.refreshToken); + await mockRepository.createRefreshToken(tokenRecord); + + // First use — valid + await request(app).post('/auth/refresh').send({ refreshToken: tokenPair.refreshToken }); + + // Second use of the same token — must be rejected const res = await request(app) .post('/auth/refresh') - .send({}); + .send({ refreshToken: tokenPair.refreshToken }); + expect(res.status).toBe(401); + expect(res.body.code).toBe('REVOKED_TOKEN'); + }); + + it('should reject missing refresh token', async () => { + const res = await request(app).post('/auth/refresh').send({}); expect(res.status).toBe(400); expect(res.body.details).toBeDefined(); }); @@ -162,7 +236,6 @@ describe('Refresh Token Integration Tests', () => { const res = await request(app) .post('/auth/refresh') .send({ refreshToken: 'invalid-token' }); - expect(res.status).toBe(401); expect(res.body.code).toBe('INVALID_REFRESH_TOKEN'); }); @@ -176,12 +249,9 @@ describe('Refresh Token Integration Tests', () => { }); const tokenPair = expiredService.createTokenPair(userId); - - // Store the refresh token const tokenRecord = expiredService.createRefreshTokenRecord(userId, tokenPair.refreshToken); await mockRepository.createRefreshToken(tokenRecord); - // Wait for expiration await new Promise(resolve => setTimeout(resolve, 10)); const res = await request(app) @@ -192,79 +262,114 @@ describe('Refresh Token Integration Tests', () => { expect(res.body.code).toBe('INVALID_REFRESH_TOKEN'); }); - it('should reject revoked refresh token', async () => { + it('should reject token with wrong hash', async () => { const userId = 'test-user-123'; const tokenPair = refreshTokenService.createTokenPair(userId); + const differentTokenPair = refreshTokenService.createTokenPair(userId); - // Store and revoke the refresh token - const tokenRecord = refreshTokenService.createRefreshTokenRecord(userId, tokenPair.refreshToken); - const storedToken = await mockRepository.createRefreshToken(tokenRecord); - await mockRepository.revokeRefreshToken(storedToken.id, userId); + const tokenRecord = refreshTokenService.createRefreshTokenRecord( + userId, + differentTokenPair.refreshToken + ); + tokenRecord.id = (refreshTokenService as any).extractTokenId(tokenPair.refreshToken); + await mockRepository.createRefreshToken(tokenRecord); const res = await request(app) .post('/auth/refresh') .send({ refreshToken: tokenPair.refreshToken }); expect(res.status).toBe(401); - expect(res.body.code).toBe('REVOKED_TOKEN'); + expect(res.body.code).toBe('INVALID_REFRESH_TOKEN'); }); + }); - it('should reject token with wrong hash', async () => { + // ── Theft detection ──────────────────────────────────────────────────────── + + describe('Theft detection: reuse of a rotated token', () => { + it('should revoke ALL user tokens when a revoked token is presented', async () => { const userId = 'test-user-123'; - const tokenPair = refreshTokenService.createTokenPair(userId); - const differentTokenPair = refreshTokenService.createTokenPair(userId); - // Store a token with different hash - const tokenRecord = refreshTokenService.createRefreshTokenRecord(userId, differentTokenPair.refreshToken); - tokenRecord.id = (refreshTokenService as any).extractTokenId(tokenPair.refreshToken); - await mockRepository.createRefreshToken(tokenRecord); + // Legitimate user gets token pair 1 + const pair1 = refreshTokenService.createTokenPair(userId); + const record1 = refreshTokenService.createRefreshTokenRecord(userId, pair1.refreshToken); + await mockRepository.createRefreshToken(record1); - const res = await request(app) + // Legitimate user rotates → pair1 consumed, pair2 issued in same family + const rotateRes = await request(app) .post('/auth/refresh') - .send({ refreshToken: tokenPair.refreshToken }); + .send({ refreshToken: pair1.refreshToken }); + expect(rotateRes.status).toBe(200); - expect(res.status).toBe(401); - expect(res.body.code).toBe('INVALID_REFRESH_TOKEN'); + // Create a token in a completely different family to prove cross-family revocation + const pair3 = refreshTokenService.createTokenPair(userId); + const record3 = refreshTokenService.createRefreshTokenRecord(userId, pair3.refreshToken); + // different familyId (default uuid) — unrelated family + await mockRepository.createRefreshToken(record3); + + // Attacker (or victim) replays the old revoked pair1 token + const theftRes = await request(app) + .post('/auth/refresh') + .send({ refreshToken: pair1.refreshToken }); + + expect(theftRes.status).toBe(401); + expect(theftRes.body.code).toBe('REVOKED_TOKEN'); + + // ALL tokens for the user must now be revoked — including pair3 + const activeCount = await mockRepository.countActiveTokens(userId); + expect(activeCount).toBe(0); }); - it('should revoke entire family atomically on reuse detection', async () => { - const userId = 'test-user-123'; - - // Create first token pair - const tokenPair1 = refreshTokenService.createTokenPair(userId); - const tokenRecord1 = refreshTokenService.createRefreshTokenRecord(userId, tokenPair1.refreshToken); - const storedToken1 = await mockRepository.createRefreshToken(tokenRecord1); + it('should revoke tokens across different families on reuse detection', async () => { + const userId = 'user-multi-family'; + + // Family A + const pairA = refreshTokenService.createTokenPair(userId); + const recordA = refreshTokenService.createRefreshTokenRecord(userId, pairA.refreshToken); + await mockRepository.createRefreshToken(recordA); + + // Family B — independent + const pairB = refreshTokenService.createTokenPair(userId); + const recordB = refreshTokenService.createRefreshTokenRecord(userId, pairB.refreshToken); + await mockRepository.createRefreshToken(recordB); - // Create second token pair in the same family - const tokenPair2 = refreshTokenService.createTokenPair(userId); - const tokenRecord2 = refreshTokenService.createRefreshTokenRecord(userId, tokenPair2.refreshToken, storedToken1.familyId); - const storedToken2 = await mockRepository.createRefreshToken(tokenRecord2); + // Rotate family A + await request(app).post('/auth/refresh').send({ refreshToken: pairA.refreshToken }); - // Mark first token as revoked (simulating it was already rotated) - await mockRepository.revokeRefreshToken(storedToken1.id, userId); + // Reuse old family A token → theft signal + await request(app).post('/auth/refresh').send({ refreshToken: pairA.refreshToken }); - // Now attempt to refresh using the revoked token (reuse) + // Family B token must also be revoked + const dbTokenB = await mockRepository.findRefreshTokenById(recordB.id, userId); + expect(dbTokenB?.isRevoked).toBe(true); + }); + + it('should return 401 REVOKED_TOKEN on reuse even if attacker already rotated', async () => { + const userId = 'test-user-theft'; + + // Victim's original token + const victimPair = refreshTokenService.createTokenPair(userId); + const victimRecord = refreshTokenService.createRefreshTokenRecord(userId, victimPair.refreshToken); + await mockRepository.createRefreshToken(victimRecord); + + // Attacker rotates the stolen token first + await request(app).post('/auth/refresh').send({ refreshToken: victimPair.refreshToken }); + + // Victim tries to use their original token const res = await request(app) .post('/auth/refresh') - .send({ refreshToken: tokenPair1.refreshToken }); + .send({ refreshToken: victimPair.refreshToken }); expect(res.status).toBe(401); expect(res.body.code).toBe('REVOKED_TOKEN'); - - // Verify all tokens in the family are now revoked - const dbToken1 = await mockRepository.findRefreshTokenById(storedToken1.id, userId); - const dbToken2 = await mockRepository.findRefreshTokenById(storedToken2.id, userId); - expect(dbToken1?.isRevoked).toBe(true); - expect(dbToken2?.isRevoked).toBe(true); }); }); + // ── POST /auth/revoke ────────────────────────────────────────────────────── + describe('POST /auth/revoke', () => { it('should revoke a valid refresh token', async () => { const userId = 'test-user-123'; const tokenPair = refreshTokenService.createTokenPair(userId); - - // Store the refresh token const tokenRecord = refreshTokenService.createRefreshTokenRecord(userId, tokenPair.refreshToken); await mockRepository.createRefreshToken(tokenRecord); @@ -275,7 +380,6 @@ describe('Refresh Token Integration Tests', () => { expect(res.status).toBe(200); expect(res.body.message).toBe('Token revoked successfully'); - // Verify token is revoked const storedToken = await mockRepository.findRefreshTokenByHash( (refreshTokenService as any).hashToken(tokenPair.refreshToken), userId @@ -284,9 +388,7 @@ describe('Refresh Token Integration Tests', () => { }); it('should handle non-existent token gracefully', async () => { - const userId = 'test-user-123'; - const tokenPair = refreshTokenService.createTokenPair(userId); - + const tokenPair = refreshTokenService.createTokenPair('test-user-123'); const res = await request(app) .post('/auth/revoke') .send({ refreshToken: tokenPair.refreshToken }); @@ -296,44 +398,37 @@ describe('Refresh Token Integration Tests', () => { }); it('should reject missing refresh token', async () => { - const res = await request(app) - .post('/auth/revoke') - .send({}); - + const res = await request(app).post('/auth/revoke').send({}); expect(res.status).toBe(400); expect(res.body.details).toBeDefined(); }); }); + // ── POST /auth/revoke-all ────────────────────────────────────────────────── + describe('POST /auth/revoke-all', () => { it('should revoke all tokens for authenticated user', async () => { const userId = 'test-user-123'; - - // Create multiple tokens const tokenPairs = [ refreshTokenService.createTokenPair(userId), refreshTokenService.createTokenPair(userId), refreshTokenService.createTokenPair(userId) ]; - // Store all tokens - for (const tokenPair of tokenPairs) { - const tokenRecord = refreshTokenService.createRefreshTokenRecord(userId, tokenPair.refreshToken); - await mockRepository.createRefreshToken(tokenRecord); + for (const pair of tokenPairs) { + const record = refreshTokenService.createRefreshTokenRecord(userId, pair.refreshToken); + await mockRepository.createRefreshToken(record); } - // Mock authentication const mockAuth = (req: any, res: any, next: any) => { req.developerId = userId; res.locals.authenticatedUser = { id: userId }; next(); }; - // Add mock auth middleware const testApp = express(); testApp.use(express.json()); testApp.use(mockAuth); - const authController = new AuthController({ refreshTokenService, refreshTokenRepository: mockRepository as any @@ -341,36 +436,30 @@ describe('Refresh Token Integration Tests', () => { testApp.use('/auth', createAuthRoutes(authController)); testApp.use(errorHandler); - const res = await request(testApp) - .post('/auth/revoke-all') - .set('x-user-id', userId) - .send(); - + const res = await request(testApp).post('/auth/revoke-all').send(); expect(res.status).toBe(200); expect(res.body.message).toBe('All tokens revoked successfully'); - // Verify all tokens are revoked const activeCount = await mockRepository.countActiveTokens(userId); expect(activeCount).toBe(0); }); }); + // ── GET /auth/tokens ─────────────────────────────────────────────────────── + describe('GET /auth/tokens', () => { it('should return token information for authenticated user', async () => { const userId = 'test-user-123'; - - // Create and store tokens const tokenPairs = [ refreshTokenService.createTokenPair(userId), refreshTokenService.createTokenPair(userId) ]; - for (const tokenPair of tokenPairs) { - const tokenRecord = refreshTokenService.createRefreshTokenRecord(userId, tokenPair.refreshToken); - await mockRepository.createRefreshToken(tokenRecord); + for (const pair of tokenPairs) { + const record = refreshTokenService.createRefreshTokenRecord(userId, pair.refreshToken); + await mockRepository.createRefreshToken(record); } - // Mock authentication const mockAuth = (req: any, res: any, next: any) => { req.developerId = userId; res.locals.authenticatedUser = { id: userId }; @@ -380,7 +469,6 @@ describe('Refresh Token Integration Tests', () => { const testApp = express(); testApp.use(express.json()); testApp.use(mockAuth); - const authController = new AuthController({ refreshTokenService, refreshTokenRepository: mockRepository as any @@ -388,11 +476,7 @@ describe('Refresh Token Integration Tests', () => { testApp.use('/auth', createAuthRoutes(authController)); testApp.use(errorHandler); - const res = await request(testApp) - .get('/auth/tokens') - .set('x-user-id', userId) - .send(); - + const res = await request(testApp).get('/auth/tokens').send(); expect(res.status).toBe(200); expect(res.body.activeRefreshTokens).toBe(2); expect(res.body.maxAllowedTokens).toBe(5);