diff --git a/server/auth/index.js b/server/auth/index.js index c49c081..900dc52 100644 --- a/server/auth/index.js +++ b/server/auth/index.js @@ -1,10 +1,12 @@ const express = require('express'); +const rateLimit = require('express-rate-limit'); const CustomStrategy = require('passport-custom').Strategy; const { URLSearchParams } = require('url'); const { User } = require('../models'); const metrics = require('../metrics'); const { buildWcaUserUpdate } = require('./wcaProfile'); const { upsertTestUser } = require('./testUser'); +const { apiRateLimitOptions } = require('../middlewares/apiRateLimit'); const checkStatus = async (res) => { if (res.ok) { // res.status >= 200 && res.status < 300 @@ -21,12 +23,14 @@ const deserializeUser = async (userModel, id, done) => { } }; -module.exports = (app, passport) => { +module.exports = (app, passport, rateLimitOptions = apiRateLimitOptions()) => { const router = express.Router(); const config = app.get('config'); const tokenURL = `${config.wcaSource}/oauth/token`; const userProfileURL = `${config.wcaSource}/api/v0/me`; + router.use(rateLimit(rateLimitOptions)); + passport.use('custom', new CustomStrategy(async (req, done) => { const { code, redirectUri } = req.body; diff --git a/server/auth/index.test.js b/server/auth/index.test.js index da5774a..61b8873 100644 --- a/server/auth/index.test.js +++ b/server/auth/index.test.js @@ -2,6 +2,29 @@ /* eslint-env jest */ const { deserializeUser } = require('./index'); +const http = require('http'); +const express = require('express'); +const createAuthRouter = require('./index'); +const { apiRateLimitOptions } = require('../middlewares/apiRateLimit'); + +const request = (server) => new Promise((resolve, reject) => { + const req = http.request({ + host: '127.0.0.1', + method: 'POST', + path: '/auth/logout', + port: server.address().port, + }, (res) => { + let body = ''; + res.on('data', (chunk) => { body += chunk; }); + res.resume(); + res.on('end', () => resolve({ + body: body ? JSON.parse(body) : null, + status: res.statusCode, + })); + }); + req.on('error', reject); + req.end(); +}); describe('session user deserialization', () => { it('uses the Mongoose promise API and returns the loaded user', async () => { @@ -25,3 +48,45 @@ describe('session user deserialization', () => { expect(done).toHaveBeenCalledWith(error); }); }); + +describe('authentication rate limiting', () => { + let server; + + beforeEach((done) => { + const app = express(); + const passport = { + deserializeUser: jest.fn(), + serializeUser: jest.fn(), + use: jest.fn(), + }; + app.set('config', { + auth: {}, + wcaSource: 'https://wca.example', + }); + app.use((req, res, next) => { + req.logout = (callback) => callback(); + next(); + }); + app.use('/auth', createAuthRouter( + app, + passport, + apiRateLimitOptions({ limit: 1, windowMs: 60_000 }), + )); + server = app.listen(0, '127.0.0.1', () => done()); + }); + + afterEach((done) => { + server.close(() => done()); + }); + + it('rejects requests beyond the configured IP window', async () => { + expect(await request(server)).toEqual({ body: null, status: 204 }); + expect(await request(server)).toEqual({ + body: { + code: 'rate_limited', + message: 'Too many requests. Please try again later.', + }, + status: 429, + }); + }); +});