Skip to content

Commit

Permalink
feat: added async ability
Browse files Browse the repository at this point in the history
feat: added hexDigest bypass

feat: remove guard in favour of js thrown err

chore: standardise export name and allOptions

chore: some code style changes

feat: add async versions of core lib

feat: added async tests and async plugin

chore: mv code to utils and ignore internal export

fix: interface inheritance

chore: tests should all run as async
  • Loading branch information
yeojz committed Aug 25, 2019
1 parent 0c9174f commit a78d757
Show file tree
Hide file tree
Showing 52 changed files with 2,241 additions and 1,338 deletions.
6 changes: 2 additions & 4 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-typescript"
]
"presets": ["@babel/preset-env", "@babel/preset-typescript"],
"plugins": ["@babel/plugin-transform-runtime"]
}
5 changes: 3 additions & 2 deletions configs/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ function webpackConfig(config, helpers) {
use: {
loader: 'babel-loader',
options: {
babelrc: false,
presets: [
'@babel/preset-typescript',
['@babel/preset-env', { modules: false, ...config.presetEnv }]
['@babel/preset-env', { modules: false, ...config.presetEnv }],
'@babel/preset-typescript'
]
}
}
Expand Down
27 changes: 27 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@
"homepage": "https://yeojz.github.io/otplib",
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/plugin-transform-runtime": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@babel/preset-typescript": "^7.3.3",
"@babel/runtime": "^7.5.5",
"@types/crypto-js": "^3.1.43",
"@types/jest": "^24.0.16",
"@types/node": "^12.6.9",
Expand Down
16 changes: 16 additions & 0 deletions packages/otplib-authenticator-async/authenticator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Authenticator } from 'otplib-authenticator';
import { testSuiteAuthenticator } from 'tests-suites/core-authenticator';
import { testClassPropertiesEqual } from 'tests-suites/helpers';
import { AuthenticatorAsync } from './authenticator';

testClassPropertiesEqual<Authenticator, AuthenticatorAsync>(
Authenticator.name,
new Authenticator(),
AuthenticatorAsync.name,
new AuthenticatorAsync()
);

testSuiteAuthenticator<AuthenticatorAsync>(
'authenticator-async',
AuthenticatorAsync
);
107 changes: 107 additions & 0 deletions packages/otplib-authenticator-async/authenticator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { HexString, SecretKey } from 'otplib-hotp';
import {
AuthenticatorOptions,
Base32SecretKey,
authenticatorDecoder,
authenticatorEncoder
} from 'otplib-authenticator';
import {
TOTPAsync,
totpCheckWithWindowAsync,
totpDigestAsync
} from 'otplib-totp-async';
import { authenticatorOptions, totpToken } from 'otplib-core';

/**
* Allow AuthenticatorOptions to accept async method options.
*/
export type AuthenticatorAsyncOptions = AuthenticatorOptions<Promise<string>>;

/**
* Generates the digest for Authenticator based tokens.
*
* Uses [[totpDigestAsync]].
*/
export async function authenticatorDigestAsync<
T extends AuthenticatorAsyncOptions = AuthenticatorAsyncOptions
>(secret: Base32SecretKey, options: Readonly<T>): Promise<HexString> {
const decodedSecret = await authenticatorDecoder<T>(secret, options);
return totpDigestAsync<T>(decodedSecret, options);
}

/**
* Async version of [[authenticatorToken]].
*/
export async function authenticatorTokenAsync<
T extends AuthenticatorAsyncOptions = AuthenticatorAsyncOptions
>(secret: Base32SecretKey, options: Readonly<T>): Promise<string> {
const digest = await authenticatorDigestAsync<T>(secret, options);
return totpToken<T>(secret, { ...options, digest });
}

/**
* Async version of [[authenticatorCheckWithWindow]].
*/
export async function authenticatorCheckWithWindowAsync<
T extends AuthenticatorAsyncOptions = AuthenticatorAsyncOptions
>(
token: string,
secret: Base32SecretKey,
options: Readonly<T>
): Promise<number | null> {
const decodedSecret = await authenticatorDecoder<T>(secret, options);
return totpCheckWithWindowAsync<T>(token, decodedSecret, options);
}

export async function authenticatorGenerateSecretAsync<
T extends AuthenticatorAsyncOptions = AuthenticatorAsyncOptions
>(
numberOfBytes: number,
options: Pick<T, 'keyEncoder' | 'encoding' | 'createRandomBytes'>
): Promise<Base32SecretKey> {
const key = await options.createRandomBytes(numberOfBytes, options.encoding);
return authenticatorEncoder<T>(key, options);
}

/**
* Async version of [[Authenticator]].
*/
export class AuthenticatorAsync<
T extends AuthenticatorAsyncOptions = AuthenticatorAsyncOptions
> extends TOTPAsync<T> {
public create(defaultOptions: Partial<T> = {}): AuthenticatorAsync<T> {
return new AuthenticatorAsync<T>(defaultOptions);
}

public allOptions(): Readonly<T> {
return authenticatorOptions<T>(this.options);
}

public async generate(secret: SecretKey): Promise<string> {
return authenticatorTokenAsync<T>(secret, this.allOptions());
}

public async checkDelta(
token: string,
secret: SecretKey
): Promise<number | null> {
return authenticatorCheckWithWindowAsync(token, secret, this.allOptions());
}

public async encode(secret: SecretKey): Promise<Base32SecretKey> {
return authenticatorEncoder<T>(secret, this.allOptions());
}

public async decode(secret: Base32SecretKey): Promise<SecretKey> {
return authenticatorDecoder<T>(secret, this.allOptions());
}

public async generateSecret(
numberOfBytes: number = 10
): Promise<Base32SecretKey> {
return authenticatorGenerateSecretAsync<T>(
numberOfBytes,
this.allOptions()
);
}
}
1 change: 1 addition & 0 deletions packages/otplib-authenticator-async/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './authenticator';
148 changes: 15 additions & 133 deletions packages/otplib-authenticator/authenticator.test.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,13 @@
import * as totp from 'otplib-totp/totp';
import { HashAlgorithms } from 'otplib-hotp';
import { testSuiteAuthenticator } from 'tests-suites/core-authenticator';
import { runOptionValidator } from 'tests-suites/helpers';
import {
AuthenticatorOptions,
authenticatorOptionValidator,
Authenticator
} from './authenticator';

interface AuthenticatorTestCase {
decoded: string;
digest: string;
secret: string;
epoch: number;
token: string;
}

export const AUTHENTICATOR_DATASET: AuthenticatorTestCase[] = [
{
decoded: '68442f372b67474e2f47617679706f6e30756f51',
digest: '422eb1a849cf0650fef4dbdd8b0ee0fe57a87eb9',
epoch: 1565103854545,
secret: 'NBCC6NZLM5DU4L2HMF3HS4DPNYYHK32R',
token: '566155'
},
{
decoded: '68442f372b67474e2f47617679706f6e30756f51',
digest: 'c305b82dbf2a8d2d8a22e9d3992e4e666222d0e2',
secret: 'NBCC6NZLM5DU4L2HMF3HS4DPNYYHK32R',
epoch: 1565103878581,
token: '522154'
},
{
decoded: '636c6c4e506479436f314f6b4852623167564f76',
digest: '64a959e511420af1a406424f87b4412977b3cbd4',
secret: 'MNWGYTSQMR4UG3ZRJ5VUQUTCGFTVMT3W',
epoch: 1565103903110,
token: '540849'
}
];

const runOptionValidator = (
opt: Partial<AuthenticatorOptions>
): { error: boolean; message?: string } => {
try {
authenticatorOptionValidator(opt);
return {
error: false
};
} catch (err) {
return {
message: err.message,
error: true
};
}
};
testSuiteAuthenticator<Authenticator>('authenticator', Authenticator);

describe('authenticatorOptionsValidator', (): void => {
const totpOptionsValidator = jest.spyOn(totp, 'totpOptionsValidator');
Expand All @@ -62,99 +17,26 @@ describe('authenticatorOptionsValidator', (): void => {
});

test('missing options.keyDecoder, should throw error', (): void => {
const result = runOptionValidator({});
const result = runOptionValidator<AuthenticatorOptions>(
authenticatorOptionValidator,
{}
);

expect(result.error).toBe(true);
expect(result.message).toContain('options.keyDecoder');
});

test('non-function options.keyEncoder, should throw error', (): void => {
const result = runOptionValidator({
keyDecoder: (): string => '',
// @ts-ignore
keyEncoder: 'not-a-function'
});
const result = runOptionValidator<AuthenticatorOptions>(
authenticatorOptionValidator,
{
keyDecoder: (): string => '',
// @ts-ignore
keyEncoder: 'not-a-function'
}
);

expect(result.error).toBe(true);
expect(result.message).toContain('options.keyEncoder');
});
});

describe('Authenticator', (): void => {
let common: Partial<AuthenticatorOptions> = {};

beforeEach((): void => {
common = {
createDigest: (): string => '',
createHmacKey: (): string => '',
keyEncoder: jest.fn((): string => ''),
keyDecoder: jest.fn((): string => ''),
createRandomBytes: jest.fn()
};
});

test('given keyEncoder should be called', (): void => {
const instance = new Authenticator(common);
instance.encode('');

expect(common.keyEncoder).toHaveBeenCalledTimes(1);
});

test('given keyDecoder should be called', (): void => {
const instance = new Authenticator(common);
instance.decode('');

expect(common.keyDecoder).toHaveBeenCalledTimes(1);
});

test('given keyDecoder should be called', (): void => {
const instance = new Authenticator(common);
instance.generateSecret();

expect(common.createRandomBytes).toHaveBeenCalledTimes(1);
expect(common.keyEncoder).toHaveBeenCalledTimes(1);
});

AUTHENTICATOR_DATASET.forEach((entry): void => {
const instance = new Authenticator({
createDigest: (): string => entry.digest,
epoch: entry.epoch,
keyDecoder: (): string => entry.decoded
});

test(`[${entry.epoch}] check`, (): void => {
expect(instance.check(entry.token, entry.secret)).toBe(true);
});
});

test('calling clone returns a new instance with new set of defaults', (): void => {
const opt = {
algorithm: HashAlgorithms.SHA256
};

const instance = new Authenticator({});
instance.options = opt;
expect(instance.options).toEqual(opt);

const instance2 = instance.clone();
expect(instance2).toBeInstanceOf(Authenticator);
expect(instance.options).toEqual(opt);

const instance3 = instance.clone({ digits: 8 });
expect(instance.options).toEqual(opt);
expect(instance3.options).toEqual({ ...opt, digits: 8 });
});

test('calling create returns a new instance with new set of defaults', (): void => {
const opt = {
algorithm: HashAlgorithms.SHA256
};

const instance = new Authenticator(opt);
expect(instance.options).toEqual(opt);

const instance2 = instance.create();
expect(instance2).toBeInstanceOf(Authenticator);
expect(instance2.options).toEqual({});
});
});
Loading

0 comments on commit a78d757

Please sign in to comment.