Skip to content

Commit

Permalink
feat(sdk-core): migrate middleware auth plugin to ES6 crypto base (#2113
Browse files Browse the repository at this point in the history
)

## Proposed change

feat(sdk-core): migrate middleware auth plugin to ES6 crypto base

## Related issues

- 🚀 Feature resolves #2110

<!-- Please make sure to follow the contributing guidelines on
https://github.com/amadeus-digital/Otter/blob/main/CONTRIBUTING.md -->
  • Loading branch information
kpanot authored Aug 30, 2024
2 parents afe641a + 414bab6 commit 07d8b27
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 44 deletions.
1 change: 1 addition & 0 deletions packages/@ama-sdk/core/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from './fetch-cache/index';
export * from './fetch-credentials/index';
export * from './json-token/index';
export * from './keepalive/index';
export * from './mgw-mdw-auth/index';
export * from './mock-intercept/index';
export * from './perf-metric/index';
export * from './pii-tokenizer/index';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Computes the SHA256 digest of the given string
* @param value Value to hash
*/
export async function sha256(value: string) {
const utf8 = new TextEncoder().encode(value);
const hashBuffer = await crypto.subtle.digest('SHA-256', utf8);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((bytes) => bytes.toString(16).padStart(2, '0'))
.join('');
return hashHex;
}

/**
* Generates hash-based message authentication code using cryptographic hash function HmacSHA256 and the provided
* secret key
* Should only be in a NodeJS MDW context
* @param value Value to hash
* @param secretKey Secret cryptographic key
*/
export async function hmacSHA256(value: string, secretKey: string) {
const enc = new TextEncoder();
const algorithm = { name: 'HMAC', hash: 'SHA-256' };

const key = await crypto.subtle.importKey('raw', enc.encode(secretKey), algorithm, false, ['sign', 'verify']);
const signature = await crypto.subtle.sign(algorithm.name, key, enc.encode(value));
const digest = btoa(String.fromCharCode(...new Uint8Array(signature)));
return digest;
}
Original file line number Diff line number Diff line change
@@ -1,44 +1,7 @@
import { v4 } from 'uuid';
import { base64EncodeUrl, createBase64Encoder, createBase64UrlEncoder } from '../../utils/json-token';
import { PluginRunner, RequestOptions, RequestPlugin } from '../core';
import type { createHmac as createHmacType, webcrypto } from 'node:crypto';


/**
* Computes the SHA256 digest of the given string
* @param value Value to hash
*/
export async function sha256(value: string) {
const utf8 = new TextEncoder().encode(value);
// TODO: Use new Ecmascript crypto feature to avoid "require" call (issue #2110)
// eslint-disable-next-line @typescript-eslint/no-require-imports
const hashBuffer = await (globalThis.crypto || (require('node:crypto').webcrypto as typeof webcrypto)).subtle.digest('SHA-256', utf8);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((bytes) => bytes.toString(16).padStart(2, '0'))
.join('');
return hashHex;
}

/**
* Generates hash-based message authentication code using cryptographic hash function HmacSHA256 and the provided
* secret key
* Should only be in a NodeJS MDW context
* @param value Value to hash
* @param secretKey Secret cryptographic key
*/
export function hmacSHA256(value: string, secretKey: string) {
try {
// TODO: Use new Ecmascript crypto feature to avoid "require" call (issue #2110)
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { createHmac }: { createHmac: typeof createHmacType } = require('node:crypto');
return createHmac('sha256', secretKey)
.update(value, 'latin1')
.digest('base64');
} catch (err) {
throw new Error('Crypto module is not available.');
}
}
import { hmacSHA256, sha256 } from './mgw-mdw-auth.helpers';

/**
* Type that represents context data.
Expand Down Expand Up @@ -230,9 +193,9 @@ export class MicroGatewayMiddlewareAuthenticationRequest implements RequestPlugi
* @param payload JWT payload
* @param secretKey secret key used to generate the signature
*/
private sign(payload: JsonTokenPayload, secretKey: string) {
private async sign(payload: JsonTokenPayload, secretKey: string) {
const message = `${this.base64UrlEncoder(JSON.stringify(jwsHeader))}.${this.base64UrlEncoder(JSON.stringify(payload))}`;
const signature = hmacSHA256(message, secretKey);
const signature = await hmacSHA256(message, secretKey);
const encodedSignature = base64EncodeUrl(signature);
return `${message}.${encodedSignature}`;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { base64EncodeUrl, createBase64Decoder, createBase64UrlDecoder, createBase64UrlEncoder } from '../../utils/json-token';
import { RequestOptions } from '../core';
import { hmacSHA256, sha256 } from './mgw-mdw-auth.helpers';
import {
hmacSHA256,
JsonTokenPayload, MicroGatewayMiddlewareAuthenticationRequest,
MicroGatewayMiddlewareAuthenticationRequestConstructor, sha256
JsonTokenPayload,
MicroGatewayMiddlewareAuthenticationRequest,
MicroGatewayMiddlewareAuthenticationRequestConstructor
} from './mgw-mdw-auth.request';

const authHeaderKey = 'Authorization';
Expand Down Expand Up @@ -31,6 +32,13 @@ const jsonAuthTokenOptions: MicroGatewayMiddlewareAuthenticationRequestConstruct
}
};

const hmacSHA256NodeImplementation = (value: string, secretKey: string) => {
const { createHmac } = require('node:crypto');
return createHmac('sha256', secretKey)
.update(value, 'latin1')
.digest('base64');
};

describe('JSON auth token request plugin', () => {

beforeEach(() => {
Expand Down Expand Up @@ -124,8 +132,28 @@ describe('JSON auth token request plugin', () => {
const secretKey = await sha256(jsonAuthTokenOptions.apiKey + (await sha256(jsonAuthTokenOptions.secret + payload.jti + payload.iat + routePath)));
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
const message = `${base64UrlEncoder(JSON.stringify(header))}.${base64UrlEncoder(JSON.stringify(payload))}`;
const signCheck = base64EncodeUrl(hmacSHA256(message, secretKey));
const signCheck = base64EncodeUrl(await hmacSHA256(message, secretKey));

expect(signature).toEqual(signCheck);
});

it('should have same hmacSHA256 of node implementation', async () => {
const base64URLDecoder = createBase64UrlDecoder();
const base64UrlEncoder = createBase64UrlEncoder();

const plugin = new MicroGatewayMiddlewareAuthenticationRequest(jsonAuthTokenOptions);
const result = await plugin.load().transform(options);
const token = result.headers.get(authHeaderKey).replace(authHeaderPrefix, '');

const tokenParts = token.split('.');
const header = JSON.parse(base64URLDecoder(tokenParts[0]));
const payload: JsonTokenPayload = JSON.parse(base64URLDecoder(tokenParts[1]));

// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
const secretKey = await sha256(jsonAuthTokenOptions.apiKey + (await sha256(jsonAuthTokenOptions.secret + payload.jti + payload.iat + routePath)));
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
const message = `${base64UrlEncoder(JSON.stringify(header))}.${base64UrlEncoder(JSON.stringify(payload))}`;

await expect(hmacSHA256(message, secretKey)).resolves.toBe(hmacSHA256NodeImplementation(message, secretKey));
});
});

0 comments on commit 07d8b27

Please sign in to comment.