0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2025-04-01 02:42:23 -05:00

feat: add rate limit to user api endpoints () ()

* feat: add rate limit to user api endpoints

* chore: fix test

* chore: refactor token endpoint

* chore: refactor
This commit is contained in:
Juan Picado 2021-12-24 01:07:26 +01:00 committed by GitHub
parent c91d6beb8b
commit f64e403f0a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 88 additions and 88 deletions

View file

@ -2,23 +2,28 @@ import _ from 'lodash';
import Cookies from 'cookies';
import { Config, RemoteUser } from '@verdaccio/types';
import { Response, Router } from 'express';
import express, { Response, Router } from 'express';
import { ErrorCode } from '../../../lib/utils';
import { API_ERROR, API_MESSAGE, HEADERS, HTTP_STATUS } from '../../../lib/constants';
import { createRemoteUser, createSessionToken, getApiToken, getAuthenticatedMessage, validatePassword } from '../../../lib/auth-utils';
import { logger } from '../../../lib/logger';
import { $RequestExtend, $ResponseExtend, $NextFunctionVer, IAuth } from '../../../../types';
import { limiter } from '../../user-rate-limit';
export default function (route: Router, auth: IAuth, config: Config): void {
route.get('/-/user/:org_couchdb_user', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
/* eslint new-cap:off */
const userRouter = express.Router();
userRouter.use(limiter);
userRouter.get('/-/user/:org_couchdb_user', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
res.status(HTTP_STATUS.OK);
next({
ok: getAuthenticatedMessage(req.remote_user.name),
});
});
route.put('/-/user/:org_couchdb_user/:_rev?/:revision?', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
userRouter.put('/-/user/:org_couchdb_user/:_rev?/:revision?', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
const { name, password } = req.body;
const remoteName = req.remote_user.name;
@ -69,7 +74,7 @@ export default function (route: Router, auth: IAuth, config: Config): void {
}
});
route.delete('/-/user/token/*', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
userRouter.delete('/-/user/token/*', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
res.status(HTTP_STATUS.OK);
next({
ok: API_MESSAGE.LOGGED_OUT,
@ -78,7 +83,7 @@ export default function (route: Router, auth: IAuth, config: Config): void {
// placeholder 'cause npm require to be authenticated to publish
// we do not do any real authentication yet
route.post('/_session', Cookies.express(), function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
userRouter.post('/_session', Cookies.express(), function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
res.cookies.set('AuthSession', String(Math.random()), createSessionToken());
next({
@ -87,4 +92,6 @@ export default function (route: Router, auth: IAuth, config: Config): void {
roles: [],
});
});
route.use(userRouter);
}

View file

@ -0,0 +1,13 @@
import { Response, Router } from 'express';
import { limiter } from '../../../user-rate-limit';
import profile from './profile';
import token from './token';
import v1Search from './search';
export default (auth, storage, config) => {
const route = Router(); /* eslint new-cap: 0 */
route.use(limiter);
route.use('/-/npm/v1/', profile(auth));
route.use('/-/npm/v1/', token(auth, storage, config));
return route;
};

View file

@ -17,7 +17,8 @@ export interface Profile {
fullname: string;
}
export default function (route: Router, auth: IAuth): void {
export default function (auth: IAuth): Router {
const profileRoute = Router(); /* eslint new-cap: 0 */
function buildProfile(name: string): Profile {
return {
tfa: false,
@ -27,68 +28,55 @@ export default function (route: Router, auth: IAuth): void {
created: '',
updated: '',
cidr_whitelist: null,
fullname: ''
fullname: '',
};
}
route.get(
'/-/npm/v1/user',
function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
if (_.isNil(req.remote_user.name) === false) {
return next(buildProfile(req.remote_user.name));
}
profileRoute.get('/user', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
if (_.isNil(req.remote_user.name) === false) {
return next(buildProfile(req.remote_user.name));
}
res.status(HTTP_STATUS.UNAUTHORIZED);
return next({
message: API_ERROR.MUST_BE_LOGGED,
});
});
profileRoute.post('/user', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
if (_.isNil(req.remote_user.name)) {
res.status(HTTP_STATUS.UNAUTHORIZED);
return next({
message: API_ERROR.MUST_BE_LOGGED
message: API_ERROR.MUST_BE_LOGGED,
});
}
);
route.post(
'/-/npm/v1/user',
function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
if (_.isNil(req.remote_user.name)) {
res.status(HTTP_STATUS.UNAUTHORIZED);
return next({
message: API_ERROR.MUST_BE_LOGGED
});
const { password, tfa } = req.body;
const { name } = req.remote_user;
if (_.isNil(password) === false) {
if (validatePassword(password.new) === false) {
/* eslint new-cap:off */
return next(ErrorCode.getCode(HTTP_STATUS.UNAUTHORIZED, API_ERROR.PASSWORD_SHORT()));
/* eslint new-cap:off */
}
const { password, tfa } = req.body;
const { name } = req.remote_user;
if (_.isNil(password) === false) {
if (validatePassword(password.new) === false) {
/* eslint new-cap:off */
return next(ErrorCode.getCode(HTTP_STATUS.UNAUTHORIZED, API_ERROR.PASSWORD_SHORT()));
/* eslint new-cap:off */
auth.changePassword(name, password.old, password.new, (err, isUpdated): $NextFunctionVer => {
if (_.isNull(err) === false) {
return next(ErrorCode.getCode(err.status, err.message) || ErrorCode.getConflict(err.message));
}
auth.changePassword(
name,
password.old,
password.new,
(err, isUpdated): $NextFunctionVer => {
if (_.isNull(err) === false) {
return next(
ErrorCode.getCode(err.status, err.message) || ErrorCode.getConflict(err.message)
);
}
if (isUpdated) {
return next(buildProfile(req.remote_user.name));
}
return next(ErrorCode.getInternalError(API_ERROR.INTERNAL_SERVER_ERROR));
}
);
} else if (_.isNil(tfa) === false) {
return next(
ErrorCode.getCode(HTTP_STATUS.SERVICE_UNAVAILABLE, SUPPORT_ERRORS.TFA_DISABLED)
);
} else {
return next(ErrorCode.getCode(HTTP_STATUS.INTERNAL_ERROR, APP_ERROR.PROFILE_ERROR));
}
if (isUpdated) {
return next(buildProfile(req.remote_user.name));
}
return next(ErrorCode.getInternalError(API_ERROR.INTERNAL_SERVER_ERROR));
});
} else if (_.isNil(tfa) === false) {
return next(ErrorCode.getCode(HTTP_STATUS.SERVICE_UNAVAILABLE, SUPPORT_ERRORS.TFA_DISABLED));
} else {
return next(ErrorCode.getCode(HTTP_STATUS.INTERNAL_ERROR, APP_ERROR.PROFILE_ERROR));
}
);
});
return profileRoute;
}

View file

@ -9,6 +9,7 @@ import { stringToMD5 } from '../../../../lib/crypto-utils';
import { logger } from '../../../../lib/logger';
import { $NextFunctionVer, $RequestExtend, IAuth, IStorageHandler } from '../../../../../types';
import { limiter } from '../../../user-rate-limit';
const debug = buildDebug('verdaccio:token');
export type NormalizeToken = Token & {
@ -23,8 +24,10 @@ function normalizeToken(token: Token): NormalizeToken {
}
// https://github.com/npm/npm-profile/blob/latest/lib/index.js
export default function (route: Router, auth: IAuth, storage: IStorageHandler, config: Config): void {
route.get('/-/npm/v1/tokens', async function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {
export default function (auth: IAuth, storage: IStorageHandler, config: Config): Router {
const tokenRoute = Router(); /* eslint new-cap: 0 */
// tokenRoute.use(limiter);
tokenRoute.get('/tokens', async function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {
const { name } = req.remote_user;
if (_.isNil(name) === false) {
@ -47,7 +50,7 @@ export default function (route: Router, auth: IAuth, storage: IStorageHandler, c
return next(ErrorCode.getUnauthorized());
});
route.post('/-/npm/v1/tokens', function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {
tokenRoute.post('/tokens', function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {
const { password, readonly, cidr_whitelist } = req.body;
const { name } = req.remote_user;
@ -107,7 +110,7 @@ export default function (route: Router, auth: IAuth, storage: IStorageHandler, c
});
});
route.delete('/-/npm/v1/tokens/token/:tokenKey', async (req: $RequestExtend, res: Response, next: $NextFunctionVer) => {
tokenRoute.delete('/tokens/token/:tokenKey', async (req: $RequestExtend, res: Response, next: $NextFunctionVer) => {
const {
params: { tokenKey },
} = req;
@ -126,4 +129,6 @@ export default function (route: Router, auth: IAuth, storage: IStorageHandler, c
}
return next(ErrorCode.getUnauthorized());
});
return tokenRoute;
}

View file

@ -11,8 +11,7 @@ import publish from './api/publish';
import search from './api/search';
import pkg from './api/package';
import stars from './api/stars';
import profile from './api/v1/profile';
import token from './api/v1/token';
import npmV1 from './api/v1';
import v1Search from './api/v1/search';
const { match, validateName, validatePackage, encodeScopePackage, antiLoop } = require('../middleware');
@ -44,14 +43,13 @@ export default function (config: Config, auth: IAuth, storage: IStorageHandler)
// for "npm whoami"
whoami(app);
pkg(app, auth, storage, config);
profile(app, auth);
search(app, auth, storage);
user(app, auth, config);
distTags(app, auth, storage);
publish(app, auth, storage, config);
ping(app);
stars(app, storage);
v1Search(app, auth, storage);
token(app, auth, storage, config);
app.use(npmV1(auth, storage, config));
user(app, auth, config);
return app;
}

View file

@ -0,0 +1,11 @@
import RateLimit from 'express-rate-limit';
// we limit max 1000 request per 15 minutes on user endpoints
const defaultUserRateLimiting = {
windowMs: 15 * 60 * 1000, // 15 minutes
max: 1000,
};
// @ts-ignore
const limiter = new RateLimit(defaultUserRateLimiting);
export { limiter };

View file

@ -15,12 +15,9 @@ const route = Router(); /* eslint new-cap: 0 */
*/
export default function (config: Config, auth: IAuth, storage: IStorageHandler): Router {
Search.configureStorage(storage);
// validate all of these params as a package name
// this might be too harsh, so ask if it causes trouble
// $FlowFixMe
route.param('package', validatePackage);
// $FlowFixMe
route.param('filename', validateName);
route.param('version', validateName);
route.param('anything', match(/.*/));
@ -32,11 +29,5 @@ export default function (config: Config, auth: IAuth, storage: IStorageHandler):
addPackageWebApi(route, storage, auth, config);
addSearchWebApi(route, storage, auth);
addUserAuthApi(route, auth, config);
// What are you looking for? logout? client side will remove token when user click logout,
// or it will auto expire after 24 hours.
// This token is different with the token send to npm client.
// We will/may replace current token with JWT in next major release, and it will not expire at all(configurable).
return route;
}

View file

@ -2,7 +2,6 @@
* @prettier
*/
import _ from 'lodash';
import RateLimit from 'express-rate-limit';
import express, { Router, Response, Request } from 'express';
import { Config, RemoteUser, JWTSignOptions } from '@verdaccio/types';
@ -10,19 +9,9 @@ import { API_ERROR, APP_ERROR, HEADERS, HTTP_STATUS } from '../../../lib/constan
import { IAuth, $NextFunctionVer } from '../../../../types';
import { ErrorCode } from '../../../lib/utils';
import { getSecurity, validatePassword } from '../../../lib/auth-utils';
import { limiter } from '../../user-rate-limit';
function addUserAuthApi(route: Router, auth: IAuth, config: Config): void {
/* eslint new-cap:off */
const userRouter = express.Router();
// we limit max 100 request per 15 minutes on user endpoints
// @ts-ignore
const limiter = new RateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
// @ts-ignore
...config?.web?.rateLimit,
});
route.use(limiter);
route.post('/login', function (req: Request, res: Response, next: $NextFunctionVer): void {
const { username, password } = req.body;
@ -69,8 +58,6 @@ function addUserAuthApi(route: Router, auth: IAuth, config: Config): void {
return next(ErrorCode.getCode(HTTP_STATUS.BAD_REQUEST, APP_ERROR.PASSWORD_VALIDATION));
}
});
route.use(userRouter);
}
export default addUserAuthApi;