2018-08-21 08:05:34 +02:00
|
|
|
import _ from 'lodash';
|
2021-03-14 08:42:46 +01:00
|
|
|
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';
|
2018-10-01 07:06:30 +02:00
|
|
|
import { convertPayloadToBase64, ErrorCode } from './utils';
|
2021-03-14 08:42:46 +01:00
|
|
|
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
|
|
|
|
2018-10-01 07:06:30 +02:00
|
|
|
import { aesDecrypt, verifyPayload } from './crypto-utils';
|
2018-07-03 07:54:24 +02:00
|
|
|
|
2021-03-14 08:42:46 +01:00
|
|
|
export function validatePassword(
|
2021-03-30 20:32:46 +02:00
|
|
|
password: string, // pragma: allowlist secret
|
2021-03-14 08:42:46 +01:00
|
|
|
minLength: number = DEFAULT_MIN_LIMIT_PASSWORD
|
|
|
|
): boolean {
|
2018-10-12 11:07:55 +02:00
|
|
|
return typeof password === 'string' && password.length >= minLength;
|
|
|
|
}
|
|
|
|
|
2018-08-21 08:05:34 +02:00
|
|
|
/**
|
|
|
|
* Create a RemoteUser object
|
|
|
|
* @return {Object} { name: xx, pluginGroups: [], real_groups: [] }
|
|
|
|
*/
|
2019-07-16 08:40:01 +02:00
|
|
|
export function createRemoteUser(name: string, pluginGroups: string[]): RemoteUser {
|
2018-09-21 17:34:12 +02:00
|
|
|
const isGroupValid: boolean = Array.isArray(pluginGroups);
|
2021-03-14 08:42:46 +01:00
|
|
|
const groups = (isGroupValid ? pluginGroups : []).concat([
|
|
|
|
ROLES.$ALL,
|
|
|
|
ROLES.$AUTH,
|
|
|
|
ROLES.DEPRECATED_ALL,
|
|
|
|
ROLES.DEPRECATED_AUTH,
|
|
|
|
ROLES.ALL
|
|
|
|
]);
|
2018-08-21 08:05:34 +02:00
|
|
|
|
|
|
|
return {
|
|
|
|
name,
|
|
|
|
groups,
|
2021-03-14 08:42:46 +01:00
|
|
|
real_groups: pluginGroups
|
2018-08-21 08:05:34 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
2018-09-28 11:35:21 +02:00
|
|
|
groups: [ROLES.$ALL, ROLES.$ANONYMOUS, ROLES.DEPRECATED_ALL, ROLES.DEPRECATED_ANONYMOUS],
|
2021-03-14 08:42:46 +01:00
|
|
|
real_groups: []
|
2018-08-21 08:05:34 +02:00
|
|
|
};
|
|
|
|
}
|
2018-07-17 20:33:51 +02:00
|
|
|
|
2019-07-16 08:40:01 +02:00
|
|
|
export function allow_action(action: string): Function {
|
2021-03-14 08:42:46 +01:00
|
|
|
return function (user: RemoteUser, pkg: Package, callback: Callback): void {
|
|
|
|
logger.trace({ remote: user.name }, `[auth/allow_action]: user: @{user.name}`);
|
2018-10-01 07:06:30 +02:00
|
|
|
const { name, groups } = user;
|
2019-08-10 13:38:06 +02:00
|
|
|
const groupAccess = pkg[action];
|
2021-03-14 08:42:46 +01:00
|
|
|
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-03 07:54:24 +02:00
|
|
|
|
2018-07-15 00:30:47 +02:00
|
|
|
if (hasPermission) {
|
2021-03-14 08:42:46 +01:00
|
|
|
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-03 07:54:24 +02:00
|
|
|
}
|
|
|
|
|
2018-07-15 00:30:47 +02:00
|
|
|
if (name) {
|
2021-03-14 08:42:46 +01:00
|
|
|
callback(
|
|
|
|
ErrorCode.getForbidden(`user ${name} is not allowed to ${action} package ${pkg.name}`)
|
|
|
|
);
|
2018-07-03 07:54:24 +02:00
|
|
|
} else {
|
2021-03-14 08:42:46 +01:00
|
|
|
callback(
|
|
|
|
ErrorCode.getUnauthorized(`authorization required to ${action} package ${pkg.name}`)
|
|
|
|
);
|
2018-07-03 07:54:24 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-08-10 13:38:06 +02:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
2019-07-16 08:40:01 +02:00
|
|
|
export function handleSpecialUnpublish(): any {
|
2021-03-14 08:42:46 +01:00
|
|
|
return function (user: RemoteUser, pkg: Package, callback: Callback): void {
|
2019-07-16 08:40:01 +02:00
|
|
|
const action = 'unpublish';
|
2019-08-10 13:38:06 +02:00
|
|
|
// verify whether the unpublish prop has been defined
|
|
|
|
const isUnpublishMissing: boolean = _.isNil(pkg[action]);
|
|
|
|
const hasGroups: boolean = isUnpublishMissing ? false : pkg[action].length > 0;
|
2021-03-14 08:42:46 +01:00
|
|
|
logger.trace(
|
|
|
|
{ user: user.name, name: pkg.name, hasGroups },
|
|
|
|
`fallback unpublish for @{name} has groups: @{hasGroups} for @{user}`
|
|
|
|
);
|
2019-02-24 23:20:25 +01:00
|
|
|
|
2019-08-10 13:38:06 +02:00
|
|
|
if (isUnpublishMissing || hasGroups === false) {
|
2019-02-24 23:20:25 +01:00
|
|
|
return callback(null, undefined);
|
|
|
|
}
|
|
|
|
|
2021-03-14 08:42:46 +01:00
|
|
|
logger.trace(
|
|
|
|
{ user: user.name, name: pkg.name, action, hasGroups },
|
|
|
|
`allow_action for @{action} for @{name} has groups: @{hasGroups} for @{user}`
|
|
|
|
);
|
2019-02-24 23:20:25 +01:00
|
|
|
return allow_action(action)(user, pkg, callback);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-03-30 20:32:46 +02:00
|
|
|
export function getDefaultPlugins(logger: any): IPluginAuth<Config> {
|
2018-07-03 07:54:24 +02:00
|
|
|
return {
|
2021-03-30 20:32:46 +02:00
|
|
|
authenticate(_user: string, _password: string, cb: Callback): void { // pragma: allowlist secret
|
2018-07-03 07:54:24 +02:00
|
|
|
cb(ErrorCode.getForbidden(API_ERROR.BAD_USERNAME_PASSWORD));
|
|
|
|
},
|
|
|
|
|
2021-03-30 20:32:46 +02:00
|
|
|
add_user(_user: string, _password: string, cb: Callback): void { // pragma: allowlist secret
|
2018-07-03 07:54:24 +02:00
|
|
|
return cb(ErrorCode.getConflict(API_ERROR.BAD_USERNAME_PASSWORD));
|
|
|
|
},
|
|
|
|
|
2019-07-16 08:40:01 +02:00
|
|
|
// FIXME: allow_action and allow_publish should be in the @verdaccio/types
|
|
|
|
// @ts-ignore
|
2021-03-30 20:32:46 +02:00
|
|
|
allow_access: allow_action('access', logger),
|
2019-07-16 08:40:01 +02:00
|
|
|
// @ts-ignore
|
2021-03-30 20:32:46 +02:00
|
|
|
allow_publish: allow_action('publish', logger),
|
2021-03-14 08:42:46 +01:00
|
|
|
allow_unpublish: handleSpecialUnpublish()
|
2018-07-03 07:54:24 +02:00
|
|
|
};
|
|
|
|
}
|
2018-08-21 08:05:34 +02:00
|
|
|
|
|
|
|
export function createSessionToken(): CookieSessionToken {
|
|
|
|
const tenHoursTime = 10 * 60 * 60 * 1000;
|
|
|
|
|
|
|
|
return {
|
|
|
|
// npmjs.org sets 10h expire
|
2021-03-14 08:42:46 +01:00
|
|
|
expires: new Date(Date.now() + tenHoursTime)
|
2018-08-21 08:05:34 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const defaultWebTokenOptions: JWTOptions = {
|
|
|
|
sign: {
|
2019-07-16 08:40:01 +02:00
|
|
|
// The expiration token for the website is 7 days
|
2021-03-14 08:42:46 +01:00
|
|
|
expiresIn: TIME_EXPIRATION_7D
|
2018-08-21 08:05:34 +02:00
|
|
|
},
|
2021-03-14 08:42:46 +01:00
|
|
|
verify: {}
|
2018-08-21 08:05:34 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
const defaultApiTokenConf: APITokenOptions = {
|
2021-03-14 08:42:46 +01:00
|
|
|
legacy: true
|
2018-08-21 08:05:34 +02:00
|
|
|
};
|
|
|
|
|
2019-07-16 08:40:01 +02:00
|
|
|
export const defaultSecurity: Security = {
|
|
|
|
web: defaultWebTokenOptions,
|
2021-03-14 08:42:46 +01:00
|
|
|
api: defaultApiTokenConf
|
2019-07-16 08:40:01 +02:00
|
|
|
};
|
2018-08-21 08:05:34 +02:00
|
|
|
|
2019-07-16 08:40:01 +02:00
|
|
|
export function getSecurity(config: Config): Security {
|
2018-08-21 08:05:34 +02:00
|
|
|
if (_.isNil(config.security) === false) {
|
|
|
|
return _.merge(defaultSecurity, config.security);
|
|
|
|
}
|
|
|
|
|
|
|
|
return defaultSecurity;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getAuthenticatedMessage(user: string): string {
|
|
|
|
return `you are authenticated as '${user}'`;
|
|
|
|
}
|
|
|
|
|
2019-07-16 08:40:01 +02:00
|
|
|
export function buildUserBuffer(name: string, password: string): Buffer {
|
|
|
|
return Buffer.from(`${name}:${password}`, 'utf8');
|
2018-08-21 08:05:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export function isAESLegacy(security: Security): boolean {
|
2018-10-01 07:06:30 +02:00
|
|
|
const { legacy, jwt } = security.api;
|
2018-08-21 08:05:34 +02:00
|
|
|
|
2018-09-28 11:35:21 +02:00
|
|
|
return _.isNil(legacy) === false && _.isNil(jwt) && legacy === true;
|
2018-08-21 08:05:34 +02:00
|
|
|
}
|
|
|
|
|
2021-03-14 08:42:46 +01:00
|
|
|
export async function getApiToken(
|
|
|
|
auth: IAuthWebUI,
|
|
|
|
config: Config,
|
|
|
|
remoteUser: RemoteUser,
|
|
|
|
aesPassword: string
|
|
|
|
): Promise<string> {
|
2018-08-21 08:05:34 +02:00
|
|
|
const security: Security = getSecurity(config);
|
|
|
|
|
|
|
|
if (isAESLegacy(security)) {
|
2018-09-28 11:35:21 +02:00
|
|
|
// fallback all goes to AES encryption
|
2019-07-16 08:40:01 +02:00
|
|
|
return await new Promise((resolve): void => {
|
2021-03-14 08:42:46 +01:00
|
|
|
resolve(
|
|
|
|
auth.aesEncrypt(buildUserBuffer(remoteUser.name as string, aesPassword)).toString('base64')
|
|
|
|
);
|
2018-08-21 08:05:34 +02:00
|
|
|
});
|
2020-01-12 14:44:22 +01:00
|
|
|
}
|
|
|
|
// i am wiling to use here _.isNil but flow does not like it yet.
|
|
|
|
const { jwt } = security.api;
|
2018-08-21 08:05:34 +02:00
|
|
|
|
2020-01-12 14:44:22 +01:00
|
|
|
if (jwt && jwt.sign) {
|
|
|
|
return await auth.jwtEncrypt(remoteUser, jwt.sign);
|
2018-08-21 08:05:34 +02:00
|
|
|
}
|
2020-01-12 14:44:22 +01:00
|
|
|
return await new Promise((resolve): void => {
|
2021-03-14 08:42:46 +01:00
|
|
|
resolve(
|
|
|
|
auth.aesEncrypt(buildUserBuffer(remoteUser.name as string, aesPassword)).toString('base64')
|
|
|
|
);
|
2020-01-12 14:44:22 +01:00
|
|
|
});
|
2018-08-21 08:05:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export function parseAuthTokenHeader(authorizationHeader: string): AuthTokenHeader {
|
|
|
|
const parts = authorizationHeader.split(' ');
|
|
|
|
const [scheme, token] = parts;
|
|
|
|
|
2018-10-01 07:06:30 +02:00
|
|
|
return { scheme, token };
|
2018-08-21 08:05:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
2018-10-01 07:06:30 +02:00
|
|
|
return { user, password };
|
2018-08-21 08:05:34 +02:00
|
|
|
}
|
|
|
|
|
2018-09-28 11:35:21 +02:00
|
|
|
export function parseAESCredentials(authorizationHeader: string, secret: string) {
|
2018-10-01 07:06:30 +02:00
|
|
|
const { scheme, token } = parseAuthTokenHeader(authorizationHeader);
|
2018-08-21 08:05:34 +02:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-16 08:40:01 +02:00
|
|
|
export const expireReasons: string[] = ['JsonWebTokenError', 'TokenExpiredError'];
|
2018-09-22 19:36:05 +02:00
|
|
|
|
2018-08-21 08:05:34 +02:00
|
|
|
export function verifyJWTPayload(token: string, secret: string): RemoteUser {
|
|
|
|
try {
|
2019-07-16 08:40:01 +02:00
|
|
|
const payload: RemoteUser = verifyPayload(token, secret);
|
2018-08-21 08:05:34 +02:00
|
|
|
|
|
|
|
return payload;
|
2018-09-21 17:34:12 +02:00
|
|
|
} catch (error) {
|
2018-08-21 08:05:34 +02:00
|
|
|
// #168 this check should be removed as soon AES encrypt is removed.
|
2018-09-22 19:36:05 +02:00
|
|
|
if (expireReasons.includes(error.name)) {
|
2018-08-21 08:05:34 +02:00
|
|
|
// 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();
|
|
|
|
}
|
2020-01-12 14:44:22 +01:00
|
|
|
throw ErrorCode.getCode(HTTP_STATUS.UNAUTHORIZED, error.message);
|
2018-08-21 08:05:34 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isAuthHeaderValid(authorization: string): boolean {
|
|
|
|
return authorization.split(' ').length === 2;
|
|
|
|
}
|
|
|
|
|
2021-03-14 08:42:46 +01:00
|
|
|
export function getMiddlewareCredentials(
|
|
|
|
security: Security,
|
|
|
|
secret: string,
|
|
|
|
authorizationHeader: string
|
|
|
|
): AuthMiddlewarePayload {
|
2018-08-21 08:05:34 +02:00
|
|
|
if (isAESLegacy(security)) {
|
|
|
|
const credentials = parseAESCredentials(authorizationHeader, secret);
|
|
|
|
if (!credentials) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const parsedCredentials = parseBasicPayload(credentials);
|
|
|
|
if (!parsedCredentials) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
return parsedCredentials;
|
2020-01-12 14:44:22 +01:00
|
|
|
}
|
|
|
|
const { scheme, token } = parseAuthTokenHeader(authorizationHeader);
|
2018-08-21 08:05:34 +02:00
|
|
|
|
2020-01-12 14:44:22 +01:00
|
|
|
if (_.isString(token) && scheme.toUpperCase() === TOKEN_BEARER.toUpperCase()) {
|
|
|
|
return verifyJWTPayload(token, secret);
|
2018-08-21 08:05:34 +02:00
|
|
|
}
|
|
|
|
}
|