2018-09-28 11:35:21 +02:00
|
|
|
/**
|
|
|
|
* @prettier
|
2018-10-01 07:06:30 +02:00
|
|
|
* @flow
|
2018-09-28 11:35:21 +02:00
|
|
|
*/
|
|
|
|
|
2018-08-21 08:05:34 +02:00
|
|
|
import _ from 'lodash';
|
2018-10-01 07:06:30 +02:00
|
|
|
import { convertPayloadToBase64, ErrorCode } from './utils';
|
|
|
|
import { API_ERROR, HTTP_STATUS, ROLES, TIME_EXPIRATION_7D, TOKEN_BASIC, TOKEN_BEARER, CHARACTER_ENCODING } from './constants';
|
2018-07-17 20:33:51 +02:00
|
|
|
|
2018-10-01 07:06:30 +02:00
|
|
|
import type { RemoteUser, Package, Callback, Config, Security, APITokenOptions, JWTOptions } from '@verdaccio/types';
|
|
|
|
import type { CookieSessionToken, IAuthWebUI, AuthMiddlewarePayload, AuthTokenHeader, BasicPayload } from '../../types';
|
|
|
|
import { aesDecrypt, verifyPayload } from './crypto-utils';
|
2018-07-03 07:54:24 +02:00
|
|
|
|
2018-08-21 08:05:34 +02:00
|
|
|
/**
|
|
|
|
* Create a RemoteUser object
|
|
|
|
* @return {Object} { name: xx, pluginGroups: [], real_groups: [] }
|
|
|
|
*/
|
|
|
|
export function createRemoteUser(name: string, pluginGroups: Array<string>): RemoteUser {
|
2018-09-21 17:34:12 +02:00
|
|
|
const isGroupValid: boolean = Array.isArray(pluginGroups);
|
2018-09-28 11:35:21 +02: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,
|
|
|
|
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
|
2018-09-28 11:35:21 +02:00
|
|
|
groups: [ROLES.$ALL, ROLES.$ANONYMOUS, ROLES.DEPRECATED_ALL, ROLES.DEPRECATED_ANONYMOUS],
|
2018-08-21 08:05:34 +02:00
|
|
|
real_groups: [],
|
|
|
|
};
|
|
|
|
}
|
2018-07-17 20:33:51 +02:00
|
|
|
|
|
|
|
export function allow_action(action: string) {
|
|
|
|
return function(user: RemoteUser, pkg: Package, callback: Callback) {
|
2018-10-01 07:06:30 +02:00
|
|
|
const { name, groups } = user;
|
2018-09-28 11:35:21 +02:00
|
|
|
const hasPermission = pkg[action].some(group => name === group || groups.includes(group));
|
2018-07-03 07:54:24 +02:00
|
|
|
|
2018-07-15 00:30:47 +02:00
|
|
|
if (hasPermission) {
|
|
|
|
return callback(null, true);
|
2018-07-03 07:54:24 +02:00
|
|
|
}
|
|
|
|
|
2018-07-15 00:30:47 +02:00
|
|
|
if (name) {
|
|
|
|
callback(ErrorCode.getForbidden(`user ${name} is not allowed to ${action} package ${pkg.name}`));
|
2018-07-03 07:54:24 +02:00
|
|
|
} else {
|
2018-07-15 00:30:47 +02:00
|
|
|
callback(ErrorCode.getForbidden(`unregistered users are not allowed to ${action} package ${pkg.name}`));
|
2018-07-03 07:54:24 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getDefaultPlugins() {
|
|
|
|
return {
|
2018-07-17 20:33:51 +02:00
|
|
|
authenticate(user: string, password: string, cb: Callback) {
|
2018-07-03 07:54:24 +02:00
|
|
|
cb(ErrorCode.getForbidden(API_ERROR.BAD_USERNAME_PASSWORD));
|
|
|
|
},
|
|
|
|
|
2018-07-17 20:33:51 +02:00
|
|
|
add_user(user: string, password: string, cb: Callback) {
|
2018-07-03 07:54:24 +02:00
|
|
|
return cb(ErrorCode.getConflict(API_ERROR.BAD_USERNAME_PASSWORD));
|
|
|
|
},
|
|
|
|
|
|
|
|
allow_access: allow_action('access'),
|
|
|
|
allow_publish: allow_action('publish'),
|
|
|
|
};
|
|
|
|
}
|
2018-08-21 08:05:34 +02:00
|
|
|
|
|
|
|
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: {
|
|
|
|
expiresIn: TIME_EXPIRATION_7D,
|
|
|
|
},
|
|
|
|
verify: {},
|
|
|
|
};
|
|
|
|
|
|
|
|
const defaultApiTokenConf: APITokenOptions = {
|
2018-09-28 11:35:21 +02:00
|
|
|
legacy: true,
|
|
|
|
sign: {},
|
2018-08-21 08:05:34 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
export function getSecurity(config: Config): Security {
|
|
|
|
const defaultSecurity: Security = {
|
|
|
|
web: defaultWebTokenOptions,
|
|
|
|
api: defaultApiTokenConf,
|
|
|
|
};
|
|
|
|
|
|
|
|
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) {
|
2018-09-21 17:34:12 +02:00
|
|
|
return Buffer.from(`${name}:${password}`, CHARACTER_ENCODING.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
|
|
|
}
|
|
|
|
|
2018-09-28 11:35:21 +02: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
|
|
|
|
return await new Promise(resolve => {
|
2018-08-21 08:05:34 +02:00
|
|
|
resolve(auth.aesEncrypt(buildUserBuffer((remoteUser: any).name, aesPassword)).toString('base64'));
|
|
|
|
});
|
|
|
|
} else {
|
2018-09-28 11:35:21 +02:00
|
|
|
// i am wiling to use here _.isNil but flow does not like it yet.
|
2018-10-01 07:06:30 +02:00
|
|
|
const { jwt } = security.api;
|
2018-08-21 08:05:34 +02:00
|
|
|
|
2018-09-21 17:34:12 +02:00
|
|
|
if (jwt && jwt.sign) {
|
2018-08-21 08:05:34 +02:00
|
|
|
return await auth.jwtEncrypt(remoteUser, jwt.sign);
|
|
|
|
} else {
|
2018-09-28 11:35:21 +02:00
|
|
|
return await new Promise(resolve => {
|
2018-08-21 08:05:34 +02:00
|
|
|
resolve(auth.aesEncrypt(buildUserBuffer((remoteUser: any).name, aesPassword)).toString('base64'));
|
|
|
|
});
|
2018-09-28 11:35:21 +02: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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-22 19:36:05 +02:00
|
|
|
export const expireReasons: Array<string> = ['JsonWebTokenError', 'TokenExpiredError'];
|
|
|
|
|
2018-08-21 08:05:34 +02:00
|
|
|
export function verifyJWTPayload(token: string, secret: string): RemoteUser {
|
|
|
|
try {
|
|
|
|
const payload: RemoteUser = (verifyPayload(token, secret): RemoteUser);
|
|
|
|
|
|
|
|
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();
|
|
|
|
} else {
|
2018-09-21 17:34:12 +02: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;
|
|
|
|
}
|
|
|
|
|
2018-09-28 11:35:21 +02: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;
|
|
|
|
} else {
|
2018-10-01 07:06:30 +02:00
|
|
|
const { scheme, token } = parseAuthTokenHeader(authorizationHeader);
|
2018-08-21 08:05:34 +02:00
|
|
|
|
|
|
|
if (_.isString(token) && scheme.toUpperCase() === TOKEN_BEARER.toUpperCase()) {
|
2018-09-28 11:35:21 +02:00
|
|
|
return verifyJWTPayload(token, secret);
|
2018-08-21 08:05:34 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|