mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-07-13 19:04:57 +02:00
Add rate limiter for auth endpoints
This commit is contained in:
parent
9c8900560c
commit
ac381854e5
5 changed files with 90 additions and 6 deletions
16
package-lock.json
generated
16
package-lock.json
generated
|
@ -12,6 +12,7 @@
|
|||
"axios": "^0.27.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"express": "^4.17.1",
|
||||
"express-rate-limit": "^7.5.1",
|
||||
"express-session": "^1.17.3",
|
||||
"graceful-fs": "^4.2.10",
|
||||
"htmlparser2": "^8.0.1",
|
||||
|
@ -1893,6 +1894,21 @@
|
|||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
|
||||
"integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/express-rate-limit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session": {
|
||||
"version": "1.17.3",
|
||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz",
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
"axios": "^0.27.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"express": "^4.17.1",
|
||||
"express-rate-limit": "^7.5.1",
|
||||
"express-session": "^1.17.3",
|
||||
"graceful-fs": "^4.2.10",
|
||||
"htmlparser2": "^8.0.1",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const { Request, Response, NextFunction } = require('express')
|
||||
const { rateLimit } = require('express-rate-limit')
|
||||
const passport = require('passport')
|
||||
const JwtStrategy = require('passport-jwt').Strategy
|
||||
const ExtractJwt = require('passport-jwt').ExtractJwt
|
||||
|
@ -9,6 +10,7 @@ const TokenManager = require('./auth/TokenManager')
|
|||
const LocalAuthStrategy = require('./auth/LocalAuthStrategy')
|
||||
const OidcAuthStrategy = require('./auth/OidcAuthStrategy')
|
||||
|
||||
const RateLimiterFactory = require('./utils/rateLimiterFactory')
|
||||
const { escapeRegExp } = require('./utils')
|
||||
|
||||
/**
|
||||
|
@ -19,6 +21,9 @@ class Auth {
|
|||
const escapedRouterBasePath = escapeRegExp(global.RouterBasePath)
|
||||
this.ignorePatterns = [new RegExp(`^(${escapedRouterBasePath}/api)?/items/[^/]+/cover$`), new RegExp(`^(${escapedRouterBasePath}/api)?/authors/[^/]+/image$`)]
|
||||
|
||||
/** @type {import('express-rate-limit').RateLimitRequestHandler} */
|
||||
this.authRateLimiter = RateLimiterFactory.getAuthRateLimiter()
|
||||
|
||||
this.tokenManager = new TokenManager()
|
||||
this.localAuthStrategy = new LocalAuthStrategy()
|
||||
this.oidcAuthStrategy = new OidcAuthStrategy()
|
||||
|
@ -305,7 +310,7 @@ class Auth {
|
|||
*/
|
||||
async initAuthRoutes(router) {
|
||||
// Local strategy login route (takes username and password)
|
||||
router.post('/login', passport.authenticate('local'), async (req, res) => {
|
||||
router.post('/login', this.authRateLimiter, passport.authenticate('local'), async (req, res) => {
|
||||
// Check if mobile app wants refresh token in response
|
||||
const returnTokens = req.query.return_tokens === 'true' || req.headers['x-return-tokens'] === 'true'
|
||||
|
||||
|
@ -314,7 +319,7 @@ class Auth {
|
|||
})
|
||||
|
||||
// Refresh token route
|
||||
router.post('/auth/refresh', async (req, res) => {
|
||||
router.post('/auth/refresh', this.authRateLimiter, async (req, res) => {
|
||||
let refreshToken = req.cookies.refresh_token
|
||||
|
||||
// If x-refresh-token header is present, use it instead of the cookie
|
||||
|
@ -345,7 +350,7 @@ class Auth {
|
|||
})
|
||||
|
||||
// openid strategy login route (this redirects to the configured openid login provider)
|
||||
router.get('/auth/openid', (req, res) => {
|
||||
router.get('/auth/openid', this.authRateLimiter, (req, res) => {
|
||||
const authorizationUrlResponse = this.oidcAuthStrategy.getAuthorizationUrl(req)
|
||||
|
||||
if (authorizationUrlResponse.error) {
|
||||
|
@ -359,11 +364,12 @@ class Auth {
|
|||
|
||||
// This will be the oauth2 callback route for mobile clients
|
||||
// It will redirect to an app-link like audiobookshelf://oauth
|
||||
router.get('/auth/openid/mobile-redirect', (req, res) => this.oidcAuthStrategy.handleMobileRedirect(req, res))
|
||||
router.get('/auth/openid/mobile-redirect', this.authRateLimiter, (req, res) => this.oidcAuthStrategy.handleMobileRedirect(req, res))
|
||||
|
||||
// openid strategy callback route (this receives the token from the configured openid login provider)
|
||||
router.get(
|
||||
'/auth/openid/callback',
|
||||
this.authRateLimiter,
|
||||
(req, res, next) => {
|
||||
const sessionKey = this.oidcAuthStrategy.getStrategy()._key
|
||||
|
||||
|
@ -436,7 +442,7 @@ class Auth {
|
|||
*
|
||||
* @example /auth/openid/config?issuer=http://192.168.1.66:9000/application/o/audiobookshelf/
|
||||
*/
|
||||
router.get('/auth/openid/config', this.isAuthenticated, async (req, res) => {
|
||||
router.get('/auth/openid/config', this.authRateLimiter, this.isAuthenticated, async (req, res) => {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[Auth] Non-admin user "${req.user.username}" attempted to get issuer config`)
|
||||
return res.sendStatus(403)
|
||||
|
|
|
@ -182,7 +182,7 @@ class ApiRouter {
|
|||
this.router.post('/me/item/:id/bookmark', MeController.createBookmark.bind(this))
|
||||
this.router.patch('/me/item/:id/bookmark', MeController.updateBookmark.bind(this))
|
||||
this.router.delete('/me/item/:id/bookmark/:time', MeController.removeBookmark.bind(this))
|
||||
this.router.patch('/me/password', MeController.updatePassword.bind(this))
|
||||
this.router.patch('/me/password', this.auth.authRateLimiter, MeController.updatePassword.bind(this))
|
||||
this.router.get('/me/items-in-progress', MeController.getAllLibraryItemsInProgress.bind(this))
|
||||
this.router.get('/me/series/:id/remove-from-continue-listening', MeController.removeSeriesFromContinueListening.bind(this))
|
||||
this.router.get('/me/series/:id/readd-to-continue-listening', MeController.readdSeriesFromContinueListening.bind(this))
|
||||
|
|
61
server/utils/rateLimiterFactory.js
Normal file
61
server/utils/rateLimiterFactory.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
const { rateLimit, RateLimitRequestHandler } = require('express-rate-limit')
|
||||
const Logger = require('../Logger')
|
||||
|
||||
/**
|
||||
* Factory for creating authentication rate limiters
|
||||
*/
|
||||
class RateLimiterFactory {
|
||||
constructor() {
|
||||
this.authRateLimiter = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authentication rate limiter
|
||||
* @returns {RateLimitRequestHandler}
|
||||
*/
|
||||
getAuthRateLimiter() {
|
||||
if (this.authRateLimiter) {
|
||||
return this.authRateLimiter
|
||||
}
|
||||
|
||||
let windowMs = 10 * 60 * 1000 // 10 minutes default
|
||||
if (parseInt(process.env.RATE_LIMIT_AUTH_WINDOW) > 0) {
|
||||
windowMs = parseInt(process.env.RATE_LIMIT_AUTH_WINDOW)
|
||||
}
|
||||
|
||||
let max = 20 // 20 attempts default
|
||||
if (parseInt(process.env.RATE_LIMIT_AUTH_MAX) > 0) {
|
||||
max = parseInt(process.env.RATE_LIMIT_AUTH_MAX)
|
||||
}
|
||||
|
||||
let message = 'Too many requests, please try again later.'
|
||||
if (process.env.RATE_LIMIT_AUTH_MESSAGE) {
|
||||
message = process.env.RATE_LIMIT_AUTH_MESSAGE
|
||||
}
|
||||
|
||||
this.authRateLimiter = rateLimit({
|
||||
windowMs,
|
||||
max,
|
||||
message,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
handler: (req, res) => {
|
||||
const userAgent = req.get('User-Agent') || 'Unknown'
|
||||
const endpoint = req.path
|
||||
const method = req.method
|
||||
|
||||
Logger.warn(`[RateLimiter] Rate limit exceeded - IP: ${req.ip}, Endpoint: ${method} ${endpoint}, User-Agent: ${userAgent}`)
|
||||
|
||||
res.status(429).json({
|
||||
error: 'Too many authentication attempts, please try again later.'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
Logger.debug(`[RateLimiterFactory] Created auth rate limiter: ${max} attempts per ${windowMs / 1000 / 60} minutes`)
|
||||
|
||||
return this.authRateLimiter
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new RateLimiterFactory()
|
Loading…
Add table
Add a link
Reference in a new issue