Skip to content

Commit

Permalink
Merge pull request #12 from jessety/dev/jesse/acceptTimestamp
Browse files Browse the repository at this point in the history
Support using the`timestamp` header as an alternative to `date`
  • Loading branch information
jessety authored Aug 21, 2022
2 parents 53de9bc + 15fba35 commit dcafdf0
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 13 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ Request signatures are designed to be used in conjunction with HTTPS.

### Headers

Each request requires three headers: `date`, `authorization` and `signature`. If the HTTP request contains a body, the `content-length` and `content-type` headers are also required.
Each request requires three headers: `authorization`, `signature` and either `date` or `timestamp`. If the HTTP request contains a body, the `content-length` and `content-type` headers are also required.

The `date` header is a standard [RFC-822 (updated in RFC-1123)](https://tools.ietf.org/html/rfc822#section-5) date, as per [RFC-7231](https://tools.ietf.org/html/rfc7231#section-7.1.1.2).
The `date` header is a standard [RFC-822 (updated in RFC-1123)](https://tools.ietf.org/html/rfc822#section-5) date, as per [RFC-7231](https://tools.ietf.org/html/rfc7231#section-7.1.1.2). Because it [cannot be programmatically set inside of a browser](https://fetch.spec.whatwg.org/#forbidden-header-name), the `timestamp` header may be substituted instead.

The `authorization` header is a standard as per [RFC-2617](https://tools.ietf.org/html/rfc2617#section-3.2.2) that, confusingly, is designed for authentication and not authorization. It should contain a string representation of the client's API key.

Expand Down
2 changes: 1 addition & 1 deletion examples/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const http = require('http');
const SimpleHMACAuth = require('../../');

const settings = {
port: 8080,
port: 8000,
secretsForAPIKeys: {
API_KEY: 'SECRET',
API_KEY_TWO: 'SECRET_TWO',
Expand Down
14 changes: 12 additions & 2 deletions src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ interface ClientSettings {
maxSockets?: number
host?: string
port?: number
ssl?: boolean,
ssl?: boolean
algorithm?: string
useDateHeader?: boolean
headers?: {[key: string]: string}
options?: http.RequestOptions | https.RequestOptions
}
Expand Down Expand Up @@ -119,6 +120,10 @@ class Client {
validatedSettings.maxSockets = 250;
}

if (typeof validatedSettings.useDateHeader !== 'boolean') {
validatedSettings.useDateHeader = false;
}

if (typeof validatedSettings.headers !== 'object') {
validatedSettings.headers = {};
}
Expand Down Expand Up @@ -246,7 +251,12 @@ class Client {
}

headers.authorization = `api-key ${apiKey}`;
headers.date = new Date().toUTCString();

if (this._settings.useDateHeader === true) {
headers.date = new Date().toUTCString();
} else {
headers.timestamp = new Date().toUTCString();
}

// Sort query keys alphabetically
const keys = Object.keys(query).sort();
Expand Down
10 changes: 6 additions & 4 deletions src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,19 +146,21 @@ class SimpleHMACAuth {
throw new AuthError(`Missing signature. Please sign all incoming requests with the 'signature' header.`, `SIGNATURE_HEADER_MISSING`);
}

if (request.headers.date === undefined) {
const timestamp = <string> request.headers.date ?? request.headers.timestamp;

throw new AuthError(`Missing timestamp. Please timestamp all incoming requests by including 'date' header.`, `DATE_HEADER_MISSING`);
if (timestamp === undefined) {

throw new AuthError(`Missing timestamp. Please timestamp all incoming requests by including either the 'date' or 'timestamp' header.`, `DATE_HEADER_MISSING`);
}

// First, confirm that the 'date' header is recent enough
const requestTime = new Date(request.headers.date);
const requestTime = new Date(timestamp);
const now = new Date();

// If this request was made over [60] seconds ago, ignore it
if ((now.getTime() / 1000) - (requestTime.getTime() / 1000) > (this.options.permittedTimestampSkew / 1000)) {

const error = new AuthError(`Timestamp is too old. Recieved: "${request.headers.date}" current time: "${now.toUTCString()}"`, `DATE_HEADER_INVALID`);
const error = new AuthError(`Timestamp is too old. Received: "${request.headers.date}" current time: "${now.toUTCString()}"`, `DATE_HEADER_INVALID`);
error.time = now.toUTCString();

throw error;
Expand Down
7 changes: 4 additions & 3 deletions src/canonicalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import crypto from 'crypto';
// Only sign these headers
export const headerWhitelist = [
'authorization',
'timestamp',
'date',
'content-length',
'content-type'
Expand All @@ -25,7 +26,7 @@ export const headerWhitelist = [
*/
export function canonicalize(method: string, uri: string, queryString = '', headers: {[key: string]: string}, data?: string): string {

// Hash the method, the path, aplhabetically sorted headers, alphabetically sorted GET parameters, and body data
// Hash the method, the path, alphabetically sorted headers, alphabetically sorted GET parameters, and body data

method = method.toUpperCase();

Expand Down Expand Up @@ -61,7 +62,7 @@ export function canonicalize(method: string, uri: string, queryString = '', head
// Sort the header keys alphabetically
headerKeys.sort();

// Create a string of all headers, arranged alphabetically, seperated by newlines
// Create a string of all headers, arranged alphabetically, separated by newlines
let headerString = '';

for (const [ index, key ] of headerKeys.entries()) {
Expand All @@ -87,7 +88,7 @@ export function canonicalize(method: string, uri: string, queryString = '', head
method + \n
URL + \n
Alphabetically sorted query string with individually escaped keys and values + \n
Alphabetically sorted headers with lower case keys, seperated by newlines + \n
Alphabetically sorted headers with lower case keys, separated by newlines + \n
Hash of body, or hash of blank string if body is empty
Or:
Expand Down
44 changes: 44 additions & 0 deletions test/Client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe('Client class', () => {
expect(client._settings.timeout).toBe(7500);
expect(client._settings.maxSockets).toBe(250);
expect(client.agent.maxSockets).toBe(250);
expect(client._settings.useDateHeader).toBe(false);
expect(client._settings.headers).toEqual({});
expect(client._settings.options).toEqual({});
expect(client._settings.verbose).toBe(false);
Expand All @@ -27,6 +28,7 @@ describe('Client class', () => {
algorithm: 'sha512',
timeout: 30 * 1000,
maxSockets: 500,
useDateHeader: true,
headers: { 'x-custom-header': 'custom-value' },
options: {
timeout: 100
Expand All @@ -41,6 +43,7 @@ describe('Client class', () => {
expect(client._settings.timeout).toBe(30000);
expect(client._settings.maxSockets).toBe(500);
expect(client.agent.maxSockets).toBe(500);
expect(client._settings.useDateHeader).toBe(true);
expect(client._settings.headers).toEqual({ 'x-custom-header': 'custom-value' });
expect(client._settings.options).toEqual({ timeout: 100 });
expect(client._settings.verbose).toBe(true);
Expand Down Expand Up @@ -209,6 +212,47 @@ describe('Client class', () => {
server.close();
});

test('sends requests with the correct date header when specified', async () => {

const port = 6002;

const server = http.createServer((request, response) => {
response.write(JSON.stringify({
timestamp: request.headers.timestamp,
date: request.headers.date
}));
response.end();
});

const client = new Client('API_KEY', 'SECRET', {
ssl: false,
host: 'localhost',
port: port
});

server.listen(port);


// When not specified, assume `timestamp`
const one: any = await client.request({ method: 'GET', path: '/' });
expect(one.timestamp).toBeDefined();
expect(one.date).toBeUndefined();

// Respect the `date` preference when set to true
client._settings.useDateHeader = true;
const two: any = await client.request({ method: 'GET', path: '/' });
expect(two.timestamp).toBeUndefined();
expect(two.date).toBeDefined();

// Respect the `date` preference when set to false
client._settings.useDateHeader = false;
const three: any = await client.request({ method: 'GET', path: '/' });
expect(three.timestamp).toBeDefined();
expect(three.date).toBeUndefined();

server.close();
});

test('makes requests using headers declared in class instantiation as well as request calls', async () => {

expect.assertions(9);
Expand Down
2 changes: 1 addition & 1 deletion test/Server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ describe('Server class', () => {
}
});

test('rejects authentication when date header is missing', async () => {
test('rejects authentication when both the date and timestamp headers are missing', async () => {

expect.assertions(1);

Expand Down

0 comments on commit dcafdf0

Please sign in to comment.