diff --git a/packages/api/src/user.ts b/packages/api/src/user.ts index 0f9dd5389..10b09681d 100644 --- a/packages/api/src/user.ts +++ b/packages/api/src/user.ts @@ -2,7 +2,8 @@ import _ from 'lodash'; import Cookies from 'cookies'; import { Response, Router } from 'express'; -import { createRemoteUser, createSessionToken, getApiToken, getAuthenticatedMessage, validatePassword, ErrorCode } from '@verdaccio/utils'; +import { createRemoteUser, createSessionToken, getAuthenticatedMessage, validatePassword, ErrorCode } from '@verdaccio/utils'; +import { getApiToken } from '@verdaccio/auth'; import { logger } from '@verdaccio/logger'; import { Config, RemoteUser } from '@verdaccio/types'; diff --git a/packages/api/src/v1/token.ts b/packages/api/src/v1/token.ts index edf2ba3fb..fd0b7db4e 100644 --- a/packages/api/src/v1/token.ts +++ b/packages/api/src/v1/token.ts @@ -1,6 +1,7 @@ import _ from 'lodash'; import { HTTP_STATUS, SUPPORT_ERRORS } from '@verdaccio/dev-commons'; -import {ErrorCode, stringToMD5, mask, getApiToken } from '@verdaccio/utils'; +import {ErrorCode, stringToMD5, mask } from '@verdaccio/utils'; +import { getApiToken } from '@verdaccio/auth'; import { logger } from '@verdaccio/logger'; import { Response, Router } from 'express'; diff --git a/packages/auth/src/auth.ts b/packages/auth/src/auth.ts index 13e829440..ac7a0214d 100644 --- a/packages/auth/src/auth.ts +++ b/packages/auth/src/auth.ts @@ -1,28 +1,28 @@ import _ from 'lodash'; import { NextFunction } from 'express'; -import { VerdaccioError } from '@verdaccio/commons-api'; +import { VerdaccioError, getBadRequest, getInternalError, getForbidden } from '@verdaccio/commons-api'; import {API_ERROR, SUPPORT_ERRORS, TOKEN_BASIC, TOKEN_BEARER} from '@verdaccio/dev-commons'; import { loadPlugin } from '@verdaccio/loaders'; import { aesEncrypt, signPayload } from '@verdaccio/utils'; import { getDefaultPlugins, - verifyJWTPayload, createAnonymousRemoteUser, - isAuthHeaderValid, - getSecurity, - isAESLegacy, convertPayloadToBase64, - ErrorCode, - parseAuthTokenHeader, - parseBasicPayload, createRemoteUser, } from '@verdaccio/utils'; import { getMatchedPackagesSpec } from '@verdaccio/utils'; import { Config, Logger, Callback, IPluginAuth, RemoteUser, JWTSignOptions, Security, AuthPluginPackage, AllowAccess, PackageAccess } from '@verdaccio/types'; import { $RequestExtend, $ResponseExtend, IAuth, AESPayload } from '@verdaccio/dev-types'; -import {getMiddlewareCredentials} from "./utils"; +import { + getMiddlewareCredentials, + getSecurity, + verifyJWTPayload, + parseBasicPayload, + parseAuthTokenHeader, + isAuthHeaderValid, + isAESLegacy } from "./utils"; /* eslint-disable @typescript-eslint/no-var-requires */ const LoggerApi = require('@verdaccio/logger'); @@ -68,7 +68,8 @@ class Auth implements IAuth { const validPlugins = _.filter(this.plugins, plugin => _.isFunction(plugin.changePassword)); if (_.isEmpty(validPlugins)) { - return cb(ErrorCode.getInternalError(SUPPORT_ERRORS.PLUGIN_MISSING_INTERFACE)); + return cb( + getInternalError(SUPPORT_ERRORS.PLUGIN_MISSING_INTERFACE)); } for (const plugin of validPlugins) { @@ -319,7 +320,7 @@ class Auth implements IAuth { if (!isAuthHeaderValid(authorization)) { this.logger.trace('api middleware auth heather is not valid'); - return next(ErrorCode.getBadRequest(API_ERROR.BAD_AUTH_HEADER)); + return next(getBadRequest(API_ERROR.BAD_AUTH_HEADER)); } const security: Security = getSecurity(this.config); @@ -363,7 +364,7 @@ class Auth implements IAuth { next(); } else { // with JWT throw 401 - next(ErrorCode.getForbidden(API_ERROR.BAD_USERNAME_PASSWORD)); + next(getForbidden(API_ERROR.BAD_USERNAME_PASSWORD)); } } } @@ -387,7 +388,7 @@ class Auth implements IAuth { ); } else { // we force npm client to ask again with basic authentication - return next(ErrorCode.getBadRequest(API_ERROR.BAD_AUTH_HEADER)); + return next(getBadRequest(API_ERROR.BAD_AUTH_HEADER)); } } @@ -421,7 +422,7 @@ class Auth implements IAuth { } if (!isAuthHeaderValid(authorization)) { - return next(ErrorCode.getBadRequest(API_ERROR.BAD_AUTH_HEADER)); + return next(getBadRequest(API_ERROR.BAD_AUTH_HEADER)); } const token = (authorization || '').replace(`${TOKEN_BEARER} `, ''); diff --git a/packages/auth/src/utils.ts b/packages/auth/src/utils.ts index b5241dee3..010a1ac55 100644 --- a/packages/auth/src/utils.ts +++ b/packages/auth/src/utils.ts @@ -1,15 +1,40 @@ -import {Security} from "@verdaccio/types"; -import {AuthMiddlewarePayload} from "@verdaccio/dev-types"; +import {Config, RemoteUser, Security} from "@verdaccio/types"; +import {AuthMiddlewarePayload, AuthTokenHeader, BasicPayload, IAuthWebUI} from "@verdaccio/dev-types"; import _ from "lodash"; -import {TOKEN_BEARER} from "@verdaccio/dev-commons"; +import {HTTP_STATUS, TOKEN_BASIC, TOKEN_BEARER} from "@verdaccio/dev-commons"; import { - isAESLegacy, - parseAESCredentials, - parseAuthTokenHeader, - parseBasicPayload, - verifyJWTPayload + aesDecrypt, + buildUserBuffer, + convertPayloadToBase64, + createAnonymousRemoteUser, + defaultSecurity, + ErrorCode, + verifyPayload } from "@verdaccio/utils"; +export function parseAuthTokenHeader(authorizationHeader: string): AuthTokenHeader { + const parts = authorizationHeader.split(' '); + const [scheme, token] = parts; + + return { scheme, token }; +} + +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 function getMiddlewareCredentials(security: Security, secret: string, authorizationHeader: string): AuthMiddlewarePayload { if (isAESLegacy(security)) { const credentials = parseAESCredentials(authorizationHeader, secret); @@ -30,3 +55,73 @@ export function getMiddlewareCredentials(security: Security, secret: string, aut return verifyJWTPayload(token, secret); } } + + +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 { + 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 getSecurity(config: Config): Security { + if (_.isNil(config.security) === false) { + return _.merge(defaultSecurity, config.security); + } + + return defaultSecurity; +} + +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) { + // #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 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 }; +} diff --git a/packages/auth/test/auth-utils.spec.ts b/packages/auth/test/auth-utils.spec.ts index b0636b38f..651623a66 100644 --- a/packages/auth/test/auth-utils.spec.ts +++ b/packages/auth/test/auth-utils.spec.ts @@ -8,17 +8,21 @@ import {setup} from '@verdaccio/logger'; import { buildUserBuffer, - getApiToken, getAuthenticatedMessage, - getSecurity, aesDecrypt, verifyPayload, buildToken, convertPayloadToBase64, - parseConfigFile + parseConfigFile, + createAnonymousRemoteUser, + createRemoteUser, + signPayload } from '@verdaccio/utils'; -import { getMiddlewareCredentials } from '../src' +import { + getMiddlewareCredentials, + getApiToken, + getSecurity } from '../src' import { IAuth } from '@verdaccio/dev-types'; import {Config, Security, RemoteUser} from '@verdaccio/types'; @@ -220,6 +224,20 @@ describe('Auth utilities', () => { }); }); + describe('verifyJWTPayload', () => { + test('should fail on verify the token and return anonymous users', () => { + expect(verifyJWTPayload('fakeToken', 'secret')).toEqual(createAnonymousRemoteUser()); + }); + + test('should fail on verify the token and return anonymous users', async () => { + const remoteUser = createRemoteUser('foo', []); + const token = await signPayload(remoteUser, '12345'); + const verifiedToken = verifyJWTPayload(token, '12345'); + expect(verifiedToken.groups).toEqual(remoteUser.groups); + expect(verifiedToken.name).toEqual(remoteUser.name); + }); + }); + describe('should get JWT credentials', () => { test('should return anonymous whether token is corrupted', () => { const config: Config = getConfig('security-jwt', '12345'); diff --git a/packages/utils/jest.config.js b/packages/utils/jest.config.js index 8bdede2e7..fcdb9d940 100644 --- a/packages/utils/jest.config.js +++ b/packages/utils/jest.config.js @@ -1,6 +1,5 @@ const config = require('../../jest/config'); module.exports = Object.assign({}, config, { - collectCoverage: true, - coveragePathIgnorePatterns: ['node_modules', 'fixtures'], + collectCoverage: false }); diff --git a/packages/utils/src/auth-utils.ts b/packages/utils/src/auth-utils.ts index 86141114d..99a96785d 100644 --- a/packages/utils/src/auth-utils.ts +++ b/packages/utils/src/auth-utils.ts @@ -1,12 +1,11 @@ import _ from 'lodash'; -import { API_ERROR, HTTP_STATUS, ROLES, TIME_EXPIRATION_7D, TOKEN_BASIC, TOKEN_BEARER, DEFAULT_MIN_LIMIT_PASSWORD } from '@verdaccio/dev-commons'; -import { CookieSessionToken, IAuthWebUI, AuthTokenHeader, BasicPayload } from '@verdaccio/dev-types'; +import { API_ERROR, ROLES, TIME_EXPIRATION_7D, DEFAULT_MIN_LIMIT_PASSWORD } from '@verdaccio/dev-commons'; +import { CookieSessionToken, AuthTokenHeader, BasicPayload } from '@verdaccio/dev-types'; import { RemoteUser, AllowAccess, PackageAccess, Callback, Config, Security, APITokenOptions, JWTOptions, IPluginAuth } from '@verdaccio/types'; import { VerdaccioError } from '@verdaccio/commons-api'; -import { convertPayloadToBase64, ErrorCode } from './utils'; -import { aesDecrypt, verifyPayload } from './crypto-utils'; +import { ErrorCode } from './utils'; import { logger } from '@verdaccio/logger'; @@ -152,14 +151,6 @@ export const defaultSecurity: Security = { api: defaultApiTokenConf, }; -export function getSecurity(config: Config): Security { - if (_.isNil(config.security) === false) { - return _.merge(defaultSecurity, config.security); - } - - return defaultSecurity; -} - export function getAuthenticatedMessage(user: string): string { return `you are authenticated as '${user}'`; } @@ -167,88 +158,3 @@ export function getAuthenticatedMessage(user: string): string { export function buildUserBuffer(name: string, password: string): Buffer { return Buffer.from(`${name}:${password}`, 'utf8'); } - -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 { - 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) { - // #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; -} diff --git a/packages/utils/test/auth-utils.spec.ts b/packages/utils/test/auth-utils.spec.ts index 8a640e641..66ec83c14 100644 --- a/packages/utils/test/auth-utils.spec.ts +++ b/packages/utils/test/auth-utils.spec.ts @@ -8,7 +8,6 @@ import { getDefaultPlugins, createSessionToken, getAuthenticatedMessage, - verifyJWTPayload, defaultNonLoggedUserRoles, signPayload } from "../src"; import { API_ERROR, ROLES } from "@verdaccio/dev-commons"; import { VerdaccioError, getForbidden } from "@verdaccio/commons-api"; @@ -164,20 +163,4 @@ describe('Auth Utilities', () => { expect(getAuthenticatedMessage('foo')).toEqual('you are authenticated as \'foo\''); }); }); - - describe('verifyJWTPayload', () => { - test('should fail on verify the token and return anonymous users', () => { - expect(verifyJWTPayload('fakeToken', 'secret')).toEqual(createAnonymousRemoteUser()); - }); - - test('should fail on verify the token and return anonymous users', async () => { - const remoteUser = createRemoteUser('foo', []); - const token = await signPayload(remoteUser, '12345'); - const verifiedToken = verifyJWTPayload(token, '12345'); - expect(verifiedToken.groups).toEqual(remoteUser.groups); - expect(verifiedToken.name).toEqual(remoteUser.name); - }); - }); - - // verifyJWTPayload }); diff --git a/packages/web/src/endpoint/user.ts b/packages/web/src/endpoint/user.ts index ad7a51138..3fe02c91b 100644 --- a/packages/web/src/endpoint/user.ts +++ b/packages/web/src/endpoint/user.ts @@ -5,7 +5,8 @@ import { Config, RemoteUser, JWTSignOptions } from '@verdaccio/types'; import { API_ERROR, APP_ERROR, HTTP_STATUS } from '@verdaccio/dev-commons'; import { IAuth, $NextFunctionVer } from '@verdaccio/dev-types'; -import { getSecurity, validatePassword, ErrorCode } from '@verdaccio/utils'; +import { validatePassword, ErrorCode } from '@verdaccio/utils'; +import { getSecurity } from '@verdaccio/auth'; function addUserAuthApi(route: Router, auth: IAuth, config: Config): void { route.post('/login', function(req: Request, res: Response, next: $NextFunctionVer): void {