Skip to content

Commit

Permalink
Merge pull request #181 from AliMD/feat/token
Browse files Browse the repository at this point in the history
New package: @alwatr/token
  • Loading branch information
alimd authored Aug 6, 2022
2 parents 736706c + f1240cc commit 359259c
Show file tree
Hide file tree
Showing 10 changed files with 331 additions and 1 deletion.
37 changes: 37 additions & 0 deletions demo/token/benchmark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {AlwatrTokenGenerator} from '@alwatr/token';

import type {DigestAlgorithm} from '@alwatr/token';


if (process.env.NODE_ENV !== 'production') {
console.log('Please run node in production for benchmark. NODE_ENV=production node demo/token/benchmark.js');
process.exit();
}

const tokenGenerator = new AlwatrTokenGenerator({
secret: 'my-very-secret-key',
duration: '1h',
algorithm: 'sha512',
encoding: 'base64',
});

const sampleData = 'Lorem ipsum dolor sit amet consectetur adipisicing elit.';

function benchmark(algorithm: DigestAlgorithm): void {
tokenGenerator.config.algorithm = algorithm;
const now = Date.now();
const testRun = 1_000_000;
let i = testRun;
for (; i > 0; i--) {
tokenGenerator.generate(sampleData);
}
const runPerSec = Math.round(testRun / (Date.now() - now) * 1000);
console.log(`Benchmark for ${algorithm} runs %s per sec`, runPerSec);
}

benchmark('md5');
benchmark('sha1');
benchmark('sha224');
benchmark('sha256');
benchmark('sha384');
benchmark('sha512');
58 changes: 58 additions & 0 deletions demo/token/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {createLogger} from '@alwatr/logger';
import {AlwatrTokenGenerator} from '@alwatr/token';

import type {TokenStatus} from '@alwatr/token';

const logger = createLogger('token/demo');

const tokenGenerator = new AlwatrTokenGenerator({
secret: 'my-very-secret-key',
duration: '2s',
algorithm: 'sha512',
encoding: 'base64url',
});

type User = {
id: string;
name: string;
role: 'admin' | 'user';
auth: string;
};

const user: User = {
id: 'alimd',
name: 'Ali Mihandoost',
role: 'admin',
auth: '', // Generated in first login
};

// ------

// For example when user authenticated we send user data contain valid auth token.
function login(): User {
user.auth = tokenGenerator.generate(`${user.id}-${user.role}`);
logger.logMethodFull('login', {}, {user});
return user;
}

// Now request received and we want to validate the token to ensure that the user is authenticated.
function userValidate(user: User): TokenStatus {
const validateStatus = tokenGenerator.verify(`${user.id}-${user.role}`, user.auth);
logger.logMethodFull('userValidate', {user}, {validateStatus});
return validateStatus;
}


// demo
const userData = login();
userValidate(userData); // { validateStatus: 'valid' }

setTimeout(() => {
// 2s later
userValidate(user); // { validateStatus: 'expired' }
}, 2001);

setTimeout(() => {
// 4s later
userValidate(user);
}, 4001); // { validateStatus: 'invalid' }
3 changes: 2 additions & 1 deletion demo/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
{"path": "../packages/core/i18n"},
{"path": "../packages/core/math"},
{"path": "../packages/core/element"},
{"path": "../packages/core/storage"}
{"path": "../packages/core/storage"},
{"path": "../packages/core/token"}
],
"exclude": ["*.d.ts", "node_modules"]
}
23 changes: 23 additions & 0 deletions packages/core/math/src/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,26 @@ export const random = {
*/
shuffle: <T>(array: T[]): T[] => array.sort(() => random.value - 0.5),
} as const;


export type DurationUnit = 's' | 'm' | 'h' | 'd' | 'w' | 'M' | 'y';
export type DurationString = `${number}${DurationUnit}`;
const unitConversion = {
s: 1_000,
m: 60_000,
h: 3_600_000,
d: 86_400_000,
w: 604_800_000,
M: 2_592_000_000,
y: 31_536_000_000,
};

export function parseDuration(duration: DurationString, unit: DurationUnit | 'ms' = 'ms'): number {
duration = duration.trim() as DurationString;
const durationNumber = +duration.substring(0, duration.length - 1).trimEnd(); // trimEnd for `10 m`
const durationUnit = duration.substring(duration.length - 1) as DurationUnit;
if (unitConversion[durationUnit] == null) {
throw new Error(`invalid_init`);
}
return durationNumber * unitConversion[durationUnit] / (unit === 'ms' ? 1 : unitConversion[unit]);
}
71 changes: 71 additions & 0 deletions packages/core/token/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# @alwatr/token

Secure authentication HOTP token generator (HMAC-based One-Time Password algorithm) written in tiny TypeScript module.

## Example

