Permalink
Browse files

feat(remote cli): Extracted jwtAuthStrategy and added tests

  • Loading branch information...
zakhenry committed Jul 26, 2016
1 parent 33e91f1 commit db3a366aa32b710afbc5edd4eeb73c568cc29385
@@ -24,8 +24,8 @@ export const banner = `
export function bannerBg(message: string = '$ Ubiquits Runtime CLI', bgString: string): string {
let shortMessage:string = '';
let longMessage:string = '';
let shortMessage: string = '';
let longMessage: string = '';
message = ` ${message} `;
@@ -0,0 +1,107 @@
import { AuthenticationStrategy, RemoteCliContext } from './remoteCli.service';
import { LoggerMock } from '../../common/services/logger.service.mock';
import { jwtAuthStrategyFactory } from './jwtAuthStrategy';
import * as chalk from 'chalk';
import Spy = jasmine.Spy;
describe('JWT Authentication Strategy', () => {
const loggerMock = new LoggerMock();
const authService = jasmine.createSpyObj('authService', ['verify']);
const payload = {
username: 'bob',
};
authService.verify.and.returnValue(Promise.resolve(payload));
const context: RemoteCliContext = {
logger: loggerMock,
authService: authService,
};
let authStrategy: AuthenticationStrategy;
beforeEach(() => {
authStrategy = jwtAuthStrategyFactory(context);
});
it('rejects callback when jwt is not passeed', () => {
const callbackSpy = jasmine.createSpy('cb');
authStrategy(null, null)({client: {}}, callbackSpy);
expect(callbackSpy)
.toHaveBeenCalledWith("JWT was not passed in connection request", false);
});
it('verifies with the passed jwt and authentication key path', (cb) => {
const jwt = 'pretend.this.is.a.jwt';
const publicKeyPath = './path/to/key';
const vantageScope = jasmine.createSpyObj('vantageScope', ['log']);
const authenticator = authStrategy(null, null)
.bind(vantageScope);
authenticator({client: {jwt, publicKeyPath, columns: 100}}, (message:string, isSuccess:boolean) => {
expect(authService.verify).toHaveBeenCalledWith(jwt, publicKeyPath);
expect(vantageScope.log)
.toHaveBeenCalledWith(chalk.grey(`You were authenticated with a JSON Web token verified against the public key at ${publicKeyPath}`));
expect(isSuccess).toBe(true);
expect(message).toBe(null);
cb();
});
});
it('rejects the callback if loading the auth service fails', (cb) => {
const vantageScope = jasmine.createSpyObj('vantageScope', ['log']);
const authenticator = authStrategy(null, null)
.bind(vantageScope);
const jwt = 'pretend.this.is.a.jwt';
const publicKeyPath = './path/to/key';
authService.verify.and.throwError('authentication lib error');
authenticator({client: {jwt, publicKeyPath}}, (message:string, isSuccess:boolean) => {
expect(isSuccess).toBe(false);
expect(message).toBe('authentication lib error');
cb();
});
});
it('rejects the callback when the authentication service errors', (cb) => {
const vantageScope = jasmine.createSpyObj('vantageScope', ['log']);
const authenticator = authStrategy(null, null)
.bind(vantageScope);
const jwt = 'pretend.this.is.a.jwt';
const publicKeyPath = './path/to/key';
authService.verify.and.returnValue(Promise.reject(new Error('authentication failed')));
authenticator({client: {jwt, publicKeyPath}}, (message:string, isSuccess:boolean) => {
expect(isSuccess).toBe(false);
expect(message).toBe('authentication failed');
cb();
});
});
});
@@ -0,0 +1,54 @@
/**
* @module server
*/
/** End Typedoc Module Declaration */
import { bannerBg } from '../../common/util/banner';
import * as chalk from 'chalk';
import {
AuthenticationStrategy,
RemoteCliContext,
AuthenticationStrategyFactory,
AuthenticationCallback
} from './remoteCli.service';
export const jwtAuthStrategyFactory:AuthenticationStrategyFactory = (remoteCliContext: RemoteCliContext): AuthenticationStrategy => {
return function (vantage: any, options: any) {
return function (args: {client: {jwt: string, publicKeyPath: string, columns: number}}, cb: AuthenticationCallback) {
try {
remoteCliContext.logger.silly.debug('Passed client arguments: ', args);
const token: string = args.client.jwt;
const keyPath: string = args.client.publicKeyPath;
if (!token) {
return cb("JWT was not passed in connection request", false);
}
remoteCliContext.logger.info(`Authenticating JSON web token against public key [${keyPath}]`);
remoteCliContext.authService.verify(token, keyPath)
.then((payload: any) => {
remoteCliContext.logger.info(`${payload.username} has been authenticated with token`)
.debug('Token:', token);
let displayBanner = `Hi ${payload.username}, Welcome to Ubiquits runtime cli.`;
if (args.client.columns >= 80) {
displayBanner = bannerBg(undefined, token);
}
this.log(chalk.grey(`You were authenticated with a JSON Web token verified against the public key at ${keyPath}`));
this.log(displayBanner);
this.log(` Type 'help' for a list of available commands`);
return cb(null, true);
})
.catch((e: Error) => {
return cb(e.message, false);
});
} catch (e) {
remoteCliContext.logger.error('Authentication error', e.message).debug(e.stack);
cb(e.message, false);
}
};
}
};
@@ -10,10 +10,11 @@ import { ServerMock } from '../servers/abstract.server.spec';
import { RemoteCliMock } from './remoteCli.service.mock';
import { AuthServiceMock } from './authService.service.mock';
import { AuthService } from './authService.service';
import * as chalk from 'chalk';
import Spy = jasmine.Spy;
describe('Remote Commands', () => {
describe('Remote CLI Commands', () => {
const vantageSpy = jasmine.createSpyObj('vantage', [
'delimiter', 'banner', 'command', 'description', 'action', 'listen', 'auth'
@@ -38,7 +39,7 @@ describe('Remote Commands', () => {
{
provide: RemoteCli,
deps: [Logger, Injector, AuthService],
useFactory: (logger: Logger, injector: Injector, authService:AuthService) => {
useFactory: (logger: Logger, injector: Injector, authService: AuthService) => {
return new mockedModule.RemoteCli(logger, injector, authService).initialize();
}
},
@@ -61,9 +62,14 @@ describe('Remote Commands', () => {
expect(vantageConstructorSpy)
.toHaveBeenCalled();
expect(vantageSpy.delimiter)
.toHaveBeenCalledWith('ubiquits-runtime~$');
.toHaveBeenCalledWith(chalk.magenta('ubiquits-runtime~$'));
//the banner is not called on init otherwise it would be output pre-initialization
expect(vantageSpy.banner)
.toHaveBeenCalledWith(jasmine.stringMatching('Welcome to Ubiquits runtime cli'));
.not
.toHaveBeenCalled();
expect(vantageSpy.auth)
.toHaveBeenCalled();
}));
@@ -76,7 +82,9 @@ describe('Remote Commands', () => {
const callbackLogFunction = vantageSpy.listen.calls.mostRecent().args[1];
const loggerSpy = spyOn((cli as any).logger, 'persistLog').and.callThrough();
const loggerSpy = spyOn((cli as any).logger, 'persistLog')
.and
.callThrough();
callbackLogFunction({conn: {remoteAddress: '127.0.0.1'}});
expect(loggerSpy)
@@ -17,6 +17,7 @@ const table: Table = require('table').default;
import Socket = SocketIO.Socket;
import { AuthService } from './authService.service';
import { jwtAuthStrategyFactory } from './jwtAuthStrategy';
export interface TableBorderTemplate {
@@ -73,6 +74,28 @@ export interface ConnectedSocketCallback {
(socket: Socket): void;
}
export interface RemoteCliContext {
logger: Logger;
authService: AuthService;
}
export interface AuthenticationStrategyFactory {
(remoteCliContext: RemoteCliContext): AuthenticationStrategy;
}
export interface Authenticator {
(args: any, cb: Function): void;
}
export interface AuthenticationStrategy {
(vantage: any, options?: any): Authenticator;
}
export interface AuthenticationCallback {
(errorMessage: string, isSuccessful: boolean): void;
}
/**
* Class allows developers to register custom commands that can be remote executed in a
* shell environment. Useful for things like migrations and debugging.
@@ -88,7 +111,7 @@ export class RemoteCli extends AbstractService {
/**
* Logger instance for the class, initialized with `remote-cli` source
*/
private logger: Logger;
protected logger: Logger;
constructor(loggerBase: Logger, private injector: Injector, protected authService: AuthService) {
super();
@@ -179,50 +202,15 @@ export class RemoteCli extends AbstractService {
return table(data, config);
}
/**
* Register the authentication strategy with vantage
*/
protected registerAuthenticationStrategy(): void {
this.vantage.auth((vantage: any, options: any) => {
const remoteCli = this;
return function (args: {client: {jwt: string, publicKeyPath: string, columns: number}}, cb: Function) {
try {
remoteCli.logger.silly.debug('Passed client arguments: ', args);
const token: string = args.client.jwt;
const keyPath: string = args.client.publicKeyPath;
if (!token) {
return cb("JWT was not passed in connection request", false);
}
remoteCli.logger.info(`Authenticating JSON web token against public key [${keyPath}]`);
remoteCli.authService.verify(token, keyPath)
.then((payload: any) => {
remoteCli.logger.info(`${payload.username} has been authenticated with token`)
.debug('Token:', token);
let displayBanner = `Hi ${payload.username}, Welcome to Ubiquits runtime cli.`;
this.log('columns', args.client.columns);
if (args.client.columns > 80) {
displayBanner = bannerBg(undefined, token);
}
this.log(chalk.grey(`You were authenticated with a JSON Web token verified against the public key at ${keyPath}`));
this.log(displayBanner);
this.log(` Type 'help' for a list of available commands`);
return cb(null, true);
})
.catch((e: Error) => {
return cb(e.message, false);
});
} catch (e) {
remoteCli.logger.error('Authentication error', e);
cb(null, false);
}
};
});
//when more auth strategies exist this should be refactored to injected class
const strategy:AuthenticationStrategy = jwtAuthStrategyFactory(this as any);
this.vantage.auth(strategy);
this.logger.debug('Registered vantage authentication strategy');
}

0 comments on commit db3a366

Please sign in to comment.