1
0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-11-08 23:25:51 +01:00

feat: add property forceMigrateToSecureLegacySignature (#4625)

* migrate to forceMigrateToSecureLegacySignature

* Update token.ts

* remove dep

* Update token.ts
This commit is contained in:
Juan Picado 2024-05-05 21:57:22 +02:00 committed by GitHub
parent 6ce34256e6
commit 2941522099
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
99 changed files with 1080 additions and 1338 deletions

@ -2,6 +2,12 @@ name: CI
on: [push, pull_request]
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
name: Node ${{ matrix.node_version }}
@ -9,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node_version: [14, 16, 18, 19, 20, 21]
node_version: [14, 16, 18, 19, 20, 21, 22]
runs-on: ubuntu-latest

1722
.pnp.cjs generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -33,7 +33,9 @@ Install with npm:
npm install --location=global verdaccio
```
> Node.js v14 or higher is required for Verdaccio 5
**Node.js v14 or higher is required for Verdaccio 5**
> It's recommended using Node.js 20 (or latest LTS)
or pull [Docker official image](https://verdaccio.org/docs/docker)
@ -55,8 +57,6 @@ helm repo update
helm install verdaccio/verdaccio
```
Are you still using **Verdaccio 4**?. Check the [migration guide from 4.x to 5.x](https://verdaccio.org/blog/2021/04/14/verdaccio-5-migration-guide).
## Programmatic API
Verdaccio can be used as a module for launch a server programmatically, [you can find more info at the website](https://verdaccio.org/docs/verdaccio-programmatically#using-the-module-api).

@ -127,6 +127,8 @@ server:
# security:
# api:
# legacy: true
# # recomended set to true for older installations
# migrateToSecureLegacySignature: true
# jwt:
# sign:
# expiresIn: 29d

@ -131,6 +131,8 @@ server:
# security:
# api:
# legacy: true
# # recomended set to true for older installations
# migrateToSecureLegacySignature: true
# jwt:
# sign:
# expiresIn: 29d

@ -20,23 +20,23 @@
},
"dependencies": {
"@cypress/request": "3.0.1",
"@verdaccio/config": "7.0.0-next-7.13",
"@verdaccio/core": "7.0.0-next-7.13",
"@verdaccio/auth": "7.0.0-next-7.15",
"@verdaccio/config": "7.0.0-next-7.15",
"@verdaccio/core": "7.0.0-next-7.15",
"@verdaccio/local-storage-legacy": "11.0.2",
"@verdaccio/logger-7": "7.0.0-next-7.13",
"@verdaccio/middleware": "7.0.0-next-7.13",
"@verdaccio/logger-7": "7.0.0-next-7.15",
"@verdaccio/middleware": "7.0.0-next-7.15",
"@verdaccio/search-indexer": "7.0.0-next-7.2",
"@verdaccio/signature": "7.0.0-next.3",
"@verdaccio/signature": "7.0.0-next-7.5",
"@verdaccio/streams": "10.2.1",
"@verdaccio/tarball": "12.0.0-next-7.13",
"@verdaccio/ui-theme": "7.0.0-next-7.13",
"@verdaccio/url": "12.0.0-next-7.13",
"@verdaccio/utils": "7.0.0-next-7.13",
"@verdaccio/tarball": "12.0.0-next-7.15",
"@verdaccio/ui-theme": "7.0.0-next-7.15",
"@verdaccio/url": "12.0.0-next-7.15",
"@verdaccio/utils": "7.0.0-next-7.15",
"JSONStream": "1.3.5",
"async": "3.2.5",
"clipanion": "3.2.1",
"compression": "1.7.4",
"cookies": "0.9.1",
"cors": "2.8.5",
"debug": "^4.3.4",
"envinfo": "7.11.1",
@ -55,13 +55,13 @@
"pkginfo": "0.4.1",
"semver": "7.6.0",
"validator": "13.11.0",
"verdaccio-audit": "12.0.0-next-7.13",
"verdaccio-htpasswd": "12.0.0-next-7.13"
"verdaccio-audit": "12.0.0-next-7.15",
"verdaccio-htpasswd": "12.0.0-next-7.15"
},
"devDependencies": {
"@babel/cli": "7.24.1",
"@babel/core": "7.24.3",
"@babel/eslint-parser": "7.23.3",
"@babel/cli": "7.24.5",
"@babel/core": "7.24.5",
"@babel/eslint-parser": "7.24.5",
"@babel/node": "7.23.9",
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-decorators": "7.24.1",
@ -76,13 +76,13 @@
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/plugin-syntax-import-meta": "7.10.4",
"@babel/plugin-transform-async-to-generator": "7.24.1",
"@babel/plugin-transform-classes": "7.24.1",
"@babel/plugin-transform-classes": "7.24.5",
"@babel/plugin-transform-runtime": "7.24.3",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "7.24.3",
"@babel/preset-typescript": "7.23.3",
"@babel/preset-env": "7.24.5",
"@babel/preset-typescript": "7.24.1",
"@babel/register": "7.23.7",
"@babel/runtime": "7.24.1",
"@babel/runtime": "7.24.5",
"@octokit/rest": "19.0.13",
"@trivago/prettier-plugin-sort-imports": "4.3.0",
"@types/async": "3.2.24",
@ -100,7 +100,7 @@
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"@verdaccio-scope/verdaccio-auth-foo": "0.0.2",
"@verdaccio/types": "12.0.0-next.2",
"@verdaccio/types": "12.0.0-next-7.3",
"babel-jest": "29.7.0",
"babel-plugin-dynamic-import-node": "2.3.3",
"cross-env": "7.0.3",
@ -122,7 +122,7 @@
"jest-junit": "15.0.0",
"lockfile-lint": "4.12.1",
"nock": "13.5.0",
"node-mocks-http": "^1.13.0",
"node-mocks-http": "^1.14.1",
"pinst": "2.1.6",
"prettier": "3.2.2",
"rimraf": "3.0.2",
@ -186,5 +186,10 @@
"url": "https://opencollective.com/verdaccio",
"logo": "https://opencollective.com/verdaccio/logo.txt"
},
"packageManager": "yarn@3.8.1"
"packageManager": "yarn@3.8.1",
"dependenciesMeta": {
"@verdaccio/signature@7.0.0-next-7.5": {
"unplugged": true
}
}
}

@ -1,14 +1,14 @@
import Cookies from 'cookies';
import express, { Response, Router } from 'express';
import _ from 'lodash';
import { getApiToken } from '@verdaccio/auth';
import { createRemoteUser } from '@verdaccio/config';
import { validationUtils } from '@verdaccio/core';
import { rateLimit } from '@verdaccio/middleware';
import { Config, RemoteUser } from '@verdaccio/types';
import { createSessionToken, getAuthenticatedMessage } from '@verdaccio/utils';
import { getAuthenticatedMessage } from '@verdaccio/utils';
import Auth from '../../../lib/auth';
import { getApiToken, validatePassword } from '../../../lib/auth-utils';
import { API_ERROR, API_MESSAGE, HEADERS, HTTP_STATUS } from '../../../lib/constants';
import { logger } from '../../../lib/logger';
import { ErrorCode } from '../../../lib/utils';
@ -63,7 +63,7 @@ export default function (route: Router, auth: Auth, config: Config): void {
}
);
} else {
if (validatePassword(password) === false) {
if (validationUtils.validatePassword(password) === false) {
// eslint-disable-next-line new-cap
return next(ErrorCode.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.PASSWORD_SHORT));
}

@ -1,11 +1,11 @@
import { Response, Router } from 'express';
import _ from 'lodash';
import { validationUtils } from '@verdaccio/core';
import { rateLimit } from '@verdaccio/middleware';
import { ConfigYaml } from '@verdaccio/types';
import Auth from '../../../../lib/auth';
import { validatePassword } from '../../../../lib/auth-utils';
import { API_ERROR, APP_ERROR, HTTP_STATUS, SUPPORT_ERRORS } from '../../../../lib/constants';
import { ErrorCode } from '../../../../lib/utils';
import { $NextFunctionVer, $RequestExtend } from '../../../../types';
@ -65,7 +65,7 @@ export default function (router: Router, auth: Auth, config: ConfigYaml) {
const { name } = req.remote_user;
if (_.isNil(password) === false) {
if (validatePassword(password.new) === false) {
if (validationUtils.validatePassword(password.new) === false) {
/* eslint new-cap:off */
return next(ErrorCode.getCode(HTTP_STATUS.UNAUTHORIZED, API_ERROR.PASSWORD_SHORT));
/* eslint new-cap:off */

@ -2,12 +2,12 @@ import buildDebug from 'debug';
import { Response, Router } from 'express';
import _ from 'lodash';
import { getApiToken } from '@verdaccio/auth';
import { rateLimit } from '@verdaccio/middleware';
import { Config, RemoteUser, Token } from '@verdaccio/types';
import { stringToMD5 } from '@verdaccio/utils';
import Auth from '../../../../lib/auth';
import { getApiToken } from '../../../../lib/auth-utils';
import { HEADERS, HTTP_STATUS, SUPPORT_ERRORS } from '../../../../lib/constants';
import { logger } from '../../../../lib/logger';
import Storage from '../../../../lib/storage';
@ -81,10 +81,10 @@ export default function (router: Router, auth: Auth, storage: Storage, config: C
}
try {
const token = await getApiToken(auth, config, user, password);
const key = stringToMD5(token);
const token = (await getApiToken(auth, config, user, password)) as string;
const key = stringToMD5(token as string);
// TODO: use a utility here
const maskedToken = mask(token, 5);
const maskedToken = mask(token as string, 5);
const created = new Date().getTime();
/**

@ -1,11 +1,12 @@
import { Request, Response, Router } from 'express';
import _ from 'lodash';
import { validationUtils } from '@verdaccio/core';
import { rateLimit } from '@verdaccio/middleware';
import { Config, JWTSignOptions, RemoteUser } from '@verdaccio/types';
import Auth from '../../../lib/auth';
import { getSecurity, validatePassword } from '../../../lib/auth-utils';
import { getSecurity } from '../../../lib/auth-utils';
import { API_ERROR, APP_ERROR, HEADERS, HTTP_STATUS } from '../../../lib/constants';
import { ErrorCode } from '../../../lib/utils';
import { $NextFunctionVer } from '../../../types';
@ -48,7 +49,7 @@ function addUserAuthApi(route: Router, auth: Auth, config: Config): Router {
const { password } = req.body;
const { name } = req.remote_user;
if (validatePassword(password.new) === false) {
if (validationUtils.validatePassword(password.new) === false) {
auth.changePassword(name as string, password.old, password.new, (err, isUpdated): void => {
if (_.isNil(err) && isUpdated) {
next({

@ -1,9 +1,7 @@
import buildDebug from 'debug';
import _ from 'lodash';
import { createAnonymousRemoteUser } from '@verdaccio/config';
import { pluginUtils } from '@verdaccio/core';
import { aesDecryptDeprecated as aesDecrypt, verifyPayload } from '@verdaccio/signature';
import {
APITokenOptions,
Callback,
@ -13,19 +11,10 @@ import {
RemoteUser,
Security,
} from '@verdaccio/types';
import { buildUserBuffer } from '@verdaccio/utils';
import { AuthMiddlewarePayload, AuthTokenHeader, BasicPayload, IAuthWebUI } from '../types';
import {
API_ERROR,
DEFAULT_MIN_LIMIT_PASSWORD,
HTTP_STATUS,
TIME_EXPIRATION_1H,
TOKEN_BASIC,
TOKEN_BEARER,
} from './constants';
import { API_ERROR, DEFAULT_MIN_LIMIT_PASSWORD, TIME_EXPIRATION_1H } from './constants';
import { logger } from './logger';
import { ErrorCode, convertPayloadToBase64 } from './utils';
import { ErrorCode } from './utils';
const debug = buildDebug('verdaccio');
@ -110,6 +99,7 @@ const defaultWebTokenOptions: JWTOptions = {
const defaultApiTokenConf: APITokenOptions = {
legacy: true,
migrateToSecureLegacySignature: false,
};
export const defaultSecurity: Security = {
@ -124,120 +114,3 @@ export function getSecurity(config: Config): Security {
return defaultSecurity;
}
export function isAESLegacy(security: Security): boolean {
const { legacy, jwt } = security.api;
return _.isNil(legacy) === false && _.isNil(jwt) && legacy === true;
}
export async function getApiToken(
auth: IAuthWebUI,
config: Config,
remoteUser: RemoteUser,
aesPassword: string
): Promise<string> {
const security: Security = getSecurity(config);
if (isAESLegacy(security)) {
// fallback all goes to AES encryption
return await new Promise((resolve): void => {
resolve(
auth.aesEncrypt(buildUserBuffer(remoteUser.name as string, aesPassword)).toString('base64')
);
});
}
// i am wiling to use here _.isNil but flow does not like it yet.
const { jwt } = security.api;
if (jwt && jwt.sign) {
return await auth.jwtEncrypt(remoteUser, jwt.sign);
}
return await new Promise((resolve): void => {
resolve(
auth.aesEncrypt(buildUserBuffer(remoteUser.name as string, aesPassword)).toString('base64')
);
});
}
export function parseAuthTokenHeader(authorizationHeader: string): AuthTokenHeader {
const parts = authorizationHeader.split(' ');
const [scheme, token] = parts;
return { scheme, token };
}
export function parseBasicPayload(credentials: string): BasicPayload {
const index = credentials.indexOf(':');
if (index < 0) {
return;
}
const user: string = credentials.slice(0, index);
const password: string = credentials.slice(index + 1);
return { user, password };
}
export function parseAESCredentials(authorizationHeader: string, secret: string) {
const { scheme, token } = parseAuthTokenHeader(authorizationHeader);
// basic is deprecated and should not be enforced
if (scheme.toUpperCase() === TOKEN_BASIC.toUpperCase()) {
const credentials = convertPayloadToBase64(token).toString();
return credentials;
} else if (scheme.toUpperCase() === TOKEN_BEARER.toUpperCase()) {
const tokenAsBuffer = convertPayloadToBase64(token);
const credentials = aesDecrypt(tokenAsBuffer, secret).toString('utf8');
return credentials;
}
}
export const expireReasons: string[] = ['JsonWebTokenError', 'TokenExpiredError'];
export function verifyJWTPayload(token: string, secret: string): RemoteUser {
try {
const payload: RemoteUser = verifyPayload(token, secret);
return payload;
} catch (error: any) {
// #168 this check should be removed as soon AES encrypt is removed.
if (expireReasons.includes(error.name)) {
// it might be possible the jwt configuration is enabled and
// old tokens fails still remains in usage, thus
// we return an anonymous user to force log in.
return createAnonymousRemoteUser();
}
throw ErrorCode.getCode(HTTP_STATUS.UNAUTHORIZED, error.message);
}
}
export function isAuthHeaderValid(authorization: string): boolean {
return authorization.split(' ').length === 2;
}
export function getMiddlewareCredentials(
security: Security,
secret: string,
authorizationHeader: string
): AuthMiddlewarePayload {
if (isAESLegacy(security)) {
const credentials = parseAESCredentials(authorizationHeader, secret);
if (!credentials) {
return;
}
const parsedCredentials = parseBasicPayload(credentials);
if (!parsedCredentials) {
return;
}
return parsedCredentials;
}
const { scheme, token } = parseAuthTokenHeader(authorizationHeader);
if (_.isString(token) && scheme.toUpperCase() === TOKEN_BEARER.toUpperCase()) {
return verifyJWTPayload(token, secret);
}
}

@ -2,9 +2,22 @@ import buildDebug from 'debug';
import { NextFunction } from 'express';
import _ from 'lodash';
import {
getMiddlewareCredentials,
isAESLegacy,
isAuthHeaderValid,
parseAuthTokenHeader,
verifyJWTPayload,
} from '@verdaccio/auth';
import { createAnonymousRemoteUser, createRemoteUser } from '@verdaccio/config';
import { VerdaccioError, pluginUtils } from '@verdaccio/core';
import { aesEncryptDeprecated as aesEncrypt, signPayload } from '@verdaccio/signature';
import {
aesEncrypt,
aesEncryptDeprecated,
parseBasicPayload,
signPayload,
utils as signatureUtils,
} from '@verdaccio/signature';
import {
AllowAccess,
Callback,
@ -19,16 +32,7 @@ import { getMatchedPackagesSpec } from '@verdaccio/utils';
import loadPlugin from '../lib/plugin-loader';
import { $RequestExtend, $ResponseExtend, AESPayload } from '../types';
import {
getDefaultPlugins,
getMiddlewareCredentials,
getSecurity,
isAESLegacy,
isAuthHeaderValid,
parseAuthTokenHeader,
parseBasicPayload,
verifyJWTPayload,
} from './auth-utils';
import { getDefaultPlugins, getSecurity } from './auth-utils';
import { API_ERROR, SUPPORT_ERRORS, TOKEN_BASIC, TOKEN_BEARER } from './constants';
import { logger } from './logger';
import { ErrorCode, convertPayloadToBase64 } from './utils';
@ -526,8 +530,17 @@ class Auth {
/**
* Encrypt a string.
*/
public aesEncrypt(buf: Buffer): Buffer {
return aesEncrypt(buf, this.secret);
public aesEncrypt(value: string): string | void {
if (this.secret.length === signatureUtils.TOKEN_VALID_LENGTH) {
debug('signing with enhanced aes legacy');
const token = aesEncrypt(value, this.secret);
return token;
} else {
debug('signing with enhanced aes deprecated legacy');
// deprecated aes (legacy) signature, only must be used for legacy version
const token = aesEncryptDeprecated(Buffer.from(value), this.secret).toString('base64');
return token;
}
}
}

@ -1,4 +1,3 @@
// import assert from 'assert';
import _ from 'lodash';
import { Config as ConfigCore } from '@verdaccio/config';
@ -6,7 +5,7 @@ import { Config as ConfigCore } from '@verdaccio/config';
class Config extends ConfigCore {
public constructor(config: any) {
config.configPath = config.self_path;
super(config, { forceEnhancedLegacySignature: false });
super(config, { forceMigrateToSecureLegacySignature: false });
}
}

@ -1,5 +1,6 @@
import assert from 'assert';
import async, { AsyncResultArrayCallback } from 'async';
import buildDebug from 'debug';
import _ from 'lodash';
import Stream from 'stream';
@ -38,6 +39,8 @@ import ProxyStorage from './up-storage';
import { setupUpLinks, updateVersionsHiddenUpLink } from './uplink-util';
import { ErrorCode, isObject, normalizeDistTags } from './utils';
const debug = buildDebug('verdaccio:storage');
class Storage {
public localStorage: LocalStorage;
public config: Config;
@ -54,11 +57,15 @@ class Storage {
this.localStorage = null;
}
public init(config: Config, filters: IPluginFilters = []): Promise<string> {
this.filters = filters;
this.localStorage = new LocalStorage(this.config, logger);
return this.localStorage.getSecret(config);
public async init(config: Config, filters: IPluginFilters = []): Promise<void> {
if (this.localStorage === null) {
this.filters = filters;
this.localStorage = new LocalStorage(this.config, logger);
await this.localStorage.getSecret(config);
debug('initialization completed');
} else {
debug('storage has been already initialized');
}
}
/**

@ -472,4 +472,9 @@ export function hasLogin(config: Config) {
return _.isNil(config?.web?.login) || config?.web?.login === true;
}
export function isNodeVersionHigherThanV22() {
const [major, minor] = process.versions.node.split('.').map(Number);
return major > 22 || (major === 22 && minor > 0);
}
export { buildTokenUtil as buildToken, parseConfigFile };

@ -44,14 +44,6 @@ export interface AESPayload {
password: string;
}
export interface AuthTokenHeader {
scheme: string;
token: string;
}
export type BasicPayload = AESPayload | void;
export type AuthMiddlewarePayload = RemoteUser | BasicPayload;
export interface Utils {
ErrorCode: any;
getLatestVersion: Callback;
@ -78,11 +70,6 @@ export type $ResponseExtend = Response & { cookies?: any };
export type $NextFunctionVer = NextFunction & any;
export type $SidebarPackage = Package & { latest: any };
export interface IAuthWebUI {
jwtEncrypt(user: RemoteUser, signOptions: JWTSignOptions): Promise<string>;
aesEncrypt(buf: Buffer): Buffer;
}
interface IAuthMiddleware {
apiJWTmiddleware(): $NextFunctionVer;
webUIJWTmiddleware(): $NextFunctionVer;

@ -17,20 +17,23 @@ describe('token', () => {
describe('basics', () => {
const FAKE_TOKEN: string = buildToken(TOKEN_BEARER, 'fake');
test.each([['user.yaml'], ['user.jwt.yaml']])('should test add a new user', async (conf) => {
nock('https://registry.verdaccio.org/').get(`/vue`).once().reply(200, { name: 'vue' });
const app = await initializeServer(conf);
const credentials = { name: 'JotaJWT', password: 'secretPass' };
const response = await createUser(app, credentials.name, credentials.password);
expect(response.body.ok).toMatch(`user '${credentials.name}' created`);
test.each([['user.yaml'], ['user.jwt.yaml']])(
'should test add a new user for %s',
async (conf) => {
nock('https://registry.verdaccio.org/').get(`/vue`).once().reply(200, { name: 'vue' });
const app = await initializeServer(conf);
const credentials = { name: 'JotaJWT', password: 'secretPass' };
const response = await createUser(app, credentials.name, credentials.password);
expect(response.body.ok).toMatch(`user '${credentials.name}' created`);
const vueResponse = await getPackage(app, response.body.token, 'vue');
expect(vueResponse.body).toBeDefined();
expect(vueResponse.body.name).toMatch('vue');
const vueResponse = await getPackage(app, response.body.token, 'vue');
expect(vueResponse.body).toBeDefined();
expect(vueResponse.body.name).toMatch('vue');
const vueFailResp = await getPackage(app, FAKE_TOKEN, 'vue', HTTP_STATUS.UNAUTHORIZED);
expect(vueFailResp.body.error).toMatch(FORBIDDEN_VUE);
});
const vueFailResp = await getPackage(app, FAKE_TOKEN, 'vue', HTTP_STATUS.UNAUTHORIZED);
expect(vueFailResp.body.error).toMatch(FORBIDDEN_VUE);
}
);
test.each([['user.yaml'], ['user.jwt.yaml']])('should login an user', async (conf) => {
const app = await initializeServer(conf);

@ -1,363 +0,0 @@
import _ from 'lodash';
import { aesDecryptDeprecated as aesDecrypt, verifyPayload } from '@verdaccio/signature';
import { Config, RemoteUser, Security } from '@verdaccio/types';
import { buildUserBuffer } from '@verdaccio/utils';
import Auth from '../../../../src/lib/auth';
import { getApiToken, getMiddlewareCredentials, getSecurity } from '../../../../src/lib/auth-utils';
import AppConfig from '../../../../src/lib/config';
import { CHARACTER_ENCODING, TOKEN_BEARER } from '../../../../src/lib/constants';
import { setup } from '../../../../src/lib/logger';
import { buildToken, convertPayloadToBase64, parseConfigFile } from '../../../../src/lib/utils';
import { IAuth } from '../../../types';
import { parseConfigurationFile } from '../../__helper';
import configExample from '../../partials/config';
setup([]);
describe('Auth utilities', () => {
jest.setTimeout(20000);
const parseConfigurationSecurityFile = (name) => {
return parseConfigurationFile(`security/${name}`);
};
function getConfig(configFileName: string, secret: string) {
const conf = parseConfigFile(parseConfigurationSecurityFile(configFileName));
const secConf = _.merge(configExample(), conf);
secConf.secret = secret;
const config: Config = new AppConfig(secConf);
return config;
}
async function signCredentials(
configFileName: string,
username: string,
password: string,
secret = '12345',
methodToSpy: string,
methodNotBeenCalled: string
): Promise<string> {
const config: Config = getConfig(configFileName, secret);
const auth: IAuth = new Auth(config);
// @ts-ignore
const spy = jest.spyOn(auth, methodToSpy);
// @ts-ignore
const spyNotCalled = jest.spyOn(auth, methodNotBeenCalled);
const user: RemoteUser = {
name: username,
real_groups: ['test', '$all', '$authenticated', '@all', '@authenticated', 'all'],
groups: ['company-role1', 'company-role2'],
};
const token = await getApiToken(auth, config, user, password);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(1);
expect(spyNotCalled).not.toHaveBeenCalled();
expect(token).toBeDefined();
return token;
}
const verifyJWT = (token: string, user: string, password: string, secret: string) => {
const payload = verifyPayload(token, secret);
expect(payload.name).toBe(user);
expect(payload.groups).toBeDefined();
expect(payload.groups).toEqual([
'company-role1',
'company-role2',
'test',
'$all',
'$authenticated',
'@all',
'@authenticated',
'all',
]);
expect(payload.real_groups).toBeDefined();
expect(payload.real_groups).toEqual([
'test',
'$all',
'$authenticated',
'@all',
'@authenticated',
'all',
]);
};
const verifyAES = (token: string, user: string, password: string, secret: string) => {
const payload = aesDecrypt(convertPayloadToBase64(token), secret).toString(
CHARACTER_ENCODING.UTF8
);
const content = payload.split(':');
expect(content[0]).toBe(user);
expect(content[0]).toBe(password);
};
describe('getApiToken test', () => {
test('should sign token with aes and security missing', async () => {
const token = await signCredentials(
'security-missing',
'test',
'test',
'1234567',
'aesEncrypt',
'jwtEncrypt'
);
verifyAES(token, 'test', 'test', '1234567');
expect(_.isString(token)).toBeTruthy();
});
test('should sign token with aes and security empty', async () => {
const token = await signCredentials(
'security-empty',
'test',
'test',
'123456',
'aesEncrypt',
'jwtEncrypt'
);
verifyAES(token, 'test', 'test', '123456');
expect(_.isString(token)).toBeTruthy();
});
test('should sign token with aes', async () => {
const token = await signCredentials(
'security-basic',
'test',
'test',
'123456',
'aesEncrypt',
'jwtEncrypt'
);
verifyAES(token, 'test', 'test', '123456');
expect(_.isString(token)).toBeTruthy();
});
test('should sign token with legacy and jwt disabled', async () => {
const token = await signCredentials(
'security-no-legacy',
'test',
'test',
'x8T#ZCx=2t',
'aesEncrypt',
'jwtEncrypt'
);
expect(_.isString(token)).toBeTruthy();
verifyAES(token, 'test', 'test', 'x8T#ZCx=2t');
});
test('should sign token with legacy enabled and jwt enabled', async () => {
const token = await signCredentials(
'security-jwt-legacy-enabled',
'test',
'test',
'secret',
'jwtEncrypt',
'aesEncrypt'
);
verifyJWT(token, 'test', 'test', 'secret');
expect(_.isString(token)).toBeTruthy();
});
test('should sign token with jwt enabled', async () => {
const token = await signCredentials(
'security-jwt',
'test',
'test',
'secret',
'jwtEncrypt',
'aesEncrypt'
);
expect(_.isString(token)).toBeTruthy();
verifyJWT(token, 'test', 'test', 'secret');
});
test('should sign with jwt whether legacy is disabled', async () => {
const token = await signCredentials(
'security-legacy-disabled',
'test',
'test',
'secret',
'jwtEncrypt',
'aesEncrypt'
);
expect(_.isString(token)).toBeTruthy();
verifyJWT(token, 'test', 'test', 'secret');
});
});
describe('getMiddlewareCredentials test', () => {
describe('should get AES credentials', () => {
test.concurrent('should unpack aes token and credentials', async () => {
const secret = 'secret';
const user = 'test';
const pass = 'test';
const token = await signCredentials(
'security-legacy',
user,
pass,
secret,
'aesEncrypt',
'jwtEncrypt'
);
const config: Config = getConfig('security-legacy', secret);
const security: Security = getSecurity(config);
const credentials = getMiddlewareCredentials(security, secret, `Bearer ${token}`);
expect(credentials).toBeDefined();
// @ts-ignore
expect(credentials.user).toEqual(user);
// @ts-ignore
expect(credentials.password).toEqual(pass);
});
test.concurrent('should unpack aes token and credentials', async () => {
const secret = 'secret';
const user = 'test';
const pass = 'test';
const token = buildUserBuffer(user, pass).toString('base64');
const config: Config = getConfig('security-legacy', secret);
const security: Security = getSecurity(config);
const credentials = getMiddlewareCredentials(security, secret, `Basic ${token}`);
expect(credentials).toBeDefined();
// @ts-ignore
expect(credentials.user).toEqual(user);
// @ts-ignore
expect(credentials.password).toEqual(pass);
});
test.concurrent('should return empty credential wrong secret key', async () => {
const secret = 'secret';
const token = await signCredentials(
'security-legacy',
'test',
'test',
secret,
'aesEncrypt',
'jwtEncrypt'
);
const config: Config = getConfig('security-legacy', secret);
const security: Security = getSecurity(config);
const credentials = getMiddlewareCredentials(
security,
'BAD_SECRET',
buildToken(TOKEN_BEARER, token)
);
expect(credentials).not.toBeDefined();
});
test.concurrent('should return empty credential wrong scheme', async () => {
const secret = 'secret';
const token = await signCredentials(
'security-legacy',
'test',
'test',
secret,
'aesEncrypt',
'jwtEncrypt'
);
const config: Config = getConfig('security-legacy', secret);
const security: Security = getSecurity(config);
const credentials = getMiddlewareCredentials(
security,
secret,
buildToken('BAD_SCHEME', token)
);
expect(credentials).not.toBeDefined();
});
test.concurrent('should return empty credential corrupted payload', async () => {
const secret = 'secret';
const config: Config = getConfig('security-legacy', secret);
const auth: IAuth = new Auth(config);
const token = auth.aesEncrypt(Buffer.from(`corruptedBuffer`)).toString('base64');
const security: Security = getSecurity(config);
const credentials = getMiddlewareCredentials(
security,
secret,
buildToken(TOKEN_BEARER, token)
);
expect(credentials).not.toBeDefined();
});
});
describe('should get JWT credentials', () => {
test('should return anonymous whether token is corrupted', () => {
const config: Config = getConfig('security-jwt', '12345');
const security: Security = getSecurity(config);
const credentials = getMiddlewareCredentials(
security,
'12345',
buildToken(TOKEN_BEARER, 'fakeToken')
) as RemoteUser;
expect(credentials).toBeDefined();
expect(credentials.name).not.toBeDefined();
expect(credentials.real_groups).toBeDefined();
expect(credentials.real_groups).toEqual([]);
expect(credentials.groups).toEqual(['$all', '$anonymous', '@all', '@anonymous']);
});
test('should return anonymous whether token and scheme are corrupted', () => {
const config: Config = getConfig('security-jwt', '12345');
const security: Security = getSecurity(config);
const credentials = getMiddlewareCredentials(
security,
'12345',
buildToken('FakeScheme', 'fakeToken')
);
expect(credentials).not.toBeDefined();
});
test('should verify succesfully a JWT token', async () => {
const secret = 'secret';
const user = 'test';
const config: Config = getConfig('security-jwt', secret);
const token = await signCredentials(
'security-jwt',
user,
'secretTest',
secret,
'jwtEncrypt',
'aesEncrypt'
);
const security: Security = getSecurity(config);
const credentials = getMiddlewareCredentials(
security,
secret,
buildToken(TOKEN_BEARER, token)
) as RemoteUser;
expect(credentials).toBeDefined();
expect(credentials.name).toEqual(user);
expect(credentials.real_groups).toBeDefined();
expect(credentials.real_groups).toEqual([
'test',
'$all',
'$authenticated',
'@all',
'@authenticated',
'all',
]);
expect(credentials.groups).toEqual([
'company-role1',
'company-role2',
'test',
'$all',
'$authenticated',
'@all',
'@authenticated',
'all',
]);
});
});
});
});

@ -61,7 +61,7 @@ const checkDefaultConfPackages = (config) => {
expect(config.url_prefix).toBeUndefined();
expect(config.url_prefix).toBeUndefined();
expect(config.security).toEqual({
api: { legacy: true },
api: { legacy: true, migrateToSecureLegacySignature: false },
web: { sign: { expiresIn: '1h' }, verify: {} },
});
};

@ -1 +1 @@
{"list":[],"secret":"12c39716d7c75d50b9988255fff332e1b066bad04e10fff9cba42434bc5fe19e"}
{"list":[],"secret":"12c39716d7c75d50b9988255fff332e1"}

BIN
yarn.lock

Binary file not shown.