0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2025-02-17 23:45:29 -05:00
verdaccio/packages/auth/src/utils.ts
Juan Picado f047cc8c25
refactor: auth with legacy sign support (#4113)
* refactor: auth with legacy sign support

refactor: auth with legacy sign support

add tests

add tests

clean up lock fil

clean up lock fil

add more ci to test

update ci

update ci

update ci

update ci

update ci

* chore: add test for deprecated legacy signature

* chore: add test for deprecated legacy signature

* chore: add test for deprecated legacy signature

* chore: add test for deprecated legacy signature

* chore: add test for deprecated legacy signature
2023-12-31 14:34:29 +01:00

252 lines
7.8 KiB
TypeScript

import buildDebug from 'debug';
import _ from 'lodash';
import { createAnonymousRemoteUser } from '@verdaccio/config';
import {
API_ERROR,
HTTP_STATUS,
TOKEN_BASIC,
TOKEN_BEARER,
errorUtils,
pluginUtils,
} from '@verdaccio/core';
import {
aesDecrypt,
aesDecryptDeprecated,
parseBasicPayload,
verifyPayload,
} from '@verdaccio/signature';
import { AuthPackageAllow, Config, Logger, RemoteUser, Security } from '@verdaccio/types';
import {
ActionsAllowed,
AllowAction,
AllowActionCallback,
AuthMiddlewarePayload,
AuthTokenHeader,
TokenEncryption,
} from './types';
const debug = buildDebug('verdaccio:auth:utils');
/**
* Split authentication header eg: Bearer [secret_token]
* @param authorizationHeader auth token
*/
export function parseAuthTokenHeader(authorizationHeader: string): AuthTokenHeader {
const parts = authorizationHeader.split(' ');
const [scheme, token] = parts;
return { scheme, token };
}
export function parseAESCredentials(
authorizationHeader: string,
secret: string,
enhanced: boolean
) {
debug('parseAESCredentials');
const { scheme, token } = parseAuthTokenHeader(authorizationHeader);
// basic is deprecated and should not be enforced
// basic is currently being used for functional test
if (scheme.toUpperCase() === TOKEN_BASIC.toUpperCase()) {
debug('legacy header basic');
const credentials = convertPayloadToBase64(token).toString();
return credentials;
} else if (scheme.toUpperCase() === TOKEN_BEARER.toUpperCase()) {
debug('legacy header bearer');
debug('legacy header enhanced?', enhanced);
const credentials = enhanced
? aesDecrypt(token.toString(), secret)
: // FUTURE: once deprecated legacy is removed this logic won't be longer need it
aesDecryptDeprecated(convertPayloadToBase64(token), secret).toString('utf-8');
return credentials;
}
}
export function getMiddlewareCredentials(
security: Security,
secretKey: string,
authorizationHeader: string,
enhanced: boolean = true
): AuthMiddlewarePayload {
debug('getMiddlewareCredentials');
// comment out for debugging purposes
if (isAESLegacy(security)) {
debug('is legacy');
const credentials = parseAESCredentials(authorizationHeader, secretKey, enhanced);
if (!credentials) {
debug('parse legacy credentials failed');
return;
}
const parsedCredentials = parseBasicPayload(credentials);
if (!parsedCredentials) {
debug('parse legacy basic payload credentials failed');
return;
}
return parsedCredentials;
}
const { scheme, token } = parseAuthTokenHeader(authorizationHeader);
debug('is jwt');
if (_.isString(token) && scheme.toUpperCase() === TOKEN_BEARER.toUpperCase()) {
return verifyJWTPayload(token, secretKey);
}
}
export function isAESLegacy(security: Security): boolean {
const { legacy, jwt } = security.api;
return _.isNil(legacy) === false && _.isNil(jwt) && legacy === true;
}
export async function getApiToken(
auth: TokenEncryption,
config: Config,
remoteUser: RemoteUser,
aesPassword: string
): Promise<string | void> {
debug('get api token');
const { security } = config;
if (isAESLegacy(security)) {
debug('security legacy enabled');
// fallback all goes to AES encryption
return await new Promise((resolve): void => {
resolve(auth.aesEncrypt(buildUser(remoteUser.name as string, aesPassword)));
});
}
const { jwt } = security.api;
if (jwt?.sign) {
return await auth.jwtEncrypt(remoteUser, jwt.sign);
}
return await new Promise((resolve): void => {
resolve(auth.aesEncrypt(buildUser(remoteUser.name as string, aesPassword)));
});
}
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 errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, error.message);
}
}
export function isAuthHeaderValid(authorization: string): boolean {
return authorization.split(' ').length === 2;
}
/**
* Return a default configuration for authentication if none is provided.
* @param logger {Logger}
* @returns object of default implementations.
*/
export function getDefaultPlugins(logger: Logger): pluginUtils.Auth<Config> {
return {
authenticate(_user: string, _password: string, cb: pluginUtils.AuthCallback): void {
debug('triggered default authenticate method');
cb(errorUtils.getForbidden(API_ERROR.BAD_USERNAME_PASSWORD));
},
adduser(_user: string, _password: string, cb: pluginUtils.AuthUserCallback): void {
debug('triggered default adduser method');
return cb(errorUtils.getConflict(API_ERROR.BAD_USERNAME_PASSWORD));
},
// @ts-ignore
allow_access: allow_action('access', logger),
// @ts-ignore
allow_publish: allow_action('publish', logger),
allow_unpublish: handleSpecialUnpublish(logger),
};
}
export function allow_action(action: ActionsAllowed, logger: Logger): AllowAction {
return function allowActionCallback(
user: RemoteUser,
pkg: AuthPackageAllow,
callback: AllowActionCallback
): void {
logger.trace({ remote: user.name }, `[auth/allow_action]: user: @{remote}`);
const { name, groups } = user;
debug('allow_action "%s": groups %s', action, groups);
const groupAccess = pkg[action] as string[];
debug('allow_action "%s": groupAccess %s', action, groupAccess);
const hasPermission = groupAccess.some((group) => {
return name === group || groups.includes(group);
});
debug('package "%s" has permission "%s"', name, hasPermission);
logger.trace(
{ pkgName: pkg.name, hasPermission, remote: user.name, groupAccess },
`[auth/allow_action]: hasPermission? @{hasPermission} for user: @{remote}, package: @{pkgName}`
);
if (hasPermission) {
logger.trace({ remote: user.name }, `auth/allow_action: access granted to: @{remote}`);
return callback(null, true);
}
if (name) {
callback(
errorUtils.getForbidden(`user ${name} is not allowed to ${action} package ${pkg.name}`)
);
} else {
callback(
errorUtils.getUnauthorized(`authorization required to ${action} package ${pkg.name}`)
);
}
};
}
/**
*
*/
export function handleSpecialUnpublish(logger: Logger): any {
return function (user: RemoteUser, pkg: AuthPackageAllow, callback: AllowActionCallback): void {
const action = 'unpublish';
// verify whether the unpublish prop has been defined
const isUnpublishMissing: boolean = !pkg[action];
debug('is unpublish method missing ? %s', isUnpublishMissing);
const hasGroups: boolean = isUnpublishMissing ? false : (pkg[action] as string[]).length > 0;
logger.trace(
{ user: user.name, name: pkg.name, hasGroups },
`fallback unpublish for @{name} has groups: @{hasGroups} for @{user}`
);
if (isUnpublishMissing || hasGroups === false) {
return callback(null, undefined);
}
logger.trace(
{ user: user.name, name: pkg.name, action, hasGroups },
`allow_action for @{action} for @{name} has groups: @{hasGroups} for @{user}`
);
return allow_action(action, logger)(user, pkg, callback);
};
}
export function buildUser(name: string, password: string): string {
return String(`${name}:${password}`);
}
export function convertPayloadToBase64(payload: string): Buffer {
return Buffer.from(payload, 'base64');
}