0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2025-01-20 22:52:46 -05:00
verdaccio/src/lib/auth-utils.ts

306 lines
8.6 KiB
TypeScript
Raw Normal View History

import _ from 'lodash';
import {
RemoteUser,
Package,
Callback,
Config,
Security,
APITokenOptions,
JWTOptions,
IPluginAuth
} from '@verdaccio/types';
import {
CookieSessionToken,
IAuthWebUI,
AuthMiddlewarePayload,
AuthTokenHeader,
BasicPayload
} from '../../types';
import { logger } from '../lib/logger';
import { convertPayloadToBase64, ErrorCode } from './utils';
import {
API_ERROR,
HTTP_STATUS,
ROLES,
TIME_EXPIRATION_7D,
TOKEN_BASIC,
TOKEN_BEARER,
DEFAULT_MIN_LIMIT_PASSWORD
} from './constants';
2018-07-17 20:33:51 +02:00
import { aesDecrypt, verifyPayload } from './crypto-utils';
export function validatePassword(
password: string, // pragma: allowlist secret
minLength: number = DEFAULT_MIN_LIMIT_PASSWORD
): boolean {
return typeof password === 'string' && password.length >= minLength;
}
/**
* Create a RemoteUser object
* @return {Object} { name: xx, pluginGroups: [], real_groups: [] }
*/
export function createRemoteUser(name: string, pluginGroups: string[]): RemoteUser {
2018-09-21 17:34:12 +02:00
const isGroupValid: boolean = Array.isArray(pluginGroups);
const groups = (isGroupValid ? pluginGroups : []).concat([
ROLES.$ALL,
ROLES.$AUTH,
ROLES.DEPRECATED_ALL,
ROLES.DEPRECATED_AUTH,
ROLES.ALL
]);
return {
name,
groups,
real_groups: pluginGroups
};
}
/**
* Builds an anonymous remote user in case none is logged in.
* @return {Object} { name: xx, groups: [], real_groups: [] }
*/
export function createAnonymousRemoteUser(): RemoteUser {
return {
name: undefined,
// groups without '$' are going to be deprecated eventually
groups: [ROLES.$ALL, ROLES.$ANONYMOUS, ROLES.DEPRECATED_ALL, ROLES.DEPRECATED_ANONYMOUS],
real_groups: []
};
}
2018-07-17 20:33:51 +02:00
export function allow_action(action: string): Function {
return function (user: RemoteUser, pkg: Package, callback: Callback): void {
logger.trace({ remote: user.name }, `[auth/allow_action]: user: @{user.name}`);
const { name, groups } = user;
const groupAccess = pkg[action];
const hasPermission = groupAccess.some((group) => name === group || groups.includes(group));
logger.trace(
{ pkgName: pkg.name, hasPermission, remote: user.name, groupAccess },
`[auth/allow_action]: hasPermission? @{hasPermission} for user: @{user}`
);
2018-07-15 00:30:47 +02:00
if (hasPermission) {
logger.trace({ remote: user.name }, `auth/allow_action: access granted to: @{user}`);
2018-07-15 00:30:47 +02:00
return callback(null, true);
}
2018-07-15 00:30:47 +02:00
if (name) {
callback(
ErrorCode.getForbidden(`user ${name} is not allowed to ${action} package ${pkg.name}`)
);
} else {
callback(
ErrorCode.getUnauthorized(`authorization required to ${action} package ${pkg.name}`)
);
}
};
}
/**
*
*/
export function handleSpecialUnpublish(): any {
return function (user: RemoteUser, pkg: Package, callback: Callback): void {
const action = 'unpublish';
// verify whether the unpublish prop has been defined
const isUnpublishMissing: boolean = _.isNil(pkg[action]);
const hasGroups: boolean = isUnpublishMissing ? false : pkg[action].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)(user, pkg, callback);
};
}
export function getDefaultPlugins(logger: any): IPluginAuth<Config> {
return {
authenticate(_user: string, _password: string, cb: Callback): void { // pragma: allowlist secret
cb(ErrorCode.getForbidden(API_ERROR.BAD_USERNAME_PASSWORD));
},
add_user(_user: string, _password: string, cb: Callback): void { // pragma: allowlist secret
return cb(ErrorCode.getConflict(API_ERROR.BAD_USERNAME_PASSWORD));
},
// FIXME: allow_action and allow_publish should be in the @verdaccio/types
// @ts-ignore
allow_access: allow_action('access', logger),
// @ts-ignore
allow_publish: allow_action('publish', logger),
allow_unpublish: handleSpecialUnpublish()
};
}
export function createSessionToken(): CookieSessionToken {
const tenHoursTime = 10 * 60 * 60 * 1000;
return {
// npmjs.org sets 10h expire
expires: new Date(Date.now() + tenHoursTime)
};
}
const defaultWebTokenOptions: JWTOptions = {
sign: {
// The expiration token for the website is 7 days
expiresIn: TIME_EXPIRATION_7D
},
verify: {}
};
const defaultApiTokenConf: APITokenOptions = {
legacy: true
};
export const defaultSecurity: Security = {
web: defaultWebTokenOptions,
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}'`;
}
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<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;
2018-09-21 17:34:12 +02:00
} 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 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);
}
}