Merge pull request #2553 from Sapd/sso

OpenID: Implement Logout + Fix state + Fix URL Regex
This commit is contained in:
advplyr 2024-02-17 17:03:12 -06:00 committed by GitHub
commit 7bf7b6bcf9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 179 additions and 430 deletions

View File

@ -82,19 +82,33 @@ export default {
this.$setLanguageCode(lang)
},
logout() {
var rootSocket = this.$root.socket || {}
const logoutPayload = {
socketId: rootSocket.id
// Disconnect from socket
if (this.$root.socket) {
console.log('Disconnecting from socket', this.$root.socket.id)
this.$root.socket.removeAllListeners()
this.$root.socket.disconnect()
}
this.$axios.$post('/logout', logoutPayload).catch((error) => {
console.error(error)
})
if (localStorage.getItem('token')) {
localStorage.removeItem('token')
}
this.$store.commit('libraries/setUserPlaylists', [])
this.$store.commit('libraries/setCollections', [])
this.$axios
.$post('/logout')
.then((logoutPayload) => {
const redirect_url = logoutPayload.redirect_url
if (redirect_url) {
window.location.href = redirect_url
} else {
this.$router.push('/login')
}
})
.catch((error) => {
console.error(error)
})
},
resetForm() {
this.password = null

View File

@ -206,7 +206,7 @@ export default {
function isValidRedirectURI(uri) {
// Check for somestring://someother/string
const pattern = new RegExp('^\\w+://[\\w\\.-]+$', 'i')
const pattern = new RegExp('^\\w+://[\\w\\.-]+(/[\\w\\./-]*)*$', 'i')
return pattern.test(uri)
}

View File

@ -81,7 +81,8 @@ class Auth {
authorization_endpoint: global.ServerSettings.authOpenIDAuthorizationURL,
token_endpoint: global.ServerSettings.authOpenIDTokenURL,
userinfo_endpoint: global.ServerSettings.authOpenIDUserInfoURL,
jwks_uri: global.ServerSettings.authOpenIDJwksURL
jwks_uri: global.ServerSettings.authOpenIDJwksURL,
end_session_endpoint: global.ServerSettings.authOpenIDLogoutURL
}).Client
const openIdClient = new openIdIssuerClient({
client_id: global.ServerSettings.authOpenIDClientID,
@ -153,6 +154,9 @@ class Auth {
return
}
// We also have to save the id_token for later (used for logout) because we cannot set cookies here
user.openid_id_token = tokenset.id_token
// permit login
return done(null, user)
}))
@ -183,49 +187,48 @@ class Auth {
}
/**
* Stores the client's choice how the login callback should happen in temp cookies
* Returns if the given auth method is API based.
*
* @param {string} authMethod
* @returns {boolean}
*/
isAuthMethodAPIBased(authMethod) {
return ['api', 'openid-mobile'].includes(authMethod)
}
/**
* Stores the client's choice of login callback method in temporary cookies.
*
* The `authMethod` parameter specifies the authentication strategy and can have the following values:
* - 'local': Standard authentication,
* - 'api': Authentication for API use
* - 'openid': OpenID authentication directly over web
* - 'openid-mobile': OpenID authentication, but done via an mobile device
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {string} authMethod - The authentication method, default is 'local'.
*/
paramsToCookies(req, res) {
// Set if isRest flag is set or if mobile oauth flow is used
if (req.query.isRest?.toLowerCase() == 'true' || req.query.redirect_uri) {
// store the isRest flag to the is_rest cookie
res.cookie('is_rest', 'true', {
maxAge: 120000, // 2 min
httpOnly: true
})
} else {
// no isRest-flag set -> set is_rest cookie to false
res.cookie('is_rest', 'false', {
maxAge: 120000, // 2 min
httpOnly: true
})
// persist state if passed in
if (req.query.state) {
res.cookie('auth_state', req.query.state, {
maxAge: 120000, // 2 min
httpOnly: true
})
}
paramsToCookies(req, res, authMethod = 'local') {
const TWO_MINUTES = 120000 // 2 minutes in milliseconds
const callback = req.query.redirect_uri || req.query.callback
// check if we are missing a callback parameter - we need one if isRest=false
// Additional handling for non-API based authMethod
if (!this.isAuthMethodAPIBased(authMethod)) {
// Store 'auth_state' if present in the request
if (req.query.state) {
res.cookie('auth_state', req.query.state, { maxAge: TWO_MINUTES, httpOnly: true })
}
// Validate and store the callback URL
if (!callback) {
res.status(400).send({
message: 'No callback parameter'
})
return
return res.status(400).send({ message: 'No callback parameter' })
}
// store the callback url to the auth_cb cookie
res.cookie('auth_cb', callback, {
maxAge: 120000, // 2 min
httpOnly: true
})
res.cookie('auth_cb', callback, { maxAge: TWO_MINUTES, httpOnly: true })
}
// Store the authentication method for long
res.cookie('auth_method', authMethod, { maxAge: 1000 * 60 * 60 * 24 * 365 * 10, httpOnly: true })
}
/**
@ -239,7 +242,7 @@ class Auth {
// get userLogin json (information about the user, server and the session)
const data_json = await this.getUserLoginResponsePayload(req.user)
if (req.cookies.is_rest === 'true') {
if (this.isAuthMethodAPIBased(req.cookies.auth_method)) {
// REST request - send data
res.json(data_json)
} else {
@ -269,109 +272,105 @@ class Auth {
// openid strategy login route (this redirects to the configured openid login provider)
router.get('/auth/openid', (req, res, next) => {
try {
// helper function from openid-client
function pick(object, ...paths) {
const obj = {}
for (const path of paths) {
if (object[path] !== undefined) {
obj[path] = object[path]
}
}
return obj
}
// Get the OIDC client from the strategy
// We need to call the client manually, because the strategy does not support forwarding the code challenge
// for API or mobile clients
const oidcStrategy = passport._strategy('openid-client')
const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http'
let mobile_redirect_uri = null
// The client wishes a different redirect_uri
// We will allow if it is in the whitelist, by saving it into this.openIdAuthSession and setting the redirect uri to /auth/openid/mobile-redirect
// where we will handle the redirect to it
if (req.query.redirect_uri) {
// Check if the redirect_uri is in the whitelist
if (Database.serverSettings.authOpenIDMobileRedirectURIs.includes(req.query.redirect_uri) ||
(Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')) {
oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/mobile-redirect`).toString()
mobile_redirect_uri = req.query.redirect_uri
} else {
Logger.debug(`[Auth] Invalid redirect_uri=${req.query.redirect_uri} - not in whitelist`)
return res.status(400).send('Invalid redirect_uri')
}
} else {
oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/callback`).toString()
}
Logger.debug(`[Auth] Oidc redirect_uri=${oidcStrategy._params.redirect_uri}`)
const client = oidcStrategy._client
const sessionKey = oidcStrategy._key
let code_challenge
let code_challenge_method
try {
const protocol = req.secure || req.get('x-forwarded-proto') === 'https' ? 'https' : 'http'
const hostUrl = new URL(`${protocol}://${req.get('host')}`)
const isMobileFlow = req.query.response_type === 'code' || req.query.redirect_uri || req.query.code_challenge
// If code_challenge is provided, expect that code_verifier will be handled by the client (mobile app)
// The web frontend of ABS does not need to do a PKCE itself, because it never handles the "code" of the oauth flow
// and as such will not send a code challenge, we will generate then one
if (req.query.code_challenge) {
code_challenge = req.query.code_challenge
code_challenge_method = req.query.code_challenge_method || 'S256'
if (!['S256', 'plain'].includes(code_challenge_method)) {
return res.status(400).send('Invalid code_challenge_method')
// Only allow code flow (for mobile clients)
if (req.query.response_type && req.query.response_type !== 'code') {
Logger.debug(`[Auth] OIDC Invalid response_type=${req.query.response_type}`)
return res.status(400).send('Invalid response_type, only code supported')
}
// Generate a state on web flow or if no state supplied
const state = (!isMobileFlow || !req.query.state) ? OpenIDClient.generators.random() : req.query.state
// Redirect URL for the SSO provider
let redirectUri
if (isMobileFlow) {
// Mobile required redirect uri
// If it is in the whitelist, we will save into this.openIdAuthSession and set the redirect uri to /auth/openid/mobile-redirect
// where we will handle the redirect to it
if (!req.query.redirect_uri || !isValidRedirectUri(req.query.redirect_uri)) {
Logger.debug(`[Auth] Invalid redirect_uri=${req.query.redirect_uri}`)
return res.status(400).send('Invalid redirect_uri')
}
// We cannot save the supplied redirect_uri in the session, because it the mobile client uses browser instead of the API
// for the request to mobile-redirect and as such the session is not shared
this.openIdAuthSession.set(state, { mobile_redirect_uri: req.query.redirect_uri })
redirectUri = new URL('/auth/openid/mobile-redirect', hostUrl).toString()
} else {
// If no code_challenge is provided, assume a web application flow and generate one
const code_verifier = OpenIDClient.generators.codeVerifier()
code_challenge = OpenIDClient.generators.codeChallenge(code_verifier)
code_challenge_method = 'S256'
redirectUri = new URL('/auth/openid/callback', hostUrl).toString()
// Store the code_verifier in the session for later use in the token exchange
req.session[sessionKey] = { ...req.session[sessionKey], code_verifier }
if (req.query.state) {
Logger.debug(`[Auth] Invalid state - not allowed on web openid flow`)
return res.status(400).send('Invalid state, not allowed on web flow')
}
}
oidcStrategy._params.redirect_uri = redirectUri
Logger.debug(`[Auth] OIDC redirect_uri=${redirectUri}`)
const params = {
state: OpenIDClient.generators.random(),
// Other params by the passport strategy
...oidcStrategy._params
}
if (!params.nonce && params.response_type.includes('id_token')) {
params.nonce = OpenIDClient.generators.random()
}
let { code_challenge, code_challenge_method, code_verifier } = generatePkce(req, isMobileFlow)
req.session[sessionKey] = {
...req.session[sessionKey],
...pick(params, 'nonce', 'state', 'max_age', 'response_type'),
state: state,
max_age: oidcStrategy._params.max_age,
response_type: 'code',
code_verifier: code_verifier, // not null if web flow
mobile: req.query.redirect_uri, // Used in the abs callback later, set mobile if redirect_uri is filled out
sso_redirect_uri: oidcStrategy._params.redirect_uri // Save the redirect_uri (for the SSO Provider) for the callback
}
// We cannot save redirect_uri in the session, because it the mobile client uses browser instead of the API
// for the request to mobile-redirect and as such the session is not shared
this.openIdAuthSession.set(params.state, { mobile_redirect_uri: mobile_redirect_uri })
// Now get the URL to direct to
const authorizationUrl = client.authorizationUrl({
...params,
scope: 'openid profile email',
...oidcStrategy._params,
state: state,
response_type: 'code',
code_challenge,
code_challenge_method
})
// params (isRest, callback) to a cookie that will be send to the client
this.paramsToCookies(req, res)
this.paramsToCookies(req, res, isMobileFlow ? 'openid-mobile' : 'openid')
// Redirect the user agent (browser) to the authorization URL
res.redirect(authorizationUrl)
} catch (error) {
Logger.error(`[Auth] Error in /auth/openid route: ${error}`)
res.status(500).send('Internal Server Error')
}
function generatePkce(req, isMobileFlow) {
if (isMobileFlow) {
if (!req.query.code_challenge) {
throw new Error('code_challenge required for mobile flow (PKCE)')
}
if (req.query.code_challenge_method && req.query.code_challenge_method !== 'S256') {
throw new Error('Only S256 code_challenge_method method supported')
}
return {
code_challenge: req.query.code_challenge,
code_challenge_method: req.query.code_challenge_method || 'S256'
}
} else {
const code_verifier = OpenIDClient.generators.codeVerifier()
const code_challenge = OpenIDClient.generators.codeChallenge(code_verifier)
return { code_challenge, code_challenge_method: 'S256', code_verifier }
}
}
function isValidRedirectUri(uri) {
// Check if the redirect_uri is in the whitelist
return Database.serverSettings.authOpenIDMobileRedirectURIs.includes(uri) ||
(Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')
}
})
// This will be the oauth2 callback route for mobile clients
@ -453,6 +452,12 @@ class Auth {
if (loginError) {
return handleAuthError(isMobile, 500, 'Error during login', `[Auth] Error in openid callback: ${loginError}`)
}
// The id_token does not provide access to the user, but is used to identify the user to the SSO provider
// instead it containts a JWT with userinfo like user email, username, etc.
// the client will get to know it anyway in the logout url according to the oauth2 spec
// so it is safe to send it to the client, but we use strict settings
res.cookie('openid_id_token', user.openid_id_token, { maxAge: 1000 * 60 * 60 * 24 * 365 * 10, httpOnly: true, secure: true, sameSite: 'Strict' })
next()
})
}
@ -520,9 +525,48 @@ class Auth {
req.logout((err) => {
if (err) {
res.sendStatus(500)
} else {
const authMethod = req.cookies.auth_method
res.clearCookie('auth_method')
if (authMethod === 'openid' || authMethod === 'openid-mobile') {
// If we are using openid, we need to redirect to the logout endpoint
// node-openid-client does not support doing it over passport
const oidcStrategy = passport._strategy('openid-client')
const client = oidcStrategy._client
let postLogoutRedirectUri = null
if (authMethod === 'openid') {
const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http'
const host = req.get('host')
// TODO: ABS does currently not support subfolders for installation
// If we want to support it we need to include a config for the serverurl
postLogoutRedirectUri = `${protocol}://${host}/login`
}
// else for openid-mobile we keep postLogoutRedirectUri on null
// nice would be to redirect to the app here, but for example Authentik does not implement
// the post_logout_redirect_uri parameter at all and for other providers
// we would also need again to implement (and even before get to know somehow for 3rd party apps)
// the correct app link like audiobookshelf://login (and maybe also provide a redirect like mobile-redirect).
// Instead because its null (and this way the parameter will be omitted completly), the client/app can simply append something like
// &post_logout_redirect_uri=audiobookshelf://login to the received logout url by itself which is the simplest solution
// (The URL needs to be whitelisted in the config of the SSO/ID provider)
const logoutUrl = client.endSessionUrl({
id_token_hint: req.cookies.openid_id_token,
post_logout_redirect_uri: postLogoutRedirectUri
})
res.clearCookie('openid_id_token')
// Tell the user agent (browser) to redirect to the authentification provider's logout URL
res.send({ redirect_url: logoutUrl })
} else {
res.sendStatus(200)
}
}
})
})
}
@ -612,7 +656,7 @@ class Auth {
* Checks if a username and password tuple is valid and the user active.
* @param {string} username
* @param {string} password
* @param {function} done
* @param {Promise<function>} done
*/
async localAuthCheckUserPw(username, password, done) {
// Load the user given it's username
@ -654,7 +698,7 @@ class Auth {
/**
* Hashes a password with bcrypt.
* @param {string} password
* @returns {string} hash
* @returns {Promise<string>} hash
*/
hashPass(password) {
return new Promise((resolve) => {
@ -688,8 +732,8 @@ class Auth {
/**
*
* @param {string} password
* @param {*} user
* @returns {boolean}
* @param {import('./models/User')} user
* @returns {Promise<boolean>}
*/
comparePassword(password, user) {
if (user.type === 'root' && !password && !user.pash) return true

View File

@ -5,7 +5,6 @@ const http = require('http')
const util = require('util')
const fs = require('./libs/fsExtra')
const fileUpload = require('./libs/expressFileupload')
const rateLimit = require('./libs/expressRateLimit')
const cookieParser = require("cookie-parser")
const { version } = require('../package.json')
@ -287,8 +286,6 @@ class Server {
]
dyanimicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
// router.post('/login', passport.authenticate('local', this.auth.login), this.auth.loginResult.bind(this))
// router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
router.post('/init', (req, res) => {
if (Database.hasRootUser) {
Logger.error(`[Server] attempt to init server when server already has a root user`)
@ -401,30 +398,6 @@ class Server {
}
}
// First time login rate limit is hit
loginLimitReached(req, res, options) {
Logger.error(`[Server] Login rate limit (${options.max}) was hit for ip ${req.ip}`)
options.message = 'Too many attempts. Login temporarily locked.'
}
getLoginRateLimiter() {
return rateLimit({
windowMs: Database.serverSettings.rateLimitLoginWindow, // 5 minutes
max: Database.serverSettings.rateLimitLoginRequests,
skipSuccessfulRequests: true,
onLimitReached: this.loginLimitReached
})
}
logout(req, res) {
if (req.body.socketId) {
Logger.info(`[Server] User ${req.user ? req.user.username : 'Unknown'} is logging out with socket ${req.body.socketId}`)
SocketAuthority.logout(req.body.socketId)
}
res.sendStatus(200)
}
/**
* Gracefully stop server
* Stops watcher and socket server

View File

@ -219,25 +219,6 @@ class SocketAuthority {
client.socket.emit('init', initialPayload)
}
logout(socketId) {
// Strip user and client from client and client socket
if (socketId && this.clients[socketId]) {
const client = this.clients[socketId]
const clientSocket = client.socket
Logger.debug(`[SocketAuthority] Found user client ${clientSocket.id}, Has user: ${!!client.user}, Socket has client: ${!!clientSocket.sheepClient}`)
if (client.user) {
Logger.debug('[SocketAuthority] User Offline ' + client.user.username)
this.adminEmitter('user_offline', client.user.toJSONForPublic())
}
delete this.clients[socketId].user
if (clientSocket && clientSocket.sheepClient) delete this.clients[socketId].socket.sheepClient
} else if (socketId) {
Logger.warn(`[SocketAuthority] No client for socket ${socketId}`)
}
}
cancelScan(id) {
Logger.debug('[SocketAuthority] Cancel scan', id)
this.Server.cancelLibraryScan(id)

View File

@ -633,7 +633,7 @@ class MiscController {
} else if (key === 'authOpenIDMobileRedirectURIs') {
function isValidRedirectURI(uri) {
if (typeof uri !== 'string') return false
const pattern = new RegExp('^\\w+://[\\w.-]+$', 'i')
const pattern = new RegExp('^\\w+://[\\w\\.-]+(/[\\w\\./-]*)*$', 'i')
return pattern.test(uri)
}

View File

@ -1,20 +0,0 @@
# MIT License
Copyright 2021 Nathan Friedly
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,196 +0,0 @@
"use strict";
//
// modified for use in audiobookshelf
// Source: https://github.com/nfriedly/express-rate-limit
//
const MemoryStore = require("./memory-store");
function RateLimit(options) {
options = Object.assign(
{
windowMs: 60 * 1000, // milliseconds - how long to keep records of requests in memory
max: 5, // max number of recent connections during `window` milliseconds before sending a 429 response
message: "Too many requests, please try again later.",
statusCode: 429, // 429 status = Too Many Requests (RFC 6585)
headers: true, //Send custom rate limit header with limit and remaining
draft_polli_ratelimit_headers: false, //Support for the new RateLimit standardization headers
// ability to manually decide if request was successful. Used when `skipSuccessfulRequests` and/or `skipFailedRequests` are set to `true`
requestWasSuccessful: function (req, res) {
return res.statusCode < 400;
},
skipFailedRequests: false, // Do not count failed requests
skipSuccessfulRequests: false, // Do not count successful requests
// allows to create custom keys (by default user IP is used)
keyGenerator: function (req /*, res*/) {
if (!req.ip) {
console.error(
"express-rate-limit: req.ip is undefined - you can avoid this by providing a custom keyGenerator function, but it may be indicative of a larger issue."
);
}
return req.ip;
},
skip: function (/*req, res*/) {
return false;
},
handler: function (req, res /*, next, optionsUsed*/) {
res.status(options.statusCode).send(options.message);
},
onLimitReached: function (/*req, res, optionsUsed*/) { },
requestPropertyName: "rateLimit", // Parameter name appended to req object
},
options
);
// store to use for persisting rate limit data
options.store = options.store || new MemoryStore(options.windowMs);
// ensure that the store has the incr method
if (
typeof options.store.incr !== "function" ||
typeof options.store.resetKey !== "function" ||
(options.skipFailedRequests &&
typeof options.store.decrement !== "function")
) {
throw new Error("The store is not valid.");
}
["global", "delayMs", "delayAfter"].forEach((key) => {
// note: this doesn't trigger if delayMs or delayAfter are set to 0, because that essentially disables them
if (options[key]) {
throw new Error(
`The ${key} option was removed from express-rate-limit v3.`
);
}
});
function rateLimit(req, res, next) {
Promise.resolve(options.skip(req, res))
.then((skip) => {
if (skip) {
return next();
}
const key = options.keyGenerator(req, res);
options.store.incr(key, function (err, current, resetTime) {
if (err) {
return next(err);
}
const maxResult =
typeof options.max === "function"
? options.max(req, res)
: options.max;
Promise.resolve(maxResult)
.then((max) => {
req[options.requestPropertyName] = {
limit: max,
current: current,
remaining: Math.max(max - current, 0),
resetTime: resetTime,
};
if (options.headers && !res.headersSent) {
res.setHeader("X-RateLimit-Limit", max);
res.setHeader(
"X-RateLimit-Remaining",
req[options.requestPropertyName].remaining
);
if (resetTime instanceof Date) {
// if we have a resetTime, also provide the current date to help avoid issues with incorrect clocks
res.setHeader("Date", new Date().toUTCString());
res.setHeader(
"X-RateLimit-Reset",
Math.ceil(resetTime.getTime() / 1000)
);
}
}
if (options.draft_polli_ratelimit_headers && !res.headersSent) {
res.setHeader("RateLimit-Limit", max);
res.setHeader(
"RateLimit-Remaining",
req[options.requestPropertyName].remaining
);
if (resetTime) {
const deltaSeconds = Math.ceil(
(resetTime.getTime() - Date.now()) / 1000
);
res.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds));
}
}
if (
options.skipFailedRequests ||
options.skipSuccessfulRequests
) {
let decremented = false;
const decrementKey = () => {
if (!decremented) {
options.store.decrement(key);
decremented = true;
}
};
if (options.skipFailedRequests) {
res.on("finish", function () {
if (!options.requestWasSuccessful(req, res)) {
decrementKey();
}
});
res.on("close", () => {
if (!res.finished) {
decrementKey();
}
});
res.on("error", () => decrementKey());
}
if (options.skipSuccessfulRequests) {
res.on("finish", function () {
if (options.requestWasSuccessful(req, res)) {
options.store.decrement(key);
}
});
}
}
if (max && current === max + 1) {
options.onLimitReached(req, res, options);
}
if (max && current > max) {
if (options.headers && !res.headersSent) {
res.setHeader(
"Retry-After",
Math.ceil(options.windowMs / 1000)
);
}
return options.handler(req, res, next, options);
}
next();
return null;
})
.catch(next);
});
return null;
})
.catch(next);
}
rateLimit.resetKey = options.store.resetKey.bind(options.store);
// Backward compatibility function
rateLimit.resetIp = rateLimit.resetKey;
return rateLimit;
}
module.exports = RateLimit;

View File

@ -1,47 +0,0 @@
"use strict";
function calculateNextResetTime(windowMs) {
const d = new Date();
d.setMilliseconds(d.getMilliseconds() + windowMs);
return d;
}
function MemoryStore(windowMs) {
let hits = {};
let resetTime = calculateNextResetTime(windowMs);
this.incr = function (key, cb) {
if (hits[key]) {
hits[key]++;
} else {
hits[key] = 1;
}
cb(null, hits[key], resetTime);
};
this.decrement = function (key) {
if (hits[key]) {
hits[key]--;
}
};
// export an API to allow hits all IPs to be reset
this.resetAll = function () {
hits = {};
resetTime = calculateNextResetTime(windowMs);
};
// export an API to allow hits from one IP to be reset
this.resetKey = function (key) {
delete hits[key];
};
// simply reset ALL hits every windowMs
const interval = setInterval(this.resetAll, windowMs);
if (interval.unref) {
interval.unref();
}
}
module.exports = MemoryStore;