```ts
import {createLogger} from '@alwatr/logger';
import {AlwatrTokenGenerator} from '@alwatr/token';

import type {TokenStatus} from '@alwatr/token';

type User = {
id: string;
name: string;
role: 'admin' | 'user';
auth: string;
};

const logger = createLogger('token/demo');

const tokenGenerator = new AlwatrTokenGenerator({
secret: 'my-very-secret-key',
duration: '1h',
algorithm: 'sha512',
encoding: 'base64url',
});

const user: User = {
id: 'alimd',
name: 'Ali Mihandoost',
role: 'admin',
auth: '', // Generated in first login
};

// ------

// For example when user authenticated we send user data contain valid auth token.
function login(): User {
user.auth = tokenGenerator.generate(`${user.id}-${user.role}`);
logger.logMethodFull('login', {}, {user});
return user;
}

// Now request received and we want to validate the token to ensure that the user is authenticated.
function userValidate(user: User): TokenStatus {
const validateStatus = tokenGenerator.verify(`${user.id}-${user.role}`, user.auth);
logger.logMethodFull('userValidate', {user}, {validateStatus});
return validateStatus;
}

// demo
const userData = login();
userValidate(userData); // 'valid'

// one hour later
userValidate(user); // 'expired'

// one hours later
userValidate(user); // 'invalid'
```

## References

- [RFC 4226](http://tools.ietf.org/html/rfc4226). HMAC-Based One-Time Password Algorithm (HOTP)
- [RFC 6238](http://tools.ietf.org/html/rfc6238). Time-Based One-Time Password Algorithm (TOTP)
- [HMAC: Keyed-Hashing for Message Authentication](https://tools.ietf.org/html/rfc2104). (February 1997). Network Working Group.
- [HMAC and Key Derivation](https://cryptobook.nakov.com/mac-and-key-derivation/hmac-and-key-derivation). Practical Cryptography for Developers.
- [HMAC Generator/Tester Tool](https://www.freeformatter.com/hmac-generator.html). FreeFormatter.
- [How API Request Signing Works (And How to Implement HMAC in NodeJS)](https://blog.andrewhoang.me/how-api-request-signing-works-and-how-to-implement-it-in-nodejs-2/). (2016). Andrew Hoang.
- [Implement HMAC Authentication](https://support.google.com/admanager/answer/7637490?hl=en). Google Ad Manager Help.
47 changes: 47 additions & 0 deletions packages/core/token/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@alwatr/token",
"version": "0.12.0",
"description": "Secure authentication HOTP token generator (the HMAC-based One-Time Password algorithm) written in tiny TypeScript module.",
"keywords": [
"token",
"authentication",
"auth",
"access",
"token",
"hmac",
"hash",
"time",
"otp",
"hotp",
"otp-token",
"typescript",
"esm",
"alwatr"
],
"main": "token.js",
"type": "module",
"types": "token.d.ts",
"author": "S. Ali Mihandoost <ali.mihandoost@gmail.com>",
"contributors": [],
"license": "MIT",
"files": [
"**/*.{d.ts.map,d.ts,js.map,js,html,md}"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/AliMD/alwatr",
"directory": "packages/core/token"
},
"homepage": "https://github.com/AliMD/alwatr/tree/main/packages/core/token#readme",
"bugs": {
"url": "https://github.com/AliMD/alwatr/issues"
},
"dependencies": {
"tslib": "^2.3.1",
"@alwatr/logger": "^0.12.0",
"@alwatr/math": "^0.12.0"
}
}
48 changes: 48 additions & 0 deletions packages/core/token/src/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {createHmac} from 'node:crypto';

import {alwatrRegisteredList, createLogger} from '@alwatr/logger';
import {parseDuration} from '@alwatr/math';

import type {TokenGeneratorConfig, TokenStatus} from './type.js';

export * from './type.js';

alwatrRegisteredList.push({
name: '@alwatr/token',
version: '{{ALWATR_VERSION}}',
});

export class AlwatrTokenGenerator {
protected _logger = createLogger('alwatr-token-generator');
private _duration: number;

get epoch(): number {
return Math.floor(Date.now() / this._duration);
}

constructor(public config: TokenGeneratorConfig) {
this._logger.logMethodArgs('constructor', config);
this._duration = parseDuration(config.duration);
}

protected _generate(data: string, epoch: number): string {
return createHmac(this.config.algorithm, data)
.update(data + epoch)
.digest(this.config.encoding);
}

generate(data: string): string {
return this._generate(data, this.epoch);
}

verify(data: string, token: string): TokenStatus {
const epoch = this.epoch;
if (token === this._generate(data, epoch)) {
return 'valid';
} else if (token === this._generate(data, epoch - 1)) {
return 'expired';
} else {
return 'invalid';
}
}
}
28 changes: 28 additions & 0 deletions packages/core/token/src/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type {DurationString} from '@alwatr/math';

export type DigestAlgorithm = 'md5' | 'sha1' | 'sha224' | 'sha256' | 'sha384' | 'sha512';

export type TokenStatus = 'valid' | 'invalid' | 'expired';

export type TokenGeneratorConfig = {
/**
* Secret string data to generate token.
*/
secret: string;

/**
* Token expiration time.
*/
duration: DurationString;

/**
* OpenSSl digest algorithm.
*/
algorithm: DigestAlgorithm;

/**
* Encoding of token.
*/
encoding: 'base64' | 'base64url' | 'hex' | 'binary';
}

16 changes: 16 additions & 0 deletions packages/core/token/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": ".tsbuildinfo",
"rootDir": "src",
"outDir": "."
},
// files, include and exclude from the inheriting config are always overwritten.
"include": ["src/**/*"],
"exclude": [],
"references": [
{ "path": "../logger" },
{ "path": "../math" },
]
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
{"path": "./packages/core/math"},
{"path": "./packages/core/nano-server"},
{"path": "./packages/core/storage"},
{"path": "./packages/core/token"},
{"path": "./demo"}
]
}

0 comments on commit 359259c

Please sign in to comment